@cashscript/utils 0.13.0-next.3 → 0.13.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,6 @@
1
+ export interface CompilerOptions {
2
+ enforceFunctionParameterTypes?: boolean;
3
+ }
1
4
  export interface AbiInput {
2
5
  name: string;
3
6
  type: string;
@@ -11,6 +14,7 @@ export interface DebugInformation {
11
14
  sourceMap: string;
12
15
  logs: readonly LogEntry[];
13
16
  requires: readonly RequireStatement[];
17
+ sourceTags?: string;
14
18
  }
15
19
  export interface LogEntry {
16
20
  ip: number;
@@ -39,6 +43,7 @@ export interface Artifact {
39
43
  compiler: {
40
44
  name: string;
41
45
  version: string;
46
+ options?: CompilerOptions;
42
47
  };
43
48
  updatedAt: string;
44
49
  }
@@ -1,6 +1,7 @@
1
1
  import { Script } from './script.js';
2
+ import { FullLocationData } from './types.js';
2
3
  export type LineToOpcodesMap = Record<string, Script>;
3
4
  export type LineToAsmMap = Record<string, string>;
4
- export declare function buildLineToOpcodesMap(bytecode: Script, sourceMap: string): LineToOpcodesMap;
5
- export declare function buildLineToAsmMap(bytecode: Script, sourceMap: string): LineToAsmMap;
6
- export declare function formatBitAuthScript(bytecode: Script, sourceMap: string, sourceCode: string): string;
5
+ export declare function buildLineToOpcodesMap(bytecode: Script, sourceMapOrLocationData: string | FullLocationData): LineToOpcodesMap;
6
+ export declare function buildLineToAsmMap(bytecode: Script, sourceMapOrLocationData: string | FullLocationData): LineToAsmMap;
7
+ export declare function formatBitAuthScript(bytecode: Script, sourceMap: string, sourceCode: string, sourceTags?: string): string;
@@ -1,33 +1,98 @@
1
+ import { range } from './data.js';
1
2
  import { scriptToBitAuthAsm } from './script.js';
2
- import { sourceMapToLocationData } from './source-map.js';
3
+ import { parseSourceTags, sourceMapToLocationData } from './source-map.js';
3
4
  import { PositionHint } from './types.js';
4
- export function buildLineToOpcodesMap(bytecode, sourceMap) {
5
- const locationData = sourceMapToLocationData(sourceMap);
6
- return locationData.reduce((lineToOpcodeMap, { location, positionHint }, index) => {
5
+ export function buildLineToOpcodesMap(bytecode, sourceMapOrLocationData) {
6
+ const locationData = typeof sourceMapOrLocationData === 'string' ? sourceMapToLocationData(sourceMapOrLocationData) : sourceMapOrLocationData;
7
+ return locationData.reduce((lineToOpcodeMap, singleLocation, index) => {
7
8
  const opcode = bytecode[index];
8
- const line = positionHint === PositionHint.END ? location?.end.line : location?.start.line;
9
+ const line = getDisplayLine(singleLocation);
9
10
  return {
10
11
  ...lineToOpcodeMap,
11
12
  [line]: [...(lineToOpcodeMap[line] || []), opcode],
12
13
  };
13
14
  }, {});
14
15
  }
15
- export function buildLineToAsmMap(bytecode, sourceMap) {
16
- const lineToOpcodesMap = buildLineToOpcodesMap(bytecode, sourceMap);
16
+ export function buildLineToAsmMap(bytecode, sourceMapOrLocationData) {
17
+ const lineToOpcodesMap = buildLineToOpcodesMap(bytecode, sourceMapOrLocationData);
17
18
  return Object.fromEntries(Object.entries(lineToOpcodesMap).map(([lineNumber, opcodeList]) => [lineNumber, scriptToBitAuthAsm(opcodeList)]));
18
19
  }
19
- export function formatBitAuthScript(bytecode, sourceMap, sourceCode) {
20
- const lineToAsmMap = buildLineToAsmMap(bytecode, sourceMap);
21
- const escapedSourceCode = sourceCode.replaceAll('/*', '\\/*').replaceAll('*/', '*\\/');
22
- const sourceCodeLines = escapedSourceCode.split('\n');
23
- const sourceCodeLineLengths = sourceCodeLines.map((line) => line.length);
24
- const bytecodeLineLengths = Object.values(lineToAsmMap).map((line) => line.length);
25
- const maxSourceCodeLength = Math.max(...sourceCodeLineLengths);
26
- const maxBytecodeLength = Math.max(...bytecodeLineLengths);
27
- const annotatedAsmLines = sourceCodeLines.map((line, index) => {
28
- const lineAsm = lineToAsmMap[index + 1];
29
- return `${(lineAsm || '').padEnd(maxBytecodeLength)} /* ${line.padEnd(maxSourceCodeLength)} */`;
20
+ export function formatBitAuthScript(bytecode, sourceMap, sourceCode, sourceTags) {
21
+ const locationData = sourceMapToLocationData(sourceMap);
22
+ const sourceLines = sourceCode.split('\n');
23
+ // Splice synthetic annotation lines (e.g. for-loop updates) into source and remap opcode lines
24
+ const insertions = buildInsertions(locationData, sourceLines, sourceTags);
25
+ const splicedSourceLines = spliceSyntheticSourceLines(sourceLines, insertions);
26
+ const splicedLocationData = updateLocationData(locationData, insertions);
27
+ // Group opcodes by display line and convert to ASM
28
+ const lineToAsm = buildLineToAsmMap(bytecode, splicedLocationData);
29
+ // Format output
30
+ const escapedLines = splicedSourceLines.map(escapeCommentChars);
31
+ const maxAsmLen = Math.max(...escapedLines.map((_, i) => (lineToAsm[i + 1] || '').length));
32
+ const maxSrcLen = Math.max(...escapedLines.map((l) => l.length));
33
+ return escapedLines.map((src, i) => {
34
+ const asm = lineToAsm[i + 1] || '';
35
+ return `${asm.padEnd(maxAsmLen)} /* ${src.padEnd(maxSrcLen)} */`;
36
+ }).join('\n');
37
+ }
38
+ // --- Helpers ---
39
+ function getDisplayLine(singleLocation) {
40
+ const { location, positionHint } = singleLocation;
41
+ return positionHint === PositionHint.END ? location.end.line : location.start.line;
42
+ }
43
+ function escapeCommentChars(text) {
44
+ return text.replaceAll('/*', '\\/*').replaceAll('*/', '*\\/');
45
+ }
46
+ function buildInsertions(locationData, sourceLines, sourceTags) {
47
+ const tags = (sourceTags ? parseSourceTags(sourceTags) : []);
48
+ return tags.map((tag) => {
49
+ const annotation = deriveTagLabel(tag, locationData, sourceLines);
50
+ const insertAfterLine = getDisplayLine(locationData[Math.max(tag.startIndex - 1, 0)]);
51
+ return { insertAfterLine, annotation, startIndex: tag.startIndex, endIndex: tag.endIndex };
30
52
  });
31
- return annotatedAsmLines.join('\n');
53
+ }
54
+ function spliceSyntheticSourceLines(sourceLines, insertions) {
55
+ return insertions.reduceRight((lines, ins) => [...lines.slice(0, ins.insertAfterLine), ins.annotation, ...lines.slice(ins.insertAfterLine)], sourceLines);
56
+ }
57
+ function updateLocationData(locationData, insertions) {
58
+ return insertions.reduceRight((location, insertion) => {
59
+ return location.map((entry, opcodeIndex) => {
60
+ const currentLineNumber = getDisplayLine(location[opcodeIndex]);
61
+ const updatedLineNumber = getUpdatedLineNumber(currentLineNumber, insertion, opcodeIndex);
62
+ if (updatedLineNumber === currentLineNumber)
63
+ return entry;
64
+ return {
65
+ location: {
66
+ start: { line: updatedLineNumber, column: 0 },
67
+ end: { line: updatedLineNumber, column: 0 },
68
+ },
69
+ positionHint: PositionHint.START,
70
+ };
71
+ });
72
+ }, locationData);
73
+ }
74
+ const getUpdatedLineNumber = (currentLineNumber, insertion, opcodeIndex) => {
75
+ const newLineNumber = insertion.insertAfterLine + 1;
76
+ const inTagRange = opcodeIndex >= insertion.startIndex && opcodeIndex <= insertion.endIndex;
77
+ if (inTagRange)
78
+ return newLineNumber;
79
+ if (currentLineNumber > insertion.insertAfterLine)
80
+ return currentLineNumber + 1;
81
+ return currentLineNumber;
82
+ };
83
+ /** Derive the annotation text (e.g. ">>> for-loop update (i = i + 1)") for a source tag. */
84
+ function deriveTagLabel(tag, locationData, sourceLines) {
85
+ const headerLine = getDisplayLine(locationData[tag.startIndex]);
86
+ // Find the column span of the update expression using single-line locations in the tag range
87
+ const singleLineLocations = range(tag.startIndex, tag.endIndex)
88
+ .map((idx) => locationData[idx].location)
89
+ .filter((loc) => loc.start.line === loc.end.line);
90
+ const startCol = Math.min(...singleLineLocations.map((loc) => loc.start.column));
91
+ const endCol = Math.max(...singleLineLocations.map((loc) => loc.end.column));
92
+ const expression = sourceLines[headerLine - 1].substring(startCol, endCol);
93
+ // Indentation: use first non-empty line after header
94
+ const bodyLine = sourceLines.slice(headerLine).find((l) => l.trim().length > 0);
95
+ const indent = bodyLine?.match(/^(\s*)/)?.[1] ?? '';
96
+ return `${indent}>>> for-loop update (${expression})`;
32
97
  }
33
98
  //# sourceMappingURL=bitauth-script.js.map
package/dist/data.d.ts CHANGED
@@ -5,3 +5,4 @@ export declare function decodeInt(encodedInt: Uint8Array, maxLength?: number): b
5
5
  export declare function encodeString(str: string): Uint8Array;
6
6
  export declare function decodeString(encodedString: Uint8Array): string;
7
7
  export declare function placeholder(size: number): Uint8Array;
8
+ export declare function range(start: number, end: number): number[];
package/dist/data.js CHANGED
@@ -34,4 +34,7 @@ export function decodeString(encodedString) {
34
34
  export function placeholder(size) {
35
35
  return new Uint8Array(size).fill(0);
36
36
  }
37
+ export function range(start, end) {
38
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
39
+ }
37
40
  //# sourceMappingURL=data.js.map
package/dist/script.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FullLocationData } from './types.js';
1
+ import { FullLocationData, SourceTagEntry } from './types.js';
2
2
  import { LogEntry, RequireStatement } from './artifact.js';
3
3
  export declare const Op: {
4
4
  [x: number]: string;
@@ -284,13 +284,14 @@ export declare function bytecodeToBitAuthAsm(bytecode: Uint8Array): string;
284
284
  export declare function countOpcodes(script: Script): number;
285
285
  export declare function calculateBytesize(script: Script): number;
286
286
  export declare function encodeNullDataScript(chunks: OpOrData[]): Uint8Array;
287
- export declare function generateRedeemScript(baseScript: Script, encodedConstructorArgs: Script): Script;
287
+ export declare function generateContractBytecodeScript(baseScript: Script, encodedConstructorArgs: Script): Script;
288
288
  interface OptimiseBytecodeResult {
289
289
  script: Script;
290
290
  locationData: FullLocationData;
291
291
  logs: LogEntry[];
292
292
  requires: RequireStatement[];
293
+ sourceTags: SourceTagEntry[];
293
294
  }
294
- export declare function optimiseBytecode(script: Script, locationData: FullLocationData, logs: LogEntry[], requires: RequireStatement[], constructorParamLength: number, runs?: number): OptimiseBytecodeResult;
295
+ export declare function optimiseBytecode(script: Script, locationData: FullLocationData, logs: LogEntry[], requires: RequireStatement[], sourceTags: SourceTagEntry[], constructorParamLength: number, runs?: number): OptimiseBytecodeResult;
295
296
  export declare function optimiseBytecodeOld(script: Script, runs?: number): Script;
296
297
  export {};
package/dist/script.js CHANGED
@@ -102,13 +102,13 @@ function getPushDataOpcode(data) {
102
102
  return Uint8Array.from([0x4c, byteLength]);
103
103
  throw Error('Pushdata too large');
104
104
  }
105
- export function generateRedeemScript(baseScript, encodedConstructorArgs) {
105
+ export function generateContractBytecodeScript(baseScript, encodedConstructorArgs) {
106
106
  return [...encodedConstructorArgs.slice().reverse(), ...baseScript];
107
107
  }
108
- export function optimiseBytecode(script, locationData, logs, requires, constructorParamLength, runs = 1000) {
108
+ export function optimiseBytecode(script, locationData, logs, requires, sourceTags, constructorParamLength, runs = 1000) {
109
109
  for (let i = 0; i < runs; i += 1) {
110
110
  const oldScript = script;
111
- const { script: newScript, locationData: newLocationData, logs: newLogs, requires: newRequires, } = replaceOps(script, locationData, logs, requires, constructorParamLength, optimisationReplacements);
111
+ const { script: newScript, locationData: newLocationData, logs: newLogs, requires: newRequires, sourceTags: newSourceTags, } = replaceOps(script, locationData, logs, requires, sourceTags, constructorParamLength, optimisationReplacements);
112
112
  // Break on fixed point
113
113
  if (scriptToAsm(oldScript) === scriptToAsm(newScript))
114
114
  break;
@@ -116,8 +116,9 @@ export function optimiseBytecode(script, locationData, logs, requires, construct
116
116
  locationData = newLocationData;
117
117
  logs = newLogs;
118
118
  requires = newRequires;
119
+ sourceTags = newSourceTags;
119
120
  }
120
- return { script, locationData, logs, requires };
121
+ return { script, locationData, logs, requires, sourceTags };
121
122
  }
122
123
  export function optimiseBytecodeOld(script, runs = 1000) {
123
124
  const optimisations = OptimisationsEquivFile
@@ -162,11 +163,12 @@ function replaceOpsOld(script, optimisations) {
162
163
  asm = asm.replace(/\s+/g, ' ').trim();
163
164
  return asmToScript(asm);
164
165
  }
165
- function replaceOps(script, locationData, logs, requires, constructorParamLength, optimisations) {
166
+ function replaceOps(script, locationData, logs, requires, sourceTags, constructorParamLength, optimisations) {
166
167
  let asm = scriptToAsm(script);
167
168
  let newLocationData = [...locationData];
168
169
  let newLogs = [...logs];
169
170
  let newRequires = [...requires];
171
+ let newSourceTags = [...sourceTags];
170
172
  optimisations.forEach(([pattern, replacement]) => {
171
173
  let processedAsm = '';
172
174
  let asmToSearch = asm;
@@ -249,6 +251,12 @@ function replaceOps(script, locationData, logs, requires, constructorParamLength
249
251
  }),
250
252
  };
251
253
  });
254
+ // Source tags use raw script indices (no constructor offset), so we adjust using scriptIndex directly
255
+ newSourceTags = newSourceTags.map((tag) => ({
256
+ ...tag,
257
+ startIndex: tag.startIndex >= scriptIndex ? Math.max(scriptIndex, tag.startIndex - lengthDiff) : tag.startIndex,
258
+ endIndex: tag.endIndex >= scriptIndex ? Math.max(scriptIndex, tag.endIndex - lengthDiff) : tag.endIndex,
259
+ }));
252
260
  // We add the replacement to the processed asm
253
261
  processedAsm = mergeAsm(processedAsm, replacement);
254
262
  // We do not add the matched pattern anywhere since it gets replaced
@@ -267,6 +275,7 @@ function replaceOps(script, locationData, logs, requires, constructorParamLength
267
275
  locationData: newLocationData,
268
276
  logs: newLogs,
269
277
  requires: newRequires,
278
+ sourceTags: newSourceTags,
270
279
  };
271
280
  }
272
281
  const getHighestEndLocation = (locations) => {
@@ -1,3 +1,5 @@
1
- import { FullLocationData } from './types.js';
1
+ import { FullLocationData, SourceTagEntry } from './types.js';
2
2
  export declare function generateSourceMap(locationData: FullLocationData): string;
3
3
  export declare const sourceMapToLocationData: (sourceMap: string) => FullLocationData;
4
+ export declare function parseSourceTags(sourceTags: string): SourceTagEntry[];
5
+ export declare function generateSourceTags(entries: SourceTagEntry[]): string;
@@ -1,4 +1,4 @@
1
- import { PositionHint } from './types.js';
1
+ import { PositionHint, SourceTagKind } from './types.js';
2
2
  /*
3
3
  * The source mappings for the bytecode use the following notation (similar to Solidity):
4
4
  *
@@ -100,4 +100,25 @@ const parsePositionHint = (hint) => {
100
100
  return PositionHint.START;
101
101
  return undefined;
102
102
  };
103
+ const SOURCE_TAG_KIND_VALUES = new Set(Object.values(SourceTagKind));
104
+ /*
105
+ * Format: "startIndex:endIndex:kind;..." e.g. "14:16:fu;38:42:fu"
106
+ * (currently only `fu` (For-Update) is supported as source tag kind)
107
+ */
108
+ export function parseSourceTags(sourceTags) {
109
+ if (!sourceTags)
110
+ return [];
111
+ return sourceTags.split(';').map((segment) => {
112
+ const [startStr, endStr, kindStr] = segment.split(':');
113
+ if (!SOURCE_TAG_KIND_VALUES.has(kindStr)) {
114
+ throw new Error(`Unknown source tag kind: ${kindStr}`);
115
+ }
116
+ return { startIndex: Number(startStr), endIndex: Number(endStr), kind: kindStr };
117
+ });
118
+ }
119
+ export function generateSourceTags(entries) {
120
+ if (entries.length === 0)
121
+ return '';
122
+ return entries.map((entry) => `${entry.startIndex}:${entry.endIndex}:${entry.kind}`).join(';');
123
+ }
103
124
  //# sourceMappingURL=source-map.js.map
package/dist/types.d.ts CHANGED
@@ -51,3 +51,11 @@ export declare enum PositionHint {
51
51
  START = 0,
52
52
  END = 1
53
53
  }
54
+ export declare enum SourceTagKind {
55
+ FOR_UPDATE = "fu"
56
+ }
57
+ export interface SourceTagEntry {
58
+ startIndex: number;
59
+ endIndex: number;
60
+ kind: SourceTagKind;
61
+ }
package/dist/types.js CHANGED
@@ -82,9 +82,9 @@ export function explicitlyCastable(from, to) {
82
82
  return false;
83
83
  }
84
84
  if (from instanceof BytesType) {
85
- // Can cast unbounded bytes or <=4 bytes to int
85
+ // Can cast any bytes to int
86
86
  if (to === PrimitiveType.INT)
87
- return !from.bound || from.bound <= 8;
87
+ return true;
88
88
  // Can't cast bytes to bool or string
89
89
  if (to === PrimitiveType.BOOL)
90
90
  return false;
@@ -191,4 +191,10 @@ export var PositionHint;
191
191
  PositionHint[PositionHint["START"] = 0] = "START";
192
192
  PositionHint[PositionHint["END"] = 1] = "END";
193
193
  })(PositionHint || (PositionHint = {}));
194
+ // Semantic tags for opcodes that need special treatment in debugging output (e.g. synthetic labels).
195
+ // Currently used for loop constructs where opcode order diverges from source line order.
196
+ export var SourceTagKind;
197
+ (function (SourceTagKind) {
198
+ SourceTagKind["FOR_UPDATE"] = "fu";
199
+ })(SourceTagKind || (SourceTagKind = {}));
194
200
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cashscript/utils",
3
- "version": "0.13.0-next.3",
3
+ "version": "0.13.0-next.5",
4
4
  "description": "CashScript utilities and types",
5
5
  "keywords": [
6
6
  "bitcoin cash",
@@ -48,5 +48,5 @@
48
48
  "typescript": "^5.9.2",
49
49
  "vitest": "^4.0.15"
50
50
  },
51
- "gitHead": "d4e9ee295be4f48ea89da87d551cf369d1b33871"
51
+ "gitHead": "672ddcaf8d8bd464e518c4902c4ee8062407512f"
52
52
  }