@cashscript/utils 0.13.2 → 0.14.0-next.1

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.
@@ -16,6 +16,19 @@ export interface DebugInformation {
16
16
  logs: readonly LogEntry[];
17
17
  requires: readonly RequireStatement[];
18
18
  sourceTags?: string;
19
+ functions?: readonly DebugFrame[];
20
+ }
21
+ export interface DebugFrame {
22
+ id: number;
23
+ name: string;
24
+ inputs: readonly AbiInput[];
25
+ bytecode: string;
26
+ sourceMap: string;
27
+ sourceTags?: string;
28
+ source?: string;
29
+ sourceFile?: string;
30
+ logs: readonly LogEntry[];
31
+ requires: readonly RequireStatement[];
19
32
  }
20
33
  export interface LogEntry {
21
34
  ip: number;
@@ -1,7 +1,2 @@
1
- import { Script } from './script.js';
2
- import { FullLocationData } from './types.js';
3
- export type LineToOpcodesMap = Record<string, Script>;
4
- export type LineToAsmMap = Record<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
+ import { DebugInformation } from './artifact.js';
2
+ export declare function formatBitAuthScript(debug: DebugInformation, sourceCode: string): string;
@@ -1,41 +1,170 @@
1
- import { range } from './data.js';
2
- import { scriptToBitAuthAsm } from './script.js';
1
+ import { hexToBin } from '@bitauth/libauth';
2
+ import { encodeInt, range } from './data.js';
3
+ import { Op, bytecodeToScript, scriptToBitAuthAsm } from './script.js';
3
4
  import { parseSourceTags, sourceMapToLocationData } from './source-map.js';
4
5
  import { PositionHint, SourceTagKind } from './types.js';
5
- export function buildLineToOpcodesMap(bytecode, sourceMapOrLocationData) {
6
- const locationData = typeof sourceMapOrLocationData === 'string' ? sourceMapToLocationData(sourceMapOrLocationData) : sourceMapOrLocationData;
7
- return locationData.reduce((lineToOpcodeMap, singleLocation, index) => {
8
- const opcode = bytecode[index];
9
- const line = getDisplayLine(singleLocation);
10
- return {
11
- ...lineToOpcodeMap,
12
- [line]: [...(lineToOpcodeMap[line] || []), opcode],
13
- };
14
- }, {});
6
+ export function formatBitAuthScript(debug, sourceCode) {
7
+ const sourceLines = sourceCode.split('\n');
8
+ const rows = walkScript({
9
+ script: bytecodeToScript(hexToBin(debug.bytecode)),
10
+ sourceMap: debug.sourceMap,
11
+ sourceTags: debug.sourceTags,
12
+ functions: debug.functions,
13
+ sourceLines,
14
+ startLine: 1,
15
+ endLine: sourceLines.length,
16
+ asmIndent: '',
17
+ });
18
+ return renderRows(rows);
15
19
  }
16
- export function buildLineToAsmMap(bytecode, sourceMapOrLocationData) {
17
- const lineToOpcodesMap = buildLineToOpcodesMap(bytecode, sourceMapOrLocationData);
18
- return Object.fromEntries(Object.entries(lineToOpcodesMap).map(([lineNumber, opcodeList]) => [lineNumber, scriptToBitAuthAsm(opcodeList)]));
20
+ function walkScript(params) {
21
+ const segments = segmentScript(params);
22
+ return renderSegments(segments, params);
23
+ }
24
+ function segmentScript(params) {
25
+ const { script, sourceLines } = params;
26
+ const locationData = sourceMapToLocationData(params.sourceMap);
27
+ const tags = parseSourceTags(params.sourceTags ?? '');
28
+ const frames = params.functions ?? [];
29
+ const segments = [];
30
+ let index = 0;
31
+ while (index < script.length) {
32
+ // Function definitions are always at the start of the script in sets of three: `<body> <id> OP_DEFINE` per frame
33
+ if (index < frames.length * 3) {
34
+ segments.push({ kind: 'functionDefinition', frame: frames[index / 3], location: locationData[index].location });
35
+ index += 3;
36
+ continue;
37
+ }
38
+ const tag = findTagAt(tags, index);
39
+ if (tag) {
40
+ segments.push(annotationSegment(script, tag, tags, locationData, sourceLines));
41
+ index = tag.endIndex + 1;
42
+ continue;
43
+ }
44
+ const endIndex = findLineGroupEnd(script, index, locationData, tags);
45
+ segments.push({ kind: 'lineGroup', asm: scriptToBitAuthAsm(script.slice(index, endIndex)), line: getDisplayLine(locationData[index]) });
46
+ index = endIndex;
47
+ }
48
+ return segments;
49
+ }
50
+ function annotationSegment(script, tag, tags, locationData, sourceLines) {
51
+ const { insertAfterLine, indent } = deriveAnchor(tag, tags, locationData, sourceLines);
52
+ return {
53
+ kind: 'annotation',
54
+ asm: scriptToBitAuthAsm(script.slice(tag.startIndex, tag.endIndex + 1)),
55
+ comment: `${indent}${tagDescription(tag, locationData, sourceLines)}`,
56
+ insertAfterLine,
57
+ };
58
+ }
59
+ // A line group runs until the next opcode that belongs to a tagged range or maps to a different source line
60
+ function findLineGroupEnd(script, start, locationData, tags) {
61
+ const line = getDisplayLine(locationData[start]);
62
+ const nextBoundary = range(start + 1, script.length - 1).find((index) => (getDisplayLine(locationData[index]) !== line || findTagAt(tags, index) !== undefined));
63
+ return nextBoundary ?? script.length;
64
+ }
65
+ function findTagAt(tags, index) {
66
+ return tags.find((tag) => index >= tag.startIndex && index <= tag.endIndex);
67
+ }
68
+ const BLANK_ROW = { asm: '', comment: '' };
69
+ function renderSegments(segments, params) {
70
+ const context = {
71
+ ...params,
72
+ functionSectionLines: deriveFunctionSectionLines(segments, params.sourceLines),
73
+ };
74
+ const initialState = { rows: [], lastRenderedLine: params.startLine - 1 };
75
+ const finalState = segments.reduce((state, segment) => renderSegment(state, segment, context), initialState);
76
+ // After the last opcode, fill in the remaining source lines (e.g. the contract's closing braces)
77
+ return [...finalState.rows, ...fillerRows(finalState.lastRenderedLine, params.endLine, context)];
78
+ }
79
+ function deriveFunctionSectionLines(segments, sourceLines) {
80
+ const localFunctionSegments = segments.filter((segment) => segment.kind === 'functionDefinition' && segment.frame.source === undefined);
81
+ return new Set(localFunctionSegments.flatMap(({ location }) => {
82
+ let lastLine = location.end.line;
83
+ while (lastLine < sourceLines.length && sourceLines[lastLine].trim() === '')
84
+ lastLine += 1;
85
+ return range(location.start.line, lastLine);
86
+ }));
87
+ }
88
+ function renderSegment(state, segment, context) {
89
+ if (segment.kind === 'functionDefinition')
90
+ return renderFunctionDefinition(state, segment, context);
91
+ if (segment.kind === 'annotation')
92
+ return renderAnnotation(state, segment, context);
93
+ return renderLineGroup(state, segment, context);
94
+ }
95
+ // The group's row renders its own source line, after filling in the opcode-less lines above it
96
+ function renderLineGroup(state, segment, context) {
97
+ return {
98
+ rows: [
99
+ ...state.rows,
100
+ ...fillerRows(state.lastRenderedLine, segment.line - 1, context),
101
+ { asm: context.asmIndent + segment.asm, comment: context.sourceLines[segment.line - 1] },
102
+ ],
103
+ lastRenderedLine: advanceTo(state.lastRenderedLine, segment.line, context),
104
+ };
105
+ }
106
+ // The `>>>` annotation row lands right after its anchor line
107
+ function renderAnnotation(state, segment, context) {
108
+ return {
109
+ rows: [
110
+ ...state.rows,
111
+ ...fillerRows(state.lastRenderedLine, segment.insertAfterLine, context),
112
+ { asm: context.asmIndent + segment.asm, comment: segment.comment },
113
+ ],
114
+ lastRenderedLine: advanceTo(state.lastRenderedLine, segment.insertAfterLine, context),
115
+ };
116
+ }
117
+ function renderFunctionDefinition(state, segment, context) {
118
+ const { frame, location } = segment;
119
+ const isImported = frame.sourceFile !== undefined;
120
+ const sourceLines = isImported ? frame.source.split('\n') : context.sourceLines;
121
+ const headerRows = isImported
122
+ ? [{ asm: '', comment: `>>> function ${frame.name} (imported from ${frame.sourceFile})` }]
123
+ : [];
124
+ return {
125
+ rows: [...state.rows, ...headerRows, ...buildFunctionSection(frame, location, sourceLines), BLANK_ROW],
126
+ lastRenderedLine: state.lastRenderedLine,
127
+ };
128
+ }
129
+ function buildFunctionSection(frame, location, sourceLines) {
130
+ const bodyScript = bytecodeToScript(hexToBin(frame.bytecode));
131
+ const defineAsm = scriptToBitAuthAsm([encodeInt(BigInt(frame.id)), Op.OP_DEFINE]);
132
+ const { start, end } = location;
133
+ if (end.line === start.line) {
134
+ const asm = ['<', scriptToBitAuthAsm(bodyScript), '>', defineAsm].filter((part) => part !== '').join(' ');
135
+ return [{ asm, comment: sourceLines[start.line - 1] }];
136
+ }
137
+ const bodyRows = walkScript({
138
+ script: bodyScript,
139
+ sourceMap: frame.sourceMap,
140
+ sourceTags: frame.sourceTags,
141
+ sourceLines,
142
+ startLine: start.line + 1,
143
+ endLine: end.line - 1,
144
+ asmIndent: ' ',
145
+ });
146
+ return [
147
+ { asm: '<', comment: sourceLines[start.line - 1] },
148
+ ...bodyRows,
149
+ { asm: `> ${defineAsm}`, comment: sourceLines[end.line - 1] },
150
+ ];
151
+ }
152
+ function fillerRows(afterLine, throughLine, context) {
153
+ return range(afterLine + 1, Math.min(throughLine, context.endLine))
154
+ .filter((line) => !context.functionSectionLines.has(line))
155
+ .map((line) => ({ asm: '', comment: context.sourceLines[line - 1] }));
156
+ }
157
+ function advanceTo(renderedLine, line, context) {
158
+ return Math.max(renderedLine, Math.min(line, context.endLine));
159
+ }
160
+ function renderRows(rows) {
161
+ const escapedRows = rows.map((row) => ({ asm: row.asm, comment: escapeCommentChars(row.comment) }));
162
+ const maxAsmLength = Math.max(...escapedRows.map((row) => row.asm.length));
163
+ const maxCommentLength = Math.max(...escapedRows.map((row) => row.comment.length));
164
+ return escapedRows
165
+ .map((row) => `${row.asm.padEnd(maxAsmLength)} /* ${row.comment.padEnd(maxCommentLength)} */`)
166
+ .join('\n');
19
167
  }
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
168
  function getDisplayLine(singleLocation) {
40
169
  const { location, positionHint } = singleLocation;
41
170
  return positionHint === PositionHint.END ? location.end.line : location.start.line;
@@ -53,14 +182,6 @@ const EPILOGUE_KINDS = [
53
182
  SourceTagKind.SCOPE_CLEANUP,
54
183
  SourceTagKind.LOOP_CONDITION,
55
184
  ];
56
- function buildInsertions(locationData, sourceLines, sourceTags) {
57
- const tags = (sourceTags ? parseSourceTags(sourceTags) : []);
58
- return tags.map((tag) => {
59
- const { insertAfterLine, indent } = deriveAnchor(tag, tags, locationData, sourceLines);
60
- const annotation = `${indent}${tagDescription(tag, locationData, sourceLines)}`;
61
- return { insertAfterLine, annotation, startIndex: tag.startIndex, endIndex: tag.endIndex };
62
- });
63
- }
64
185
  function deriveAnchor(tag, tags, locationData, sourceLines) {
65
186
  if (PROLOGUE_KINDS.includes(tag.kind)) {
66
187
  const prologueTags = tags.filter((t) => PROLOGUE_KINDS.includes(t.kind));
@@ -68,14 +189,14 @@ function deriveAnchor(tag, tags, locationData, sourceLines) {
68
189
  const lastPrologueOpcode = Math.max(...prologueTags.map((t) => t.endIndex));
69
190
  const firstBodyOpcode = lastPrologueOpcode + 1;
70
191
  const firstBodyLine = getDisplayLine(locationData[firstBodyOpcode]);
192
+ // `insertAfterLine` of `firstBodyLine - 1` lands all the prologue annotations directly above the
193
+ // first body statement, at its indentation.
71
194
  return {
72
- // `insertAfterLine` splices *after* a line, so `firstBodyLine - 1` lands all the prologue
73
- // annotations directly above the first body statement, at its indentation.
74
195
  insertAfterLine: firstBodyLine - 1,
75
196
  indent: lineIndent(sourceLines, firstBodyLine),
76
197
  };
77
198
  }
78
- // Scope cleanup and loop-back condition tags always get inserted right before the scope's closing brace.
199
+ // Scope cleanup and loop-back condition tags always land right before the scope's closing brace.
79
200
  if (EPILOGUE_KINDS.includes(tag.kind)) {
80
201
  const braceLine = getDisplayLine(locationData[tag.startIndex]);
81
202
  return {
@@ -88,35 +209,6 @@ function deriveAnchor(tag, tags, locationData, sourceLines) {
88
209
  indent: deriveIndent(getDisplayLine(locationData[tag.startIndex]), sourceLines),
89
210
  };
90
211
  }
91
- function spliceSyntheticSourceLines(sourceLines, insertions) {
92
- return insertions.reduceRight((lines, ins) => [...lines.slice(0, ins.insertAfterLine), ins.annotation, ...lines.slice(ins.insertAfterLine)], sourceLines);
93
- }
94
- function updateLocationData(locationData, insertions) {
95
- return insertions.reduceRight((location, insertion) => {
96
- return location.map((entry, opcodeIndex) => {
97
- const currentLineNumber = getDisplayLine(location[opcodeIndex]);
98
- const updatedLineNumber = getUpdatedLineNumber(currentLineNumber, insertion, opcodeIndex);
99
- if (updatedLineNumber === currentLineNumber)
100
- return entry;
101
- return {
102
- location: {
103
- start: { line: updatedLineNumber, column: 0 },
104
- end: { line: updatedLineNumber, column: 0 },
105
- },
106
- positionHint: PositionHint.START,
107
- };
108
- });
109
- }, locationData);
110
- }
111
- const getUpdatedLineNumber = (currentLineNumber, insertion, opcodeIndex) => {
112
- const newLineNumber = insertion.insertAfterLine + 1;
113
- const inTagRange = opcodeIndex >= insertion.startIndex && opcodeIndex <= insertion.endIndex;
114
- if (inTagRange)
115
- return newLineNumber;
116
- if (currentLineNumber > insertion.insertAfterLine)
117
- return currentLineNumber + 1;
118
- return currentLineNumber;
119
- };
120
212
  // e.g. ">>> for-loop update (i = i + 1)"
121
213
  function tagDescription(tag, locationData, sourceLines) {
122
214
  switch (tag.kind) {
@@ -1,2 +1,2 @@
1
- declare const _default: "\n# This file can be run with CashProof to prove that the optimisations preserve exact functionality\n# This includes most of CashScript's bytecode optimisations, although some are incompatible with CashProof\n\n# Hardcoded arithmetic\n# OP_NOT OP_IF <=> OP_NOTIF;\nOP_1 OP_ADD <=> OP_1ADD;\nOP_1 OP_SUB <=> OP_1SUB;\nOP_1 OP_NEGATE <=> OP_1NEGATE;\nOP_0 OP_NUMEQUAL OP_NOT <=> OP_0NOTEQUAL;\nOP_NUMEQUAL OP_NOT <=> OP_NUMNOTEQUAL;\nOP_SHA256 OP_SHA256 <=> OP_HASH256;\nOP_SHA256 OP_RIPEMD160 <=> OP_HASH160;\n\n# Hardcoded stack ops\nOP_2 OP_PICK OP_1 OP_PICK OP_3 OP_PICK <=> OP_3DUP OP_SWAP;\nOP_2 OP_PICK OP_2 OP_PICK OP_2 OP_PICK <=> OP_3DUP;\n\nOP_0 OP_PICK OP_2 OP_PICK <=> OP_2DUP OP_SWAP;\nOP_2 OP_PICK OP_4 OP_PICK <=> OP_2OVER OP_SWAP;\nOP_3 OP_PICK OP_3 OP_PICK <=> OP_2OVER;\n\nOP_2 OP_ROLL OP_3 OP_ROLL <=> OP_2SWAP OP_SWAP;\nOP_3 OP_ROLL OP_3 OP_ROLL <=> OP_2SWAP;\nOP_4 OP_ROLL OP_5 OP_ROLL <=> OP_2ROT OP_SWAP;\nOP_5 OP_ROLL OP_5 OP_ROLL <=> OP_2ROT;\n\nOP_0 OP_PICK <=> OP_DUP;\nOP_1 OP_PICK <=> OP_OVER;\nOP_0 OP_ROLL <=> ;\nOP_1 OP_ROLL <=> OP_SWAP;\nOP_2 OP_ROLL <=> OP_ROT;\nOP_DROP OP_DROP <=> OP_2DROP;\n\n# Secondary effects\nOP_DUP OP_SWAP <=> OP_DUP;\nOP_SWAP OP_SWAP <=> ;\nOP_2SWAP OP_2SWAP <=> ;\nOP_ROT OP_ROT OP_ROT <=> ;\nOP_2ROT OP_2ROT OP_2ROT <=> ;\nOP_OVER OP_OVER <=> OP_2DUP;\nOP_DUP OP_DROP <=> ;\nOP_DUP OP_NIP <=> ;\n\n# Enabling secondary effects\nOP_DUP OP_OVER <=> OP_DUP OP_DUP;\n\n# Merge OP_VERIFY\nOP_EQUAL OP_VERIFY <=> OP_EQUALVERIFY;\nOP_NUMEQUAL OP_VERIFY <=> OP_NUMEQUALVERIFY;\nOP_CHECKSIG OP_VERIFY <=> OP_CHECKSIGVERIFY;\n# OP_CHECKMULTISIG OP_VERIFY <=> OP_CHECKMULTISIGVERIFY;\nOP_CHECKDATASIG OP_VERIFY <=> OP_CHECKDATASIGVERIFY;\n\n# Remove/replace extraneous OP_SWAP\n# OP_SWAP OP_AND <=> OP_AND;\n# OP_SWAP OP_OR <=> OP_OR;\n# OP_SWAP OP_XOR <=> OP_XOR;\nOP_SWAP OP_ADD <=> OP_ADD;\nOP_SWAP OP_EQUAL <=> OP_EQUAL;\nOP_SWAP OP_NUMEQUAL <=> OP_NUMEQUAL;\nOP_SWAP OP_NUMNOTEQUAL <=> OP_NUMNOTEQUAL;\nOP_SWAP OP_GREATERTHANOREQUAL <=> OP_LESSTHANOREQUAL;\nOP_SWAP OP_LESSTHANOREQUAL <=> OP_GREATERTHANOREQUAL;\nOP_SWAP OP_GREATERTHAN <=> OP_LESSTHAN;\nOP_SWAP OP_LESSTHAN <=> OP_GREATERTHAN;\nOP_SWAP OP_DROP <=> OP_NIP;\nOP_SWAP OP_NIP <=> OP_DROP;\n\n# Remove/replace extraneous OP_DUP\n# OP_DUP OP_AND <=> ;\n# OP_DUP OP_OR <=> ;\nOP_DUP OP_DROP <=> ;\nOP_DUP OP_NIP <=> ;\n\n# Random optimisations (don't know what I'm targeting with this)\nOP_2DUP OP_DROP <=> OP_OVER;\nOP_2DUP OP_NIP <=> OP_DUP;\nOP_CAT OP_DROP <=> OP_2DROP;\nOP_NIP OP_DROP <=> OP_2DROP;\n\n# Far-fetched stuff\nOP_DUP OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_OVER OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_2 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_3 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_4 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_5 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_6 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_7 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_8 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_9 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_10 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_11 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_12 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_13 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_14 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_15 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_16 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\n\nOP_DUP OP_ROT OP_DROP <=> OP_NIP OP_DUP;\nOP_OVER OP_ROT OP_DROP <=> OP_SWAP;\nOP_2 OP_PICK OP_ROT OP_DROP <=> OP_NIP OP_OVER;\n\nOP_0 OP_NIP <=> OP_DROP OP_0;\nOP_1 OP_NIP <=> OP_DROP OP_1;\nOP_2 OP_NIP <=> OP_DROP OP_2;\nOP_3 OP_NIP <=> OP_DROP OP_3;\nOP_4 OP_NIP <=> OP_DROP OP_4;\nOP_5 OP_NIP <=> OP_DROP OP_5;\nOP_6 OP_NIP <=> OP_DROP OP_6;\nOP_7 OP_NIP <=> OP_DROP OP_7;\nOP_8 OP_NIP <=> OP_DROP OP_8;\nOP_9 OP_NIP <=> OP_DROP OP_9;\nOP_10 OP_NIP <=> OP_DROP OP_10;\nOP_11 OP_NIP <=> OP_DROP OP_11;\nOP_12 OP_NIP <=> OP_DROP OP_12;\nOP_13 OP_NIP <=> OP_DROP OP_13;\nOP_14 OP_NIP <=> OP_DROP OP_14;\nOP_15 OP_NIP <=> OP_DROP OP_15;\nOP_16 OP_NIP <=> OP_DROP OP_16;\n\nOP_2 OP_PICK OP_SWAP OP_2 OP_PICK OP_NIP <=> OP_DROP OP_2DUP;\n\n# .slice(0, x) optimisation & .slice(x, y.length) optimisation\nOP_0 OP_SPLIT OP_NIP <=> ;\nOP_SIZE OP_SPLIT OP_DROP <=> ;\n\n# These are new optimisations that we cannot prove since CashProof doesn't work any more\n# //////////////////////////////////////////////////////////////////////////////////////\n\nOP_LESSTHAN OP_NOT <=> OP_GREATERTHANOREQUAL;\nOP_GREATERTHAN OP_NOT <=> OP_LESSTHANOREQUAL;\nOP_LESSTHANOREQUAL OP_NOT <=> OP_GREATERTHAN;\nOP_GREATERTHANOREQUAL OP_NOT <=> OP_LESSTHAN;\n";
1
+ declare const _default: "\n# This file can be run with CashProof to prove that the optimisations preserve exact functionality\n# This includes most of CashScript's bytecode optimisations, although some are incompatible with CashProof\n\n# Hardcoded arithmetic\n# OP_NOT OP_IF <=> OP_NOTIF;\nOP_1 OP_ADD <=> OP_1ADD;\nOP_1 OP_SUB <=> OP_1SUB;\nOP_1 OP_NEGATE <=> OP_1NEGATE;\nOP_0 OP_NUMEQUAL OP_NOT <=> OP_0NOTEQUAL;\nOP_NUMEQUAL OP_NOT <=> OP_NUMNOTEQUAL;\nOP_SHA256 OP_SHA256 <=> OP_HASH256;\nOP_SHA256 OP_RIPEMD160 <=> OP_HASH160;\n\n# Hardcoded stack ops\nOP_2 OP_PICK OP_1 OP_PICK OP_3 OP_PICK <=> OP_3DUP OP_SWAP;\nOP_2 OP_PICK OP_2 OP_PICK OP_2 OP_PICK <=> OP_3DUP;\n\nOP_0 OP_PICK OP_2 OP_PICK <=> OP_2DUP OP_SWAP;\nOP_2 OP_PICK OP_4 OP_PICK <=> OP_2OVER OP_SWAP;\nOP_3 OP_PICK OP_3 OP_PICK <=> OP_2OVER;\n\nOP_2 OP_ROLL OP_3 OP_ROLL <=> OP_2SWAP OP_SWAP;\nOP_3 OP_ROLL OP_3 OP_ROLL <=> OP_2SWAP;\nOP_4 OP_ROLL OP_5 OP_ROLL <=> OP_2ROT OP_SWAP;\nOP_5 OP_ROLL OP_5 OP_ROLL <=> OP_2ROT;\n\nOP_0 OP_PICK <=> OP_DUP;\nOP_1 OP_PICK <=> OP_OVER;\nOP_0 OP_ROLL <=> ;\nOP_1 OP_ROLL <=> OP_SWAP;\nOP_2 OP_ROLL <=> OP_ROT;\nOP_DROP OP_DROP <=> OP_2DROP;\n\n# Secondary effects\nOP_DUP OP_SWAP <=> OP_DUP;\nOP_SWAP OP_SWAP <=> ;\nOP_2SWAP OP_2SWAP <=> ;\nOP_ROT OP_ROT OP_ROT <=> ;\nOP_2ROT OP_2ROT OP_2ROT <=> ;\nOP_OVER OP_OVER <=> OP_2DUP;\nOP_DUP OP_DROP <=> ;\nOP_DUP OP_NIP <=> ;\n\n# Enabling secondary effects\nOP_DUP OP_OVER <=> OP_DUP OP_DUP;\n\n# Merge OP_VERIFY\nOP_EQUAL OP_VERIFY <=> OP_EQUALVERIFY;\nOP_NUMEQUAL OP_VERIFY <=> OP_NUMEQUALVERIFY;\nOP_CHECKSIG OP_VERIFY <=> OP_CHECKSIGVERIFY;\n# OP_CHECKMULTISIG OP_VERIFY <=> OP_CHECKMULTISIGVERIFY;\nOP_CHECKDATASIG OP_VERIFY <=> OP_CHECKDATASIGVERIFY;\n\n# Remove/replace extraneous OP_SWAP\n# OP_SWAP OP_AND <=> OP_AND;\n# OP_SWAP OP_OR <=> OP_OR;\n# OP_SWAP OP_XOR <=> OP_XOR;\nOP_SWAP OP_ADD <=> OP_ADD;\nOP_SWAP OP_MUL <=> OP_MUL;\nOP_SWAP OP_EQUAL <=> OP_EQUAL;\nOP_SWAP OP_NUMEQUAL <=> OP_NUMEQUAL;\nOP_SWAP OP_NUMNOTEQUAL <=> OP_NUMNOTEQUAL;\nOP_SWAP OP_GREATERTHANOREQUAL <=> OP_LESSTHANOREQUAL;\nOP_SWAP OP_LESSTHANOREQUAL <=> OP_GREATERTHANOREQUAL;\nOP_SWAP OP_GREATERTHAN <=> OP_LESSTHAN;\nOP_SWAP OP_LESSTHAN <=> OP_GREATERTHAN;\nOP_SWAP OP_DROP <=> OP_NIP;\nOP_SWAP OP_NIP <=> OP_DROP;\n\n# Remove/replace extraneous OP_DUP\n# OP_DUP OP_AND <=> ;\n# OP_DUP OP_OR <=> ;\nOP_DUP OP_DROP <=> ;\nOP_DUP OP_NIP <=> ;\n\n# Random optimisations (don't know what I'm targeting with this)\nOP_2DUP OP_DROP <=> OP_OVER;\nOP_2DUP OP_NIP <=> OP_DUP;\nOP_CAT OP_DROP <=> OP_2DROP;\nOP_NIP OP_DROP <=> OP_2DROP;\n\n# Far-fetched stuff\nOP_DUP OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_OVER OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_2 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_3 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_4 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_5 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_6 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_7 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_8 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_9 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_10 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_11 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_12 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_13 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_14 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_15 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\nOP_16 OP_PICK OP_ROT OP_SWAP OP_DROP <=> OP_SWAP;\n\nOP_DUP OP_ROT OP_DROP <=> OP_NIP OP_DUP;\nOP_OVER OP_ROT OP_DROP <=> OP_SWAP;\nOP_2 OP_PICK OP_ROT OP_DROP <=> OP_NIP OP_OVER;\n\nOP_0 OP_NIP <=> OP_DROP OP_0;\nOP_1 OP_NIP <=> OP_DROP OP_1;\nOP_2 OP_NIP <=> OP_DROP OP_2;\nOP_3 OP_NIP <=> OP_DROP OP_3;\nOP_4 OP_NIP <=> OP_DROP OP_4;\nOP_5 OP_NIP <=> OP_DROP OP_5;\nOP_6 OP_NIP <=> OP_DROP OP_6;\nOP_7 OP_NIP <=> OP_DROP OP_7;\nOP_8 OP_NIP <=> OP_DROP OP_8;\nOP_9 OP_NIP <=> OP_DROP OP_9;\nOP_10 OP_NIP <=> OP_DROP OP_10;\nOP_11 OP_NIP <=> OP_DROP OP_11;\nOP_12 OP_NIP <=> OP_DROP OP_12;\nOP_13 OP_NIP <=> OP_DROP OP_13;\nOP_14 OP_NIP <=> OP_DROP OP_14;\nOP_15 OP_NIP <=> OP_DROP OP_15;\nOP_16 OP_NIP <=> OP_DROP OP_16;\n\nOP_2 OP_PICK OP_SWAP OP_2 OP_PICK OP_NIP <=> OP_DROP OP_2DUP;\n\n# .slice(0, x) optimisation & .slice(x, y.length) optimisation\nOP_0 OP_SPLIT OP_NIP <=> ;\nOP_SIZE OP_SPLIT OP_DROP <=> ;\n\n# These are new optimisations that we cannot prove since CashProof doesn't work any more\n# //////////////////////////////////////////////////////////////////////////////////////\n\nOP_LESSTHAN OP_NOT <=> OP_GREATERTHANOREQUAL;\nOP_GREATERTHAN OP_NOT <=> OP_LESSTHANOREQUAL;\nOP_LESSTHANOREQUAL OP_NOT <=> OP_GREATERTHAN;\nOP_GREATERTHANOREQUAL OP_NOT <=> OP_LESSTHAN;\n";
2
2
  export default _default;
@@ -57,6 +57,7 @@ OP_CHECKDATASIG OP_VERIFY <=> OP_CHECKDATASIGVERIFY;
57
57
  # OP_SWAP OP_OR <=> OP_OR;
58
58
  # OP_SWAP OP_XOR <=> OP_XOR;
59
59
  OP_SWAP OP_ADD <=> OP_ADD;
60
+ OP_SWAP OP_MUL <=> OP_MUL;
60
61
  OP_SWAP OP_EQUAL <=> OP_EQUAL;
61
62
  OP_SWAP OP_NUMEQUAL <=> OP_NUMEQUAL;
62
63
  OP_SWAP OP_NUMNOTEQUAL <=> OP_NUMNOTEQUAL;
@@ -41,6 +41,7 @@ const provableOptimisations = [
41
41
  ['OP_CHECKDATASIG OP_VERIFY', 'OP_CHECKDATASIGVERIFY'],
42
42
  // Remove/replace extraneous OP_SWAP
43
43
  ['OP_SWAP OP_ADD', 'OP_ADD'],
44
+ ['OP_SWAP OP_MUL', 'OP_MUL'],
44
45
  // This was added to keep the old behaviour while explicitly disallowing partial matches in the optimisation regex
45
46
  ['OP_SWAP OP_EQUALVERIFY', 'OP_EQUALVERIFY'],
46
47
  ['OP_SWAP OP_EQUAL', 'OP_EQUAL'],
package/dist/types.d.ts CHANGED
@@ -24,7 +24,8 @@ export declare enum PrimitiveType {
24
24
  PUBKEY = "pubkey",
25
25
  SIG = "sig",
26
26
  DATASIG = "datasig",
27
- ANY = "any"
27
+ ANY = "any",
28
+ VOID = "void"
28
29
  }
29
30
  export declare function explicitlyCastable(from?: Type, to?: Type): boolean;
30
31
  export declare function implicitlyCastable(actual?: Type, expected?: Type): boolean;
package/dist/types.js CHANGED
@@ -38,6 +38,7 @@ export var PrimitiveType;
38
38
  PrimitiveType["SIG"] = "sig";
39
39
  PrimitiveType["DATASIG"] = "datasig";
40
40
  PrimitiveType["ANY"] = "any";
41
+ PrimitiveType["VOID"] = "void";
41
42
  })(PrimitiveType || (PrimitiveType = {}));
42
43
  const ExplicitlyCastableTo = {
43
44
  [PrimitiveType.INT]: [PrimitiveType.INT, PrimitiveType.BOOL],
@@ -47,10 +48,14 @@ const ExplicitlyCastableTo = {
47
48
  [PrimitiveType.SIG]: [PrimitiveType.SIG],
48
49
  [PrimitiveType.DATASIG]: [PrimitiveType.DATASIG],
49
50
  [PrimitiveType.ANY]: [],
51
+ [PrimitiveType.VOID]: [],
50
52
  };
51
53
  export function explicitlyCastable(from, to) {
52
54
  if (!from || !to)
53
55
  return false;
56
+ // `void` is not a real value type, so it can never participate in a cast
57
+ if (from === PrimitiveType.VOID || to === PrimitiveType.VOID)
58
+ return false;
54
59
  // Tuples can't be cast
55
60
  if (from instanceof TupleType || to instanceof TupleType)
56
61
  return false;
@@ -104,6 +109,9 @@ export function explicitlyCastable(from, to) {
104
109
  export function implicitlyCastable(actual, expected) {
105
110
  if (!actual || !expected)
106
111
  return false;
112
+ // `void` is not a real value type, so it can never be assigned to or from (not even to `any`)
113
+ if (actual === PrimitiveType.VOID || expected === PrimitiveType.VOID)
114
+ return false;
107
115
  if (actual instanceof TupleType && expected instanceof TupleType) {
108
116
  const leftIsCompatible = implicitlyCastable(actual.leftType, expected.leftType);
109
117
  const rightIsCompatible = implicitlyCastable(actual.rightType, expected.rightType);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cashscript/utils",
3
- "version": "0.13.2",
3
+ "version": "0.14.0-next.1",
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": "a4f112abb0d035ecf018ebc694e2b081f25b3409"
51
+ "gitHead": "2fe39181a098c7c1139705d84c2065542f21f634"
52
52
  }