diff --git a/packages/cashc/src/Errors.ts b/packages/cashc/src/Errors.ts index aca72ce5d..12b51aa35 100644 --- a/packages/cashc/src/Errors.ts +++ b/packages/cashc/src/Errors.ts @@ -20,6 +20,7 @@ import { ContractNode, SliceNode, IntLiteralNode, + TupleAssignmentNode, } from './ast/AST.js'; import { Symbol, SymbolType } from './ast/SymbolTable.js'; import { Location } from './ast/Location.js'; @@ -283,6 +284,23 @@ export class DivisionByZeroError extends CashScriptError { } } +export class DuplicateTupleTargetError extends CashScriptError { + constructor( + node: TupleAssignmentNode, + name: string, + ) { + super(node, `Duplicate target '${name}' in tuple destructuring`); + } +} + +export class TupleTargetOrderError extends CashScriptError { + constructor( + node: TupleAssignmentNode, + ) { + super(node, 'Declaration targets must come before all reassignment targets in a tuple destructuring'); + } +} + export class ConstantModificationError extends CashScriptError { constructor(node: VariableDefinitionNode | ConstantDefinitionNode); constructor(node: Node, name: string); diff --git a/packages/cashc/src/ast/AST.ts b/packages/cashc/src/ast/AST.ts index 01f08e16c..5025bea9b 100644 --- a/packages/cashc/src/ast/AST.ts +++ b/packages/cashc/src/ast/AST.ts @@ -156,7 +156,12 @@ export class VariableDefinitionNode extends NonControlStatementNode implements N export interface TupleAssignmentTarget { name: string; - type: Type; + // For a fresh-declaration target (`int x`) this is the declared type. For a reassignment target + // (`x`, no type) it is undefined at parse time and filled in from the existing variable's symbol + // during SymbolTableTraversal. + type?: Type; + // True for a reassignment of an already-declared variable (no `typeName` in source). + isReassignment?: boolean; } export class TupleAssignmentNode extends NonControlStatementNode { diff --git a/packages/cashc/src/ast/AstBuilder.ts b/packages/cashc/src/ast/AstBuilder.ts index 05db951d4..222f95635 100644 --- a/packages/cashc/src/ast/AstBuilder.ts +++ b/packages/cashc/src/ast/AstBuilder.ts @@ -249,11 +249,17 @@ export default class AstBuilder visitTupleAssignment(ctx: TupleAssignmentContext): TupleAssignmentNode { const expression = this.visit(ctx.expression()); - const types = ctx.typeName_list(); - const targets = ctx.Identifier_list().map((name, i) => ({ - name: name.getText(), - type: parseType(types[i].getText()), - })); + // Each target is either `typeName Identifier` (fresh declaration) or `Identifier` (reassignment + // of an existing variable). A reassignment target has no type at parse time; SymbolTableTraversal + // fills it in from the existing variable's symbol. + const targets = ctx.tupleTarget_list().map((target) => { + const typeName = target.typeName(); + return { + name: target.Identifier().getText(), + type: typeName ? parseType(typeName.getText()) : undefined, + isReassignment: typeName === null, + }; + }); const tupleAssignment = new TupleAssignmentNode(targets, expression); tupleAssignment.location = Location.fromCtx(ctx); return tupleAssignment; diff --git a/packages/cashc/src/cashc-cli.ts b/packages/cashc/src/cashc-cli.ts index 997b0a3e4..e263eba93 100644 --- a/packages/cashc/src/cashc-cli.ts +++ b/packages/cashc/src/cashc-cli.ts @@ -27,6 +27,10 @@ program .option('-s, --size', 'Display the size in bytes of the compiled bytecode.') .option('-S, --skip-enforce-function-parameter-types', 'Do not enforce function parameter types.') .option('-L, --skip-enforce-locktime-guard', 'Do not inject a tx.time guard when tx.locktime is used.') + .addOption( + new Option('-O, --optimize-for ', 'Optimisation objective: minimise bytecode size or executed op-cost (default).') + .choices(['size', 'opcost']), + ) .addOption( new Option('-f, --format ', 'Specify the format of the output.') .choices(['json', 'ts']) @@ -53,6 +57,7 @@ function run(): void { const compilerOptions: CompilerOptions = { enforceFunctionParameterTypes: !opts.skipEnforceFunctionParameterTypes, enforceLocktimeGuard: !opts.skipEnforceLocktimeGuard, + ...(opts.optimizeFor ? { optimizeFor: opts.optimizeFor } : {}), }; try { diff --git a/packages/cashc/src/compiler.ts b/packages/cashc/src/compiler.ts index 29e5b5056..9fd6d8b82 100644 --- a/packages/cashc/src/compiler.ts +++ b/packages/cashc/src/compiler.ts @@ -27,6 +27,8 @@ import { ImportResolver, resolveDependencies, } from './dependency-resolution.js'; +import { sinkDefinitions } from './def-sinking.js'; +import { applyStackRescheduling } from './stack-rescheduling.js'; import GenerateTargetTraversal from './generation/GenerateTargetTraversal.js'; import { FoldGlobalConstantsTraversal } from './semantic/FoldGlobalConstantsTraversal.js'; import SymbolTableTraversal from './semantic/SymbolTableTraversal.js'; @@ -36,12 +38,22 @@ import EnsureFunctionsSafeTraversal from './semantic/EnsureFunctionsSafeTraversa import InjectLocktimeGuardTraversal from './semantic/InjectLocktimeGuardTraversal.js'; import DeadCodeEliminationTraversal from './semantic/DeadCodeEliminationTraversal.js'; import { LowerGlobalConstantsTraversal } from './semantic/LowerGlobalConstantsTraversal.js'; +import { hoistRepeatedConstants } from './constant-hoisting.js'; export const DEFAULT_COMPILER_OPTIONS: CompilerOptions = { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + // recorded explicitly so artifacts always state the objective they were compiled under + // (see CompilerOptions in @cashscript/utils for the size/opcost trade-off) + optimizeFor: 'opcost', }; +// Above this unoptimised op-count the legacy-optimiser cross-check is skipped automatically. +// The legacy optimiser is near-linear in practice (measured ~0.15s at 10k elements, ~0.5s at +// 30-40k), so this gate bounds the cost of a redundant second optimisation pass on very large +// generated contracts — it is not guarding against asymptotic blowup. +const OPTIMISATION_CROSS_CHECK_MAX_OPS = 30_000; + export interface CompileOptions extends CompilerOptions { errorListener?: CashScriptErrorListener; } @@ -77,6 +89,24 @@ export const compileFile: (codeFile: PathLike, compilerOptions?: CompileOptions) export interface InternalCompilerOptions extends CompilerOptions { disableInlining?: boolean; + // Skip the backwards-compat cross-check that re-optimises the bytecode with the legacy + // ASM-regex optimiser and compares the results. The check is also skipped automatically for + // very large scripts, where the redundant second optimisation pass measurably slows compiles. + disableOptimisationCrossCheck?: boolean; + // Skip def-sinking. Under `optimizeFor: 'size'`, definitions move down to just before their + // first use when that shrinks the bytecode (the compiler keeps the smaller of the sunk and + // unsunk compiles). This flag is for tools that need the source-ordered compile as input. + disableDefSinking?: boolean; + // Skip constant hoisting. Under `optimizeFor: 'size'`, repeated in-body literals are bound to + // locals when that shrinks the bytecode (the compiler keeps the hoisted compile only when it + // is strictly smaller). This flag is for tools that need the literal-shaped compile as input. + disableConstantHoisting?: boolean; +} + +// The AST rewrites applied before semantic analysis in a single compile candidate. +interface RewriteFlags { + sinkDefs: boolean; + hoistConstants: boolean; } export function compileStringInternal( @@ -103,7 +133,39 @@ function compileCode( resolver: ImportResolver, compilerOptions: CompileOptions & InternalCompilerOptions, ): Artifact { - const { errorListener, disableInlining, ...artifactCompilerOptions } = compilerOptions; + const optimizeFor = compilerOptions.optimizeFor ?? DEFAULT_COMPILER_OPTIONS.optimizeFor; + if (optimizeFor !== 'size') { + return compileImpl(code, resolver, compilerOptions, { sinkDefs: false, hoistConstants: false }); + } + + // Each 'size' rewrite helps most contracts but can cost bytes on some, so the 'size' objective + // compiles every enabled combination and keeps the smallest artifact. Candidates are ordered + // so ties resolve conservatively: the sunk compile wins a def-sinking tie (established + // behaviour), and the hoisted compile is only kept when it is strictly smaller. + const sinkOptions = compilerOptions.disableDefSinking ? [false] : [true, false]; + const hoistOptions = compilerOptions.disableConstantHoisting ? [false] : [false, true]; + const candidates = sinkOptions + .flatMap((sinkDefs) => hoistOptions.map((hoistConstants) => ({ sinkDefs, hoistConstants }))); + const compiledBytes = (artifact: Artifact): number => artifact.debug?.bytecode.length ?? artifact.bytecode.length; + return candidates + .map((rewrites) => compileImpl(code, resolver, compilerOptions, rewrites)) + .reduce((best, candidate) => (compiledBytes(candidate) < compiledBytes(best) ? candidate : best)); +} + +function compileImpl( + code: string, + resolver: ImportResolver, + compilerOptions: CompileOptions & InternalCompilerOptions, + rewrites: RewriteFlags, +): Artifact { + const { + errorListener, disableInlining, disableOptimisationCrossCheck, + // consumed in compileCode (which picks the smallest rewrite combination); destructured here + // only to keep them out of the serialized artifact options + // eslint-disable-next-line @typescript-eslint/no-unused-vars + disableDefSinking, disableConstantHoisting, + ...artifactCompilerOptions + } = compilerOptions; const mergedCompilerOptions = { ...DEFAULT_COMPILER_OPTIONS, ...artifactCompilerOptions }; // Lexing + parsing @@ -111,6 +173,18 @@ function compileCode( checkVersionConstraints(ast.pragmas); ast = resolveDependencies(ast, resolver, errorListener) as Ast; + + // Under the 'size' objective, bind repeated in-body literals to locals (see CompilerOptions). + // Runs before semantic analysis so the introduced locals get symbols like any other variable. + if (rewrites.hoistConstants) { + ast = hoistRepeatedConstants(ast) as Ast; + } + + // Sink variable definitions to just before their first use + if (rewrites.sinkDefs) { + ast = sinkDefinitions(ast) as Ast; + } + if (!ast.contract) throw new MissingContractError(); const constructorParamLength = ast.contract.parameters.length; @@ -137,8 +211,7 @@ function compileCode( ast = ast.accept(traversal) as Ast; // Bytecode optimisation - const optimisedBytecodeOld = optimiseBytecodeOld(traversal.output); - const optimisationResult = optimiseBytecode( + let optimisationResult = optimiseBytecode( traversal.output, sourceMapToLocationData(traversal.sourceMap), traversal.consoleLogs, @@ -148,10 +221,33 @@ function compileCode( constructorParamLength, ); - if (scriptToAsm(optimisedBytecodeOld) !== scriptToAsm(optimisationResult.script)) { - console.error(scriptToAsm(optimisedBytecodeOld)); - console.error(scriptToAsm(optimisationResult.script)); - throw new Error('New bytecode optimisation is not backwards compatible, please report this issue to the CashScript team'); + // Backwards-compat cross-check against the legacy ASM-regex optimiser. The legacy optimiser is + // near-linear in practice (~0.15s at 10k elements, ~0.5s at 30-40k measured), so the size gate + // is about not paying a redundant second optimisation pass on very large generated contracts, + // not about asymptotic blowup; the new optimiser is exercised by the full test suite either + // way. Skippable explicitly via the disableOptimisationCrossCheck compiler option. + if (!disableOptimisationCrossCheck && traversal.output.length <= OPTIMISATION_CROSS_CHECK_MAX_OPS) { + const optimisedBytecodeOld = optimiseBytecodeOld(traversal.output); + if (scriptToAsm(optimisedBytecodeOld) !== scriptToAsm(optimisationResult.script)) { + console.error(scriptToAsm(optimisedBytecodeOld)); + console.error(scriptToAsm(optimisationResult.script)); + throw new Error('New bytecode optimisation is not backwards compatible, please report this issue to the CashScript team'); + } + } + + // Stack rescheduling (opt-in): re-derive straight-line evaluation schedules from the + // dataflow DAG, ranked by the optimizeFor objective. Runs after the legacy-optimiser + // cross-check (which compares pre-reschedule outputs) and is restricted to + // single-function contracts (a function selector makes the entry stack depth + // path-dependent, which the block model does not represent). + let frames = traversal.frames; + if (mergedCompilerOptions.rescheduleStacks && ast.contract!.functions.length === 1) { + ({ result: optimisationResult, frames } = applyStackRescheduling(optimisationResult, frames, { + arities: traversal.definedFunctionArities, + mainInArity: ast.contract!.functions[0].parameters.length + constructorParamLength, + objective: mergedCompilerOptions.optimizeFor ?? 'opcost', + constructorParamLength, + })); } const debug = { @@ -160,7 +256,7 @@ function compileCode( logs: optimisationResult.logs, requires: optimisationResult.requires, sourceTags: generateSourceTags(optimisationResult.sourceTags) || undefined, - functions: traversal.frames.length > 0 ? traversal.frames : undefined, + functions: frames.length > 0 ? frames : undefined, inlineRanges: generateInlineRanges(optimisationResult.inlineRanges) || undefined, }; diff --git a/packages/cashc/src/constant-hoisting.ts b/packages/cashc/src/constant-hoisting.ts new file mode 100644 index 000000000..bd561c9e6 --- /dev/null +++ b/packages/cashc/src/constant-hoisting.ts @@ -0,0 +1,245 @@ +import { binToHex } from '@bitauth/libauth'; +import { encodeInt, scriptToBytecode } from '@cashscript/utils'; +import { + SourceFileNode, + FunctionDefinitionNode, + BlockNode, + LiteralNode, + IntLiteralNode, + HexLiteralNode, + IdentifierNode, + VariableDefinitionNode, + ParameterNode, + TupleAssignmentNode, + BinaryOpNode, + UnaryOpNode, + SliceNode, + FunctionCallNode, + ConsoleStatementNode, + TimeOpNode, + ExpressionNode, + Node, +} from './ast/AST.js'; +import { BinaryOperator, UnaryOperator } from './ast/Operator.js'; +import { GlobalFunction } from './ast/Globals.js'; +import AstTraversal from './ast/AstTraversal.js'; + +// Byte-size optimisation with an op-cost trade-off, so gated behind `optimizeFor: 'size'`: +// a literal that occurs two or more times within one function body is bound to a local at the +// top of the body when that is cheaper by exact byte accounting, and the occurrences become +// identifier references. The second and later uses then cost a stack pick instead of re-pushing +// the literal (~-30 bytes per duplicate of a 32-byte field prime), at the price of a couple of +// extra ops per call for the binding and cleanup. +// +// That trade is right for size-scored contracts and wrong for op-bound ones (where unlocking +// scripts are zero-padded to buy op budget, byte savings are free anyway and the extra ops +// translate directly into more padding) — hence the default 'opcost' objective skips it. +// +// Runs before semantic analysis, so the introduced locals get symbols like any other variable. +// Only raw literals are targeted: uses of named top-level constants are still identifiers at +// this point, and repeated large constants are already deduplicated by the shared-definition +// mechanism (LowerGlobalConstantsTraversal + OP_DEFINE/OP_INVOKE sharing). +// +// Several later passes pattern-match literal nodes in specific syntactic positions, so those +// occurrences must keep their literal shape and are excluded from both counting and replacement +// (see ExcludedLiteralCollector). The pass is additionally guarded in compileCode: the hoisted +// compile is only kept when the final artifact is strictly smaller than the unhoisted one. + +export function hoistRepeatedConstants(ast: SourceFileNode): SourceFileNode { + ast.functions.forEach((func) => hoistInBody(func.body, func.parameters)); + ast.contract?.functions.forEach((func: FunctionDefinitionNode) => hoistInBody(func.body, func.parameters)); + return ast; +} + +// Exact byte accounting: replacing each duplicate push with an identifier access costs about +// 2 bytes (depth + OP_PICK; the declaration itself re-uses the first push) plus ~1 byte of +// end-of-scope cleanup, so hoisting pays when the duplicates' push bytes beat that overhead. +const worthHoisting = (pushBytes: number, count: number): boolean => (count - 1) * pushBytes > 2 * (count - 1) + 1; + +function hoistInBody(body: BlockNode, parameters: ParameterNode[]): void { + if (!body.statements || body.statements.length === 0) return; + + const excluded = collectExcludedLiterals(body); + + const counter = new LiteralCounter(excluded); + // Parameters live outside the body, so seed their names explicitly — a parameter named `hc0` + // must not collide with a generated local. + parameters.forEach((parameter) => counter.usedNames.add(parameter.name)); + counter.visit(body); + + const hoisted = [...counter.literals.values()] + .filter(({ pushBytes, count }) => worthHoisting(pushBytes, count)); + if (hoisted.length === 0) return; + + // Fresh local names that collide with nothing already named in the body. + const names = new Map(); + let n = 0; + for (const { key } of hoisted) { + while (counter.usedNames.has(`hc${n}`)) n += 1; + names.set(key, `hc${n}`); + n += 1; + } + + new LiteralReplacer(names, excluded).visit(body); + + // Declarations go at the very top of the body; source locations borrow from the first + // statement so source-map generation always has a valid location to point at. + const location = body.statements[0].location; + const declarations = hoisted.map(({ key, template }) => { + const literal = cloneLiteral(template); + literal.location = location; + const definition = new VariableDefinitionNode(template.type!, [], names.get(key)!, literal); + definition.location = location; + return definition; + }); + body.statements.unshift(...declarations); +} + +type Hoistable = IntLiteralNode | HexLiteralNode; + +const literalKey = (node: Hoistable): string => ( + node instanceof IntLiteralNode ? `i${node.value}` : `x${binToHex(node.value)}` +); + +const literalPushBytes = (node: Hoistable): number => ( + scriptToBytecode([node instanceof IntLiteralNode ? encodeInt(node.value) : node.value]).length +); + +function cloneLiteral(node: Hoistable): LiteralNode { + return node instanceof IntLiteralNode ? new IntLiteralNode(node.value) : new HexLiteralNode(node.value); +} + +function collectExcludedLiterals(body: BlockNode): Set { + const collector = new ExcludedLiteralCollector(); + collector.visit(body); + return collector.excluded; +} + +// Split bounds and bit-shift counts: TypeCheckTraversal keys bounded-type inference and the +// static IndexOutOfBoundsError / BitshiftBitcountNegativeError checks on a literal right operand. +const BOUND_SENSITIVE_OPERATORS = [BinaryOperator.SPLIT, BinaryOperator.SHIFT_LEFT, BinaryOperator.SHIFT_RIGHT]; + +const isSizeOp = (node: ExpressionNode): boolean => ( + node instanceof UnaryOpNode && node.operator === UnaryOperator.SIZE +); + +// Marks the literal occurrences that later passes pattern-match syntactically, and which must +// therefore stay literals (they take part in neither counting nor replacement): +// - split / bit-shift right operands and slice bounds — bounded-type inference and static +// bounds checks in TypeCheckTraversal require literal nodes there; +// - toPaddedBytes size arguments — inferPaddedBytesType reads a literal second parameter; +// - literals compared against `.length` — TypeCheckTraversal narrows unbounded bytes from them; +// - console statement parameters — logs are stripped from the real bytecode, so hoisting them +// would materialise debug-only data as live stack values; +// - tx.time / this.age operands — InjectLocktimeGuardTraversal's isLocktimeCheck heuristic +// keys on a literal operand, and replacing it triggers a synthetic guard. +class ExcludedLiteralCollector extends AstTraversal { + excluded = new Set(); + + private exclude(node: Node | undefined): void { + if (node instanceof IntLiteralNode || node instanceof HexLiteralNode) this.excluded.add(node); + } + + visitBinaryOp(node: BinaryOpNode): Node { + if (BOUND_SENSITIVE_OPERATORS.includes(node.operator)) this.exclude(node.right); + if (isSizeOp(node.left)) this.exclude(node.right); + if (isSizeOp(node.right)) this.exclude(node.left); + return super.visitBinaryOp(node); + } + + visitSlice(node: SliceNode): Node { + this.exclude(node.start); + this.exclude(node.end); + return super.visitSlice(node); + } + + visitFunctionCall(node: FunctionCallNode): Node { + if (node.identifier.name === GlobalFunction.TO_PADDED_BYTES) this.exclude(node.parameters[1]); + return super.visitFunctionCall(node); + } + + visitConsoleStatement(node: ConsoleStatementNode): Node { + node.parameters.forEach((parameter) => this.exclude(parameter)); + return node; + } + + visitTimeOp(node: TimeOpNode): Node { + this.exclude(node.expression); + return super.visitTimeOp(node); + } +} + +// Counts hoistable literals by value and records every name already used in the body +// (identifiers, parameters, definitions, tuple targets), so the introduced locals cannot +// shadow or be shadowed by anything. +class LiteralCounter extends AstTraversal { + literals = new Map(); + usedNames = new Set(); + + constructor(private excluded: Set) { + super(); + } + + private countLiteral(node: Hoistable): void { + if (this.excluded.has(node)) return; + const key = literalKey(node); + const entry = this.literals.get(key); + if (entry) entry.count += 1; + else this.literals.set(key, { key, template: node, pushBytes: literalPushBytes(node), count: 1 }); + } + + visitIntLiteral(node: IntLiteralNode): Node { + this.countLiteral(node); + return node; + } + + visitHexLiteral(node: HexLiteralNode): Node { + this.countLiteral(node); + return node; + } + + visitIdentifier(node: IdentifierNode): Node { + this.usedNames.add(node.name); + return node; + } + + visitParameter(node: ParameterNode): Node { + this.usedNames.add(node.name); + return node; + } + + visitVariableDefinition(node: VariableDefinitionNode): Node { + this.usedNames.add(node.name); + return super.visitVariableDefinition(node); + } + + visitTupleAssignment(node: TupleAssignmentNode): Node { + node.targets.forEach((target) => this.usedNames.add(target.name)); + return super.visitTupleAssignment(node); + } +} + +// Swaps each hoisted literal occurrence for a reference to its local. The identifier takes +// the literal's source location (same convention as constant inlining, in reverse). +class LiteralReplacer extends AstTraversal { + constructor(private names: Map, private excluded: Set) { + super(); + } + + private replace(node: Hoistable): Node { + if (this.excluded.has(node)) return node; + const name = this.names.get(literalKey(node)); + if (name === undefined) return node; + const identifier = new IdentifierNode(name); + identifier.location = node.location; + return identifier; + } + + visitIntLiteral(node: IntLiteralNode): Node { + return this.replace(node); + } + + visitHexLiteral(node: HexLiteralNode): Node { + return this.replace(node); + } +} diff --git a/packages/cashc/src/def-sinking.ts b/packages/cashc/src/def-sinking.ts new file mode 100644 index 000000000..97b74f9f2 --- /dev/null +++ b/packages/cashc/src/def-sinking.ts @@ -0,0 +1,215 @@ +import { + SourceFileNode, + FunctionDefinitionNode, + BlockNode, + StatementNode, + Node, + VariableDefinitionNode, + TupleAssignmentNode, + AssignNode, + ReturnNode, + ConsoleStatementNode, + ControlStatementNode, + BranchNode, + DoWhileNode, + WhileNode, + ForNode, + ExpressionNode, + IdentifierNode, + FunctionCallNode, + InstantiationNode, +} from './ast/AST.js'; +import { Modifier } from './ast/Globals.js'; +import AstTraversal from './ast/AstTraversal.js'; + +// Moves each variable definition down its block to just before the statement that first uses +// it. A definition compiles to a rename of the value its initializer leaves on top of the +// stack, so sinking shrinks live ranges: intervening reads get shallower depth arguments and a +// single-use definition's access pair collapses in the peephole. The reorder can change which +// require() fails first on an invalid witness and the order of console.log output — never +// whether a transaction is accepted. +// +// Byte-size optimisation, gated behind `optimizeFor: 'size'`: relocating evaluation deepens the +// initializer's own input reads, and under ROLL/PICK depth metering the executed-op-cost +// balance of that trade is workload-dependent. Runs before semantic analysis so the final-use +// bookkeeping (opRolls) sees the reordered statements. + +export function sinkDefinitions(ast: SourceFileNode): SourceFileNode { + ast.functions.forEach(sinkInFunction); + ast.contract?.functions.forEach((func: FunctionDefinitionNode) => sinkInFunction(func)); + return ast; +} + +function sinkInFunction(func: FunctionDefinitionNode): void { + const reassigned = collectReassignedNames(func.body); + sinkInBlock(func.body, reassigned); +} + +// A definition (scalar, or tuple-destructure declaring only new variables) normalised to the +// names it introduces and the free variables its initializer reads. +interface SinkableDefinition { + names: Set; + freeVariables: Set; +} + +function asSinkableDefinition( + statement: StatementNode, + reassigned: Set, +): SinkableDefinition | undefined { + if (statement instanceof VariableDefinitionNode) { + // `unused` definitions have no use site; reassigned variables keep their definition point. + if (statement.modifiers.includes(Modifier.UNUSED)) return undefined; + if (reassigned.has(statement.name)) return undefined; + return { names: new Set([statement.name]), freeVariables: collectReferences(statement.expression).references }; + } + + if (statement instanceof TupleAssignmentNode) { + if (statement.targets.some((target) => target.isReassignment)) return undefined; + if (statement.targets.some((target) => reassigned.has(target.name))) return undefined; + return { + names: new Set(statement.targets.map((target) => target.name)), + freeVariables: collectReferences(statement.tuple).references, + }; + } + + return undefined; +} + +function sinkInBlock(block: BlockNode, reassigned: Set): void { + const statements = block.statements; + if (!statements) return; + + // Each nested block sinks independently; a definition never crosses a scope boundary. + statements.forEach((statement) => sinkInNestedBlocks(statement, reassigned)); + if (statements.length < 2) return; + + const facts = statements.map(computeStatementFacts); + + // Nothing may move past the block's last non-console statement, which semantic analysis + // requires to stay a require (contract functions) or return (returning global functions). + let lastEffective = statements.length - 1; + while (lastEffective >= 0 && statements[lastEffective] instanceof ConsoleStatementNode) { + lastEffective -= 1; + } + + // Bottom-up, so a chain `int a = ...; int b = f(a);` sinks as a whole in one pass. + for (let i = statements.length - 2; i >= 0; i -= 1) { + const definition = asSinkableDefinition(statements[i], reassigned); + if (definition === undefined) continue; + + let target = i; + while (target + 1 <= lastEffective) { + const next = facts[target + 1]; + if (intersects(next.references, definition.names)) break; // first use — land just above it + if (next.barrier) break; + if (intersects(next.assigns, definition.freeVariables)) break; // initializer input changes + if (target + 1 === lastEffective) break; + target += 1; + } + + if (target > i) { + const [statement] = statements.splice(i, 1); + statements.splice(target, 0, statement); + const [fact] = facts.splice(i, 1); + facts.splice(target, 0, fact); + } + } +} + +// Branch and loop children are typed as BlockNode but braceless bodies (`if (x) require(y);`, +// `else if` chains) are bare statements, so dispatch on the actual node. +function sinkInNestedBlocks(statement: StatementNode, reassigned: Set): void { + if (statement instanceof BranchNode) { + sinkInChild(statement.ifBlock, reassigned); + if (statement.elseBlock) sinkInChild(statement.elseBlock, reassigned); + } else if (statement instanceof DoWhileNode || statement instanceof WhileNode || statement instanceof ForNode) { + sinkInChild(statement.block, reassigned); + } +} + +function sinkInChild(child: Node, reassigned: Set): void { + if (child instanceof BlockNode) sinkInBlock(child, reassigned); + else sinkInNestedBlocks(child as StatementNode, reassigned); +} + +interface StatementFacts { + references: Set; + assigns: Set; + barrier: boolean; +} + +function computeStatementFacts(statement: StatementNode): StatementFacts { + const { references, assigns, defines } = collectReferences(statement); + const barrier = statement instanceof ControlStatementNode + || statement instanceof ReturnNode + || (statement instanceof TupleAssignmentNode && statement.targets.some((target) => target.isReassignment)); + return { references, assigns: new Set([...assigns, ...defines]), barrier }; +} + +function collectReferences(node: ExpressionNode | StatementNode): ReferenceCollector { + const collector = new ReferenceCollector(); + collector.visit(node); + return collector; +} + +const intersects = (a: Set, b: Set): boolean => { + for (const element of a) if (b.has(element)) return true; + return false; +}; + +// Records every variable referenced and every variable assigned anywhere in a statement. +// Console parameters count as references (codegen needs logged variables on the stack); +// callee names of calls and instantiations do not — they are not stack variables. +class ReferenceCollector extends AstTraversal { + references = new Set(); + assigns = new Set(); + defines = new Set(); + + visitIdentifier(node: IdentifierNode): Node { + this.references.add(node.name); + return node; + } + + visitFunctionCall(node: FunctionCallNode): Node { + node.parameters = this.visitList(node.parameters); + return node; + } + + visitInstantiation(node: InstantiationNode): Node { + node.parameters = this.visitList(node.parameters); + return node; + } + + visitAssign(node: AssignNode): Node { + this.assigns.add(node.identifier.name); + return super.visitAssign(node); + } + + // Definition-introduced names are tracked separately from reassignments: the sink barrier must + // treat them like assigns (a sunk definition may never move past a later definition of one of + // its inputs — otherwise sinking would launder a use-before-definition program, rejected by the + // source-ordered compile, into one that compiles), while collectReassignedNames must not (a + // definition alone does not pin a variable in place). + visitVariableDefinition(node: VariableDefinitionNode): Node { + this.defines.add(node.name); + return super.visitVariableDefinition(node); + } + + visitTupleAssignment(node: TupleAssignmentNode): Node { + node.targets.forEach((target) => { + if (target.isReassignment) { + this.assigns.add(target.name); + this.references.add(target.name); + } else { + this.defines.add(target.name); + } + }); + return super.visitTupleAssignment(node); + } +} + +function collectReassignedNames(body: BlockNode): Set { + const collector = new ReferenceCollector(); + collector.visit(body); + return collector.assigns; +} diff --git a/packages/cashc/src/generation/GenerateTargetTraversal.ts b/packages/cashc/src/generation/GenerateTargetTraversal.ts index 076c9650a..11403b58f 100644 --- a/packages/cashc/src/generation/GenerateTargetTraversal.ts +++ b/packages/cashc/src/generation/GenerateTargetTraversal.ts @@ -73,7 +73,9 @@ import { compileUnaryOp, } from './utils.js'; import { isNumericType } from '../utils.js'; -import { collectFunctionCalls, isRecursive, shouldInline } from './inlining.js'; +import { + collectEntryReachableCalls, collectFunctionCalls, collectLoopResidentFunctions, isRecursive, shouldInline, +} from './inlining.js'; import type { InternalCompilerOptions } from '../compiler.js'; interface InlineRange { @@ -99,6 +101,18 @@ export default class GenerateTargetTraversal extends AstTraversal { private constructorParameterCount: number; inlineRanges: InlineRange[] = []; + // Stack arity (parameter/return counts) of every OP_DEFINE'd function, keyed by its + // functionId — consumed by post-codegen passes (stack rescheduling) that need OP_INVOKE + // stack effects without re-deriving them from the bytecode. + definedFunctionArities: Map = new Map(); + + // Depth of nested user-function-call argument staging (see stageUserFunctionArguments). + private userCallArgDepth = 0; + // Per-variable occurrence counts across the outermost call's whole argument tree. Names appearing + // 2+ times stay OP_PICK (a ROLL under reversed emission would consume them too early). Populated + // when userCallArgDepth goes 0 -> 1 and cleared when it returns to 0. + private callArgNameCounts: Map | null = null; + constructor(private compilerOptions: InternalCompilerOptions) { super(); } @@ -204,6 +218,8 @@ export default class GenerateTargetTraversal extends AstTraversal { }); const reachableCalls = [node.contract!, ...node.functions].flatMap((n) => collectFunctionCalls(n)); + const entryReachableCalls = collectEntryReachableCalls(node); + const loopResidentFunctions = collectLoopResidentFunctions(node); const definedFunctions: Array<{ func: FunctionDefinitionNode, compiledResult: OptimiseBytecodeResult }> = []; let nextFunctionId = recursiveFunctions.length; @@ -212,7 +228,11 @@ export default class GenerateTargetTraversal extends AstTraversal { const compiledResult = this.compileGlobalFunctionBody(func); // If the function should be inlined, we ONLY update the symbol - if (shouldInline(symbol, compiledResult, reachableCalls, nextFunctionId, this.compilerOptions)) { + const inline = shouldInline( + symbol, compiledResult, reachableCalls, entryReachableCalls, loopResidentFunctions, nextFunctionId, + this.compilerOptions, + ); + if (inline) { symbol.setInlinedBytecode(compiledResult.script, this.buildDebugFrame(func, compiledResult)); return; } @@ -230,6 +250,7 @@ export default class GenerateTargetTraversal extends AstTraversal { // Emit definitions in ID order so debug.functions[n] corresponds to the n-th define site (id n). definedFunctions.forEach(({ func, compiledResult }, functionId) => { this.frames.push(this.buildDebugFrame(func, compiledResult, functionId)); + this.definedFunctionArities.set(functionId, { in: func.parameters.length, out: func.returnTypes?.length ?? 0 }); const locationData = { location: func.location, positionHint: PositionHint.START }; this.emit(scriptToBytecode(compiledResult.script), locationData); // this.emit(encodeInt(BigInt(functionId)), locationData); // @@ -247,11 +268,10 @@ export default class GenerateTargetTraversal extends AstTraversal { bodyTraversal.currentFunction = node; bodyTraversal.constructorParameterCount = 0; - // Seed the stack with parameters in reverse order so the last parameter is on top - // (similar to how builtin functions work) - for (let i = node.parameters.length - 1; i >= 0; i -= 1) { - bodyTraversal.visit(node.parameters[i]); - } + // Seed the stack with the parameters (visitParameter pushes them to the stack bottom in order, + // so the first parameter ends up on top) — matching the right-to-left argument staging at call + // sites, where declaration-order variable arguments then need no reordering at all. + node.parameters.forEach((parameter) => bodyTraversal.visit(parameter)); bodyTraversal.dropUnusedParameters(node.parameters); bodyTraversal.visit(node.body); @@ -507,9 +527,39 @@ export default class GenerateTargetTraversal extends AstTraversal { } visitTupleAssignment(node: TupleAssignmentNode): Node { + // The RHS leaves N values on the stack, last on top (as `.split` and multi-return calls do). node.tuple = this.visit(node.tuple); - this.popFromStack(node.targets.length); - node.targets.forEach((target) => this.pushToStack(target.name)); + const n = node.targets.length; + + // Outside a loop/branch, binding is a pure rename: the result entries take the target names, and + // a reassigned variable's old slot becomes a dead duplicate cleaned up at end of scope. + const scopedReassign = this.scopeDepth > 0 && node.targets.some((target) => target.isReassignment); + if (!scopedReassign) { + this.popFromStack(n); + node.targets.forEach((target) => this.pushToStack(target.name)); + return node; + } + + // In a loop/branch the stack layout must be preserved, so reassignments are folded in place. This + // requires declarations to form a contiguous block below all reassignments (the natural layout of + // a multi-return: fresh values, then updated accumulators). SymbolTableTraversal enforces this + // ordering for every tuple destructuring (TupleTargetOrderError), so the check here is an + // internal invariant only. + const firstReassign = node.targets.findIndex((target) => target.isReassignment); + const lastDeclaration = node.targets.reduce((acc, target, i) => (target.isReassignment ? acc : i), -1); + if (lastDeclaration > firstReassign) { + throw new Error('Declaration targets must come before all reassignment targets in a scoped tuple destructuring'); + } + + // Fold the trailing reassignment block into the existing slots top-down (each target's value is on + // top when processed), then rename the leading declaration values in place (no opcodes). + for (let i = n - 1; i >= firstReassign; i -= 1) { + this.emitReplace(this.getStackIndex(node.targets[i].name), node); + this.popFromStack(); + } + for (let s = 0; s < firstReassign; s += 1) { + this.stack[s] = node.targets[firstReassign - 1 - s].name; + } return node; } @@ -775,7 +825,14 @@ export default class GenerateTargetTraversal extends AstTraversal { } const symbol = node.identifier.symbol!; - node.parameters = this.visitList(node.parameters); + + // User-defined function arguments are staged right-to-left (first argument on top, matching the + // body's parameter layout); built-in functions keep natural left-to-right evaluation. + if (symbol.definition instanceof FunctionDefinitionNode) { + this.stageUserFunctionArguments(node); + } else { + node.parameters = this.visitList(node.parameters); + } const startIp = this.output.length + this.constructorParameterCount; const endIp = startIp + symbol.bytecode!.length - 1; @@ -797,6 +854,30 @@ export default class GenerateTargetTraversal extends AstTraversal { return node; } + // Stages user-function call arguments right-to-left, so the first argument ends up on top of the + // stack — the layout the function body is compiled against. With arguments that are variables in + // declaration order (the common case), the emitted ROLLs then cancel to nothing in the optimiser, + // so a typical call costs zero staging instructions. Reversed emission breaks the textual + // final-use order, so at the outermost call each variable's occurrences are counted across the + // whole (possibly nested) argument tree: only names used exactly once may ROLL (see isOpRoll); + // everything else stays a PICK. + private stageUserFunctionArguments(node: FunctionCallNode): void { + const isOutermostCall = this.userCallArgDepth === 0; + if (isOutermostCall) { + const counter = new ArgIdentifierCounter(); + node.parameters.forEach((argument) => counter.visit(argument)); + this.callArgNameCounts = counter.counts; + } + + this.userCallArgDepth += 1; + for (let i = node.parameters.length - 1; i >= 0; i -= 1) { + node.parameters[i] = this.visit(node.parameters[i]); + } + this.userCallArgDepth -= 1; + + if (isOutermostCall) this.callArgNameCounts = null; + } + visitMultiSig(node: FunctionCallNode): Node { this.emit(encodeBool(false), { location: node.location, positionHint: PositionHint.START }); this.pushToStack('(value)'); @@ -990,7 +1071,12 @@ export default class GenerateTargetTraversal extends AstTraversal { } isOpRoll(node: IdentifierNode): boolean { - return this.currentFunction.opRolls.get(node.name) === node && this.scopeDepth === 0; + // Must be the variable's final use (opRolls site) and not inside an if/loop scope. + if (this.currentFunction.opRolls.get(node.name) !== node || this.scopeDepth !== 0) return false; + // Inside a user-function-call argument list, ROLL is only safe when this variable appears exactly + // once across the whole argument tree; otherwise reversed emission would consume it too early. + if (this.userCallArgDepth > 0) return this.callArgNameCounts?.get(node.name) === 1; + return true; } visitBoolLiteral(node: BoolLiteralNode): Node { @@ -1018,3 +1104,26 @@ export default class GenerateTargetTraversal extends AstTraversal { } } +// Counts identifier reads across a user-function call's whole argument tree, used to decide whether +// a final-use variable in the argument list is safe to OP_ROLL: a name referenced exactly once has +// no other use that the reversed-emission ROLL could consume early. Function/instantiation callee +// identifiers are not stack-variable reads, so they are skipped (only the call arguments are counted). +class ArgIdentifierCounter extends AstTraversal { + counts: Map = new Map(); + + visitIdentifier(node: IdentifierNode): Node { + this.counts.set(node.name, (this.counts.get(node.name) ?? 0) + 1); + return node; + } + + visitFunctionCall(node: FunctionCallNode): Node { + node.parameters = this.visitList(node.parameters); + return node; + } + + visitInstantiation(node: InstantiationNode): Node { + node.parameters = this.visitList(node.parameters); + return node; + } +} + diff --git a/packages/cashc/src/generation/inlining.ts b/packages/cashc/src/generation/inlining.ts index 3ecad1f50..f90d79c84 100644 --- a/packages/cashc/src/generation/inlining.ts +++ b/packages/cashc/src/generation/inlining.ts @@ -1,10 +1,22 @@ import { encodeInt, + OptimizationTarget, OptimiseBytecodeResult, Script, scriptToBytecode, } from '@cashscript/utils'; -import { FunctionCallNode, FunctionDefinitionNode, Node } from '../ast/AST.js'; +import { + AssignNode, + BlockNode, + DoWhileNode, + ForNode, + FunctionCallNode, + FunctionDefinitionNode, + Node, + SourceFileNode, + VariableDefinitionNode, + WhileNode, +} from '../ast/AST.js'; import AstTraversal from '../ast/AstTraversal.js'; import { Symbol } from '../ast/SymbolTable.js'; import type { InternalCompilerOptions } from '../compiler.js'; @@ -13,26 +25,94 @@ export const shouldInline = ( symbol: Symbol, optimisedResult: OptimiseBytecodeResult, reachableCalls: FunctionCallNode[], + entryReachableCalls: FunctionCallNode[][], + loopResidentFunctions: Set, nextFunctionId: number, compilerOptions: InternalCompilerOptions, ): boolean => { if (compilerOptions.disableInlining) return false; if (symbol.functionId !== undefined) return false; + // Loop-resident functions stay OP_DEFINE'd to avoid stepping cost. Tiny bodies (<= 2 script + // elements) are exempt: inlined they step no more opcodes than the 2-op invoke site even when + // skipped and execute fewer when taken, so the op-cost axis cannot lose. Bytes can still lose + // (a 2-element body may carry a large push), so whether inlining actually happens is still + // decided by the byte model below. + const definition = symbol.definition; + if ( + definition instanceof FunctionDefinitionNode + && loopResidentFunctions.has(definition) + && optimisedResult.script.length > 2 + ) { + return false; + } + const callCount = reachableCalls.filter((call) => call.identifier.symbol === symbol).length; - return isWorthInlining(nextFunctionId, optimisedResult.script, callCount); + const entryCallCounts = entryReachableCalls + .map((calls) => calls.filter((call) => call.identifier.symbol === symbol).length); + return isWorthInlining(nextFunctionId, optimisedResult.script, callCount, entryCallCounts, compilerOptions.optimizeFor); }; -function isWorthInlining(candidateFunctionId: number, bodyScript: Script, callCount: number): boolean { +// Op-cost accounting (CHIP-2021-05 VM limits): every evaluated instruction costs a base 100 and +// stack pushes add 1 per pushed byte, so sharing a body of B bytes (1-byte funcid) pays +// OP_DEFINE = 301 + 2B once per spend (OP_DEFINE re-prices the body's +// stack-pushed bytes) plus OP_INVOKE = 201 per call, while an inlined body executes at +// identical cost to an invoked one. For EXECUTED call sites inlining therefore always wins +// op-cost, so under the 'opcost' objective tiny bodies inline regardless of use count. The cap +// bounds the collateral bytes: each inlined call site costs B bytes instead of the ~2-byte invoke +// site, so 6 tolerates ~4 extra bytes per additional call site (~50 op-cost bought per byte). +// Loop-resident bodies are excluded before this rule (stepping a skipped inlined body costs +// 100/opcode per ITERATION), and the density guard below excludes bodies whose call sites are +// spread across contract entry functions. +const OPCOST_INLINE_MAX_BODY_BYTES = 6; + +function isWorthInlining( + candidateFunctionId: number, + bodyScript: Script, + callCount: number, + entryCallCounts: number[], + optimizeFor?: OptimizationTarget, +): boolean { const bodyBytes = scriptToBytecode(bodyScript).length; + if ( + optimizeFor !== 'size' + && bodyBytes <= OPCOST_INLINE_MAX_BODY_BYTES + && passesDensityGuard(entryCallCounts, bodyScript.length, bodyBytes) + ) { + return true; + } + const idBytes = scriptToBytecode([encodeInt(BigInt(candidateFunctionId))]).length; - const bytesWhenDefined = bodyBytes + idBytes + 1 + callCount * (idBytes + 1); + // The define site pushes the body as data, so it carries a push-length prefix; serializing the + // wrapped body computes prefix + body exactly at any size. + const bodyPushBytes = scriptToBytecode([scriptToBytecode(bodyScript)]).length; + const bytesWhenDefined = bodyPushBytes + idBytes + 1 + callCount * (idBytes + 1); const bytesWhenInlined = callCount * bodyBytes; return bytesWhenInlined <= bytesWhenDefined; } +// A multi-function contract compiles into one script with a selector branch per entry function, +// and the VM steps (and charges 100/opcode for) the branches a spend does NOT take. An inlined +// copy in an untaken branch therefore costs 100 x elements per spend where an invoke site costs +// 200 — so a tiny body is only forced inline when no spend path can lose op-cost: for every entry +// function, the extra stepping of the OTHER entries' copies must not exceed what inlining saves +// (the one-time define and the entry's own invoke overhead). Single-entry contracts pass +// trivially (no other branches); bodies of <= 2 elements pass at any spread (a copy steps no more +// than the invoke site it replaces). No spend-frequency assumptions: the bound must hold for +// every entry. Call sites inside still-defined helpers are attributed to every entry reaching the +// helper, an over-approximation that only makes the guard more conservative (keeps OP_DEFINE and +// forgoes at most the 201/call invoke saving — never a stepping regression). +function passesDensityGuard(entryCallCounts: number[], bodyElements: number, bodyBytes: number): boolean { + if (bodyElements <= 2) return true; + const totalCalls = entryCallCounts.reduce((total, count) => total + count, 0); + const defineCost = 301 + 2 * bodyBytes; + return entryCallCounts.every((count) => ( + 100 * (bodyElements - 2) * (totalCalls - count) <= defineCost + 201 * count + )); +} + class FunctionCallCollector extends AstTraversal { functionCalls: FunctionCallNode[] = []; @@ -49,6 +129,30 @@ export function collectFunctionCalls(node: Node): FunctionCallNode[] { return collector.functionCalls; } +// Call sites reachable from each contract entry function: the entry's own body plus the bodies +// of every global function it (transitively) calls. Feeds the inlining density guard, which +// needs to know how a callee's call sites distribute over the selector branches. +export function collectEntryReachableCalls(node: SourceFileNode): FunctionCallNode[][] { + return (node.contract?.functions ?? []).map((entry) => { + const calls = [...collectFunctionCalls(entry)]; + const seen = new Set(); + const queue = callDefinitions(calls); + while (queue.length > 0) { + const definition = queue.shift()!; + if (seen.has(definition)) continue; + seen.add(definition); + const inner = collectFunctionCalls(definition.body); + calls.push(...inner); + queue.push(...callDefinitions(inner)); + } + return calls; + }); +} + +const callDefinitions = (calls: FunctionCallNode[]): FunctionDefinitionNode[] => calls + .map((call) => call.identifier.symbol?.definition) + .filter((definition): definition is FunctionDefinitionNode => definition instanceof FunctionDefinitionNode); + export function isRecursive(func: FunctionDefinitionNode): boolean { return transitiveCalledFunctions(func).includes(func); } @@ -72,3 +176,73 @@ function calledFunctions(func: FunctionDefinitionNode): FunctionDefinitionNode[] .filter((definition): definition is FunctionDefinitionNode => definition instanceof FunctionDefinitionNode) .filter((definition, index, definitions) => definitions.indexOf(definition) === index); } + +// Functions that must stay OP_DEFINE'd because a call site sits inside a loop — directly, or via +// the callee chain of such a function. Splicing a body into a loop makes every iteration step over +// it, and the VM charges per-opcode cost even for opcodes in an untaken branch, so a small byte +// saving multiplies into a large op-cost regression (measured ~2.8x on sparse-input double-and-add +// loops, where the group-law body sits in a rarely-taken `if`). With OP_DEFINE the skipped call +// site costs 2 stepped opcodes instead of the whole body. +// +// The callee closure protects callees on CONDITIONAL paths inside a loop-resident caller: the +// caller's defined body is stepped end-to-end on every invoke, so a callee inlined into one of its +// untaken branches would be stepped per invocation too. A callee on the caller's always-path is +// stepped ≈ executed either way, so excluding it over-approximates — but the residual cost is only +// ~2 stepped ops per invocation (paid on every call) plus ~5 define bytes per function, so the +// closure is kept coarse rather than branch-aware. Same +// deliberate imprecision for always-executed call sites directly in loops: inlining there would +// actually save the 2 invoke ops per iteration, but the asymmetry (2 ops/iteration sacrificed vs +// ~100×body-size/iteration protected) makes conservative the right default. +export function collectLoopResidentFunctions(node: SourceFileNode): Set { + const loopResident = new Set(); + let loopDepth = 0; + + const collector = new class extends AstTraversal { + visitWhile(n: WhileNode): Node { + loopDepth += 1; + const result = super.visitWhile(n); + loopDepth -= 1; + return result; + } + + visitDoWhile(n: DoWhileNode): Node { + loopDepth += 1; + const result = super.visitDoWhile(n); + loopDepth -= 1; + return result; + } + + visitFor(n: ForNode): Node { + // The init statement runs once, before OP_BEGIN, so a call there is not loop-resident; + // only the condition, update, and body are re-stepped every iteration. + n.init = this.visit(n.init) as VariableDefinitionNode | AssignNode; + loopDepth += 1; + n.condition = this.visit(n.condition); + n.update = this.visit(n.update) as AssignNode; + n.block = this.visit(n.block) as BlockNode; + loopDepth -= 1; + return n; + } + + visitFunctionCall(n: FunctionCallNode): Node { + const definition = n.identifier.symbol?.definition; + if (loopDepth > 0 && definition instanceof FunctionDefinitionNode) loopResident.add(definition); + return super.visitFunctionCall(n); + } + }(); + + node.functions.forEach((func) => collector.visit(func.body)); + if (node.contract) collector.visit(node.contract); + + // Close over the callee chain of every loop-resident function. + const queue = [...loopResident]; + while (queue.length > 0) { + calledFunctions(queue.shift()!).forEach((callee) => { + if (loopResident.has(callee)) return; + loopResident.add(callee); + queue.push(callee); + }); + } + + return loopResident; +} diff --git a/packages/cashc/src/grammar/CashScript.g4 b/packages/cashc/src/grammar/CashScript.g4 index 7800db392..f43ef0e02 100644 --- a/packages/cashc/src/grammar/CashScript.g4 +++ b/packages/cashc/src/grammar/CashScript.g4 @@ -101,7 +101,16 @@ variableDefinition ; tupleAssignment - : typeName Identifier (',' typeName Identifier)+ '=' expression + : tupleTarget (',' tupleTarget)+ '=' expression + | '(' tupleTarget (',' tupleTarget)+ ')' '=' expression + ; + +// A destructuring target is either a fresh declaration (`int x`) or an existing variable to +// reassign (`x`, no type). Reassigning existing variables avoids the fresh-temp + per-element +// rebind workaround; the code generator lowers a top-level reassignment to a stack rename. +tupleTarget + : typeName Identifier + | Identifier ; assignStatement diff --git a/packages/cashc/src/grammar/CashScript.interp b/packages/cashc/src/grammar/CashScript.interp index 63f11d9df..04be55783 100644 --- a/packages/cashc/src/grammar/CashScript.interp +++ b/packages/cashc/src/grammar/CashScript.interp @@ -198,6 +198,7 @@ returnStatement controlStatement variableDefinition tupleAssignment +tupleTarget assignStatement timeOpStatement requireStatement @@ -222,4 +223,4 @@ typeCast atn: -[4, 1, 85, 515, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 1, 0, 5, 0, 90, 8, 0, 10, 0, 12, 0, 93, 9, 0, 1, 0, 5, 0, 96, 8, 0, 10, 0, 12, 0, 99, 9, 0, 1, 0, 5, 0, 102, 8, 0, 10, 0, 12, 0, 105, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 3, 3, 118, 8, 3, 1, 4, 3, 4, 121, 8, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 3, 7, 134, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 5, 8, 144, 8, 8, 10, 8, 12, 8, 147, 9, 8, 1, 8, 1, 8, 3, 8, 151, 8, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 5, 10, 167, 8, 10, 10, 10, 12, 10, 170, 9, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 5, 12, 181, 8, 12, 10, 12, 12, 12, 184, 9, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 5, 13, 192, 8, 13, 10, 13, 12, 13, 195, 9, 13, 1, 13, 3, 13, 198, 8, 13, 3, 13, 200, 8, 13, 1, 13, 1, 13, 1, 14, 1, 14, 5, 14, 206, 8, 14, 10, 14, 12, 14, 209, 9, 14, 1, 14, 1, 14, 1, 15, 1, 15, 5, 15, 215, 8, 15, 10, 15, 12, 15, 218, 9, 15, 1, 15, 1, 15, 3, 15, 222, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 3, 16, 228, 8, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 238, 8, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 5, 19, 246, 8, 19, 10, 19, 12, 19, 249, 9, 19, 1, 20, 1, 20, 3, 20, 253, 8, 20, 1, 21, 1, 21, 5, 21, 257, 8, 21, 10, 21, 12, 21, 260, 9, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 4, 22, 272, 8, 22, 11, 22, 12, 22, 273, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 3, 23, 284, 8, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 3, 24, 293, 8, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 3, 25, 302, 8, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 316, 8, 27, 1, 28, 1, 28, 1, 28, 3, 28, 321, 8, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 3, 32, 349, 8, 32, 1, 33, 1, 33, 1, 34, 1, 34, 3, 34, 355, 8, 34, 1, 35, 1, 35, 1, 35, 1, 35, 5, 35, 361, 8, 35, 10, 35, 12, 35, 364, 9, 35, 1, 35, 3, 35, 367, 8, 35, 3, 35, 369, 8, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 5, 37, 380, 8, 37, 10, 37, 12, 37, 383, 9, 37, 1, 37, 3, 37, 386, 8, 37, 3, 37, 388, 8, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 401, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 427, 8, 38, 10, 38, 12, 38, 430, 9, 38, 1, 38, 3, 38, 433, 8, 38, 3, 38, 435, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 441, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 493, 8, 38, 10, 38, 12, 38, 496, 9, 38, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 3, 40, 505, 8, 40, 1, 41, 1, 41, 3, 41, 509, 8, 41, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 0, 1, 76, 44, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 0, 15, 1, 0, 4, 10, 2, 0, 10, 10, 22, 23, 1, 0, 24, 25, 1, 0, 37, 41, 2, 0, 37, 41, 43, 46, 2, 0, 5, 5, 51, 52, 1, 0, 53, 55, 2, 0, 52, 52, 56, 56, 1, 0, 57, 58, 1, 0, 6, 9, 1, 0, 59, 60, 1, 0, 47, 48, 2, 0, 17, 17, 65, 65, 1, 0, 72, 74, 2, 0, 72, 73, 80, 80, 546, 0, 91, 1, 0, 0, 0, 2, 108, 1, 0, 0, 0, 4, 113, 1, 0, 0, 0, 6, 115, 1, 0, 0, 0, 8, 120, 1, 0, 0, 0, 10, 124, 1, 0, 0, 0, 12, 126, 1, 0, 0, 0, 14, 133, 1, 0, 0, 0, 16, 135, 1, 0, 0, 0, 18, 154, 1, 0, 0, 0, 20, 161, 1, 0, 0, 0, 22, 173, 1, 0, 0, 0, 24, 178, 1, 0, 0, 0, 26, 187, 1, 0, 0, 0, 28, 203, 1, 0, 0, 0, 30, 221, 1, 0, 0, 0, 32, 227, 1, 0, 0, 0, 34, 237, 1, 0, 0, 0, 36, 239, 1, 0, 0, 0, 38, 241, 1, 0, 0, 0, 40, 252, 1, 0, 0, 0, 42, 254, 1, 0, 0, 0, 44, 265, 1, 0, 0, 0, 46, 283, 1, 0, 0, 0, 48, 285, 1, 0, 0, 0, 50, 296, 1, 0, 0, 0, 52, 305, 1, 0, 0, 0, 54, 308, 1, 0, 0, 0, 56, 320, 1, 0, 0, 0, 58, 322, 1, 0, 0, 0, 60, 330, 1, 0, 0, 0, 62, 336, 1, 0, 0, 0, 64, 348, 1, 0, 0, 0, 66, 350, 1, 0, 0, 0, 68, 354, 1, 0, 0, 0, 70, 356, 1, 0, 0, 0, 72, 372, 1, 0, 0, 0, 74, 375, 1, 0, 0, 0, 76, 440, 1, 0, 0, 0, 78, 497, 1, 0, 0, 0, 80, 504, 1, 0, 0, 0, 82, 506, 1, 0, 0, 0, 84, 510, 1, 0, 0, 0, 86, 512, 1, 0, 0, 0, 88, 90, 3, 2, 1, 0, 89, 88, 1, 0, 0, 0, 90, 93, 1, 0, 0, 0, 91, 89, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 97, 1, 0, 0, 0, 93, 91, 1, 0, 0, 0, 94, 96, 3, 12, 6, 0, 95, 94, 1, 0, 0, 0, 96, 99, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 97, 98, 1, 0, 0, 0, 98, 103, 1, 0, 0, 0, 99, 97, 1, 0, 0, 0, 100, 102, 3, 14, 7, 0, 101, 100, 1, 0, 0, 0, 102, 105, 1, 0, 0, 0, 103, 101, 1, 0, 0, 0, 103, 104, 1, 0, 0, 0, 104, 106, 1, 0, 0, 0, 105, 103, 1, 0, 0, 0, 106, 107, 5, 0, 0, 1, 107, 1, 1, 0, 0, 0, 108, 109, 5, 1, 0, 0, 109, 110, 3, 4, 2, 0, 110, 111, 3, 6, 3, 0, 111, 112, 5, 2, 0, 0, 112, 3, 1, 0, 0, 0, 113, 114, 5, 3, 0, 0, 114, 5, 1, 0, 0, 0, 115, 117, 3, 8, 4, 0, 116, 118, 3, 8, 4, 0, 117, 116, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 7, 1, 0, 0, 0, 119, 121, 3, 10, 5, 0, 120, 119, 1, 0, 0, 0, 120, 121, 1, 0, 0, 0, 121, 122, 1, 0, 0, 0, 122, 123, 5, 66, 0, 0, 123, 9, 1, 0, 0, 0, 124, 125, 7, 0, 0, 0, 125, 11, 1, 0, 0, 0, 126, 127, 5, 11, 0, 0, 127, 128, 5, 76, 0, 0, 128, 129, 5, 2, 0, 0, 129, 13, 1, 0, 0, 0, 130, 134, 3, 16, 8, 0, 131, 134, 3, 18, 9, 0, 132, 134, 3, 20, 10, 0, 133, 130, 1, 0, 0, 0, 133, 131, 1, 0, 0, 0, 133, 132, 1, 0, 0, 0, 134, 15, 1, 0, 0, 0, 135, 136, 5, 12, 0, 0, 136, 137, 5, 82, 0, 0, 137, 150, 3, 26, 13, 0, 138, 139, 5, 13, 0, 0, 139, 140, 5, 14, 0, 0, 140, 145, 3, 84, 42, 0, 141, 142, 5, 15, 0, 0, 142, 144, 3, 84, 42, 0, 143, 141, 1, 0, 0, 0, 144, 147, 1, 0, 0, 0, 145, 143, 1, 0, 0, 0, 145, 146, 1, 0, 0, 0, 146, 148, 1, 0, 0, 0, 147, 145, 1, 0, 0, 0, 148, 149, 5, 16, 0, 0, 149, 151, 1, 0, 0, 0, 150, 138, 1, 0, 0, 0, 150, 151, 1, 0, 0, 0, 151, 152, 1, 0, 0, 0, 152, 153, 3, 24, 12, 0, 153, 17, 1, 0, 0, 0, 154, 155, 3, 84, 42, 0, 155, 156, 5, 17, 0, 0, 156, 157, 5, 82, 0, 0, 157, 158, 5, 10, 0, 0, 158, 159, 3, 76, 38, 0, 159, 160, 5, 2, 0, 0, 160, 19, 1, 0, 0, 0, 161, 162, 5, 18, 0, 0, 162, 163, 5, 82, 0, 0, 163, 164, 3, 26, 13, 0, 164, 168, 5, 19, 0, 0, 165, 167, 3, 22, 11, 0, 166, 165, 1, 0, 0, 0, 167, 170, 1, 0, 0, 0, 168, 166, 1, 0, 0, 0, 168, 169, 1, 0, 0, 0, 169, 171, 1, 0, 0, 0, 170, 168, 1, 0, 0, 0, 171, 172, 5, 20, 0, 0, 172, 21, 1, 0, 0, 0, 173, 174, 5, 12, 0, 0, 174, 175, 5, 82, 0, 0, 175, 176, 3, 26, 13, 0, 176, 177, 3, 24, 12, 0, 177, 23, 1, 0, 0, 0, 178, 182, 5, 19, 0, 0, 179, 181, 3, 32, 16, 0, 180, 179, 1, 0, 0, 0, 181, 184, 1, 0, 0, 0, 182, 180, 1, 0, 0, 0, 182, 183, 1, 0, 0, 0, 183, 185, 1, 0, 0, 0, 184, 182, 1, 0, 0, 0, 185, 186, 5, 20, 0, 0, 186, 25, 1, 0, 0, 0, 187, 199, 5, 14, 0, 0, 188, 193, 3, 28, 14, 0, 189, 190, 5, 15, 0, 0, 190, 192, 3, 28, 14, 0, 191, 189, 1, 0, 0, 0, 192, 195, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0, 194, 197, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 196, 198, 5, 15, 0, 0, 197, 196, 1, 0, 0, 0, 197, 198, 1, 0, 0, 0, 198, 200, 1, 0, 0, 0, 199, 188, 1, 0, 0, 0, 199, 200, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 202, 5, 16, 0, 0, 202, 27, 1, 0, 0, 0, 203, 207, 3, 84, 42, 0, 204, 206, 3, 78, 39, 0, 205, 204, 1, 0, 0, 0, 206, 209, 1, 0, 0, 0, 207, 205, 1, 0, 0, 0, 207, 208, 1, 0, 0, 0, 208, 210, 1, 0, 0, 0, 209, 207, 1, 0, 0, 0, 210, 211, 5, 82, 0, 0, 211, 29, 1, 0, 0, 0, 212, 216, 5, 19, 0, 0, 213, 215, 3, 32, 16, 0, 214, 213, 1, 0, 0, 0, 215, 218, 1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 219, 1, 0, 0, 0, 218, 216, 1, 0, 0, 0, 219, 222, 5, 20, 0, 0, 220, 222, 3, 32, 16, 0, 221, 212, 1, 0, 0, 0, 221, 220, 1, 0, 0, 0, 222, 31, 1, 0, 0, 0, 223, 228, 3, 40, 20, 0, 224, 225, 3, 34, 17, 0, 225, 226, 5, 2, 0, 0, 226, 228, 1, 0, 0, 0, 227, 223, 1, 0, 0, 0, 227, 224, 1, 0, 0, 0, 228, 33, 1, 0, 0, 0, 229, 238, 3, 42, 21, 0, 230, 238, 3, 44, 22, 0, 231, 238, 3, 46, 23, 0, 232, 238, 3, 48, 24, 0, 233, 238, 3, 50, 25, 0, 234, 238, 3, 36, 18, 0, 235, 238, 3, 52, 26, 0, 236, 238, 3, 38, 19, 0, 237, 229, 1, 0, 0, 0, 237, 230, 1, 0, 0, 0, 237, 231, 1, 0, 0, 0, 237, 232, 1, 0, 0, 0, 237, 233, 1, 0, 0, 0, 237, 234, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 236, 1, 0, 0, 0, 238, 35, 1, 0, 0, 0, 239, 240, 3, 72, 36, 0, 240, 37, 1, 0, 0, 0, 241, 242, 5, 21, 0, 0, 242, 247, 3, 76, 38, 0, 243, 244, 5, 15, 0, 0, 244, 246, 3, 76, 38, 0, 245, 243, 1, 0, 0, 0, 246, 249, 1, 0, 0, 0, 247, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 39, 1, 0, 0, 0, 249, 247, 1, 0, 0, 0, 250, 253, 3, 54, 27, 0, 251, 253, 3, 56, 28, 0, 252, 250, 1, 0, 0, 0, 252, 251, 1, 0, 0, 0, 253, 41, 1, 0, 0, 0, 254, 258, 3, 84, 42, 0, 255, 257, 3, 78, 39, 0, 256, 255, 1, 0, 0, 0, 257, 260, 1, 0, 0, 0, 258, 256, 1, 0, 0, 0, 258, 259, 1, 0, 0, 0, 259, 261, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 261, 262, 5, 82, 0, 0, 262, 263, 5, 10, 0, 0, 263, 264, 3, 76, 38, 0, 264, 43, 1, 0, 0, 0, 265, 266, 3, 84, 42, 0, 266, 271, 5, 82, 0, 0, 267, 268, 5, 15, 0, 0, 268, 269, 3, 84, 42, 0, 269, 270, 5, 82, 0, 0, 270, 272, 1, 0, 0, 0, 271, 267, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 271, 1, 0, 0, 0, 273, 274, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 276, 5, 10, 0, 0, 276, 277, 3, 76, 38, 0, 277, 45, 1, 0, 0, 0, 278, 279, 5, 82, 0, 0, 279, 280, 7, 1, 0, 0, 280, 284, 3, 76, 38, 0, 281, 282, 5, 82, 0, 0, 282, 284, 7, 2, 0, 0, 283, 278, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 47, 1, 0, 0, 0, 285, 286, 5, 26, 0, 0, 286, 287, 5, 14, 0, 0, 287, 288, 5, 79, 0, 0, 288, 289, 5, 6, 0, 0, 289, 292, 3, 76, 38, 0, 290, 291, 5, 15, 0, 0, 291, 293, 3, 66, 33, 0, 292, 290, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 5, 16, 0, 0, 295, 49, 1, 0, 0, 0, 296, 297, 5, 26, 0, 0, 297, 298, 5, 14, 0, 0, 298, 301, 3, 76, 38, 0, 299, 300, 5, 15, 0, 0, 300, 302, 3, 66, 33, 0, 301, 299, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 303, 1, 0, 0, 0, 303, 304, 5, 16, 0, 0, 304, 51, 1, 0, 0, 0, 305, 306, 5, 27, 0, 0, 306, 307, 3, 70, 35, 0, 307, 53, 1, 0, 0, 0, 308, 309, 5, 28, 0, 0, 309, 310, 5, 14, 0, 0, 310, 311, 3, 76, 38, 0, 311, 312, 5, 16, 0, 0, 312, 315, 3, 30, 15, 0, 313, 314, 5, 29, 0, 0, 314, 316, 3, 30, 15, 0, 315, 313, 1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 55, 1, 0, 0, 0, 317, 321, 3, 58, 29, 0, 318, 321, 3, 60, 30, 0, 319, 321, 3, 62, 31, 0, 320, 317, 1, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 319, 1, 0, 0, 0, 321, 57, 1, 0, 0, 0, 322, 323, 5, 30, 0, 0, 323, 324, 3, 30, 15, 0, 324, 325, 5, 31, 0, 0, 325, 326, 5, 14, 0, 0, 326, 327, 3, 76, 38, 0, 327, 328, 5, 16, 0, 0, 328, 329, 5, 2, 0, 0, 329, 59, 1, 0, 0, 0, 330, 331, 5, 31, 0, 0, 331, 332, 5, 14, 0, 0, 332, 333, 3, 76, 38, 0, 333, 334, 5, 16, 0, 0, 334, 335, 3, 30, 15, 0, 335, 61, 1, 0, 0, 0, 336, 337, 5, 32, 0, 0, 337, 338, 5, 14, 0, 0, 338, 339, 3, 64, 32, 0, 339, 340, 5, 2, 0, 0, 340, 341, 3, 76, 38, 0, 341, 342, 5, 2, 0, 0, 342, 343, 3, 46, 23, 0, 343, 344, 5, 16, 0, 0, 344, 345, 3, 30, 15, 0, 345, 63, 1, 0, 0, 0, 346, 349, 3, 42, 21, 0, 347, 349, 3, 46, 23, 0, 348, 346, 1, 0, 0, 0, 348, 347, 1, 0, 0, 0, 349, 65, 1, 0, 0, 0, 350, 351, 5, 76, 0, 0, 351, 67, 1, 0, 0, 0, 352, 355, 5, 82, 0, 0, 353, 355, 3, 80, 40, 0, 354, 352, 1, 0, 0, 0, 354, 353, 1, 0, 0, 0, 355, 69, 1, 0, 0, 0, 356, 368, 5, 14, 0, 0, 357, 362, 3, 68, 34, 0, 358, 359, 5, 15, 0, 0, 359, 361, 3, 68, 34, 0, 360, 358, 1, 0, 0, 0, 361, 364, 1, 0, 0, 0, 362, 360, 1, 0, 0, 0, 362, 363, 1, 0, 0, 0, 363, 366, 1, 0, 0, 0, 364, 362, 1, 0, 0, 0, 365, 367, 5, 15, 0, 0, 366, 365, 1, 0, 0, 0, 366, 367, 1, 0, 0, 0, 367, 369, 1, 0, 0, 0, 368, 357, 1, 0, 0, 0, 368, 369, 1, 0, 0, 0, 369, 370, 1, 0, 0, 0, 370, 371, 5, 16, 0, 0, 371, 71, 1, 0, 0, 0, 372, 373, 5, 82, 0, 0, 373, 374, 3, 74, 37, 0, 374, 73, 1, 0, 0, 0, 375, 387, 5, 14, 0, 0, 376, 381, 3, 76, 38, 0, 377, 378, 5, 15, 0, 0, 378, 380, 3, 76, 38, 0, 379, 377, 1, 0, 0, 0, 380, 383, 1, 0, 0, 0, 381, 379, 1, 0, 0, 0, 381, 382, 1, 0, 0, 0, 382, 385, 1, 0, 0, 0, 383, 381, 1, 0, 0, 0, 384, 386, 5, 15, 0, 0, 385, 384, 1, 0, 0, 0, 385, 386, 1, 0, 0, 0, 386, 388, 1, 0, 0, 0, 387, 376, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 389, 1, 0, 0, 0, 389, 390, 5, 16, 0, 0, 390, 75, 1, 0, 0, 0, 391, 392, 6, 38, -1, 0, 392, 393, 5, 14, 0, 0, 393, 394, 3, 76, 38, 0, 394, 395, 5, 16, 0, 0, 395, 441, 1, 0, 0, 0, 396, 397, 3, 86, 43, 0, 397, 398, 5, 14, 0, 0, 398, 400, 3, 76, 38, 0, 399, 401, 5, 15, 0, 0, 400, 399, 1, 0, 0, 0, 400, 401, 1, 0, 0, 0, 401, 402, 1, 0, 0, 0, 402, 403, 5, 16, 0, 0, 403, 441, 1, 0, 0, 0, 404, 441, 3, 72, 36, 0, 405, 406, 5, 33, 0, 0, 406, 407, 5, 82, 0, 0, 407, 441, 3, 74, 37, 0, 408, 409, 5, 36, 0, 0, 409, 410, 5, 34, 0, 0, 410, 411, 3, 76, 38, 0, 411, 412, 5, 35, 0, 0, 412, 413, 7, 3, 0, 0, 413, 441, 1, 0, 0, 0, 414, 415, 5, 42, 0, 0, 415, 416, 5, 34, 0, 0, 416, 417, 3, 76, 38, 0, 417, 418, 5, 35, 0, 0, 418, 419, 7, 4, 0, 0, 419, 441, 1, 0, 0, 0, 420, 421, 7, 5, 0, 0, 421, 441, 3, 76, 38, 15, 422, 434, 5, 34, 0, 0, 423, 428, 3, 76, 38, 0, 424, 425, 5, 15, 0, 0, 425, 427, 3, 76, 38, 0, 426, 424, 1, 0, 0, 0, 427, 430, 1, 0, 0, 0, 428, 426, 1, 0, 0, 0, 428, 429, 1, 0, 0, 0, 429, 432, 1, 0, 0, 0, 430, 428, 1, 0, 0, 0, 431, 433, 5, 15, 0, 0, 432, 431, 1, 0, 0, 0, 432, 433, 1, 0, 0, 0, 433, 435, 1, 0, 0, 0, 434, 423, 1, 0, 0, 0, 434, 435, 1, 0, 0, 0, 435, 436, 1, 0, 0, 0, 436, 441, 5, 35, 0, 0, 437, 441, 5, 81, 0, 0, 438, 441, 5, 82, 0, 0, 439, 441, 3, 80, 40, 0, 440, 391, 1, 0, 0, 0, 440, 396, 1, 0, 0, 0, 440, 404, 1, 0, 0, 0, 440, 405, 1, 0, 0, 0, 440, 408, 1, 0, 0, 0, 440, 414, 1, 0, 0, 0, 440, 420, 1, 0, 0, 0, 440, 422, 1, 0, 0, 0, 440, 437, 1, 0, 0, 0, 440, 438, 1, 0, 0, 0, 440, 439, 1, 0, 0, 0, 441, 494, 1, 0, 0, 0, 442, 443, 10, 14, 0, 0, 443, 444, 7, 6, 0, 0, 444, 493, 3, 76, 38, 15, 445, 446, 10, 13, 0, 0, 446, 447, 7, 7, 0, 0, 447, 493, 3, 76, 38, 14, 448, 449, 10, 12, 0, 0, 449, 450, 7, 8, 0, 0, 450, 493, 3, 76, 38, 13, 451, 452, 10, 11, 0, 0, 452, 453, 7, 9, 0, 0, 453, 493, 3, 76, 38, 12, 454, 455, 10, 10, 0, 0, 455, 456, 7, 10, 0, 0, 456, 493, 3, 76, 38, 11, 457, 458, 10, 9, 0, 0, 458, 459, 5, 61, 0, 0, 459, 493, 3, 76, 38, 10, 460, 461, 10, 8, 0, 0, 461, 462, 5, 4, 0, 0, 462, 493, 3, 76, 38, 9, 463, 464, 10, 7, 0, 0, 464, 465, 5, 62, 0, 0, 465, 493, 3, 76, 38, 8, 466, 467, 10, 6, 0, 0, 467, 468, 5, 63, 0, 0, 468, 493, 3, 76, 38, 7, 469, 470, 10, 5, 0, 0, 470, 471, 5, 64, 0, 0, 471, 493, 3, 76, 38, 6, 472, 473, 10, 21, 0, 0, 473, 474, 5, 34, 0, 0, 474, 475, 5, 69, 0, 0, 475, 493, 5, 35, 0, 0, 476, 477, 10, 18, 0, 0, 477, 493, 7, 11, 0, 0, 478, 479, 10, 17, 0, 0, 479, 480, 5, 49, 0, 0, 480, 481, 5, 14, 0, 0, 481, 482, 3, 76, 38, 0, 482, 483, 5, 16, 0, 0, 483, 493, 1, 0, 0, 0, 484, 485, 10, 16, 0, 0, 485, 486, 5, 50, 0, 0, 486, 487, 5, 14, 0, 0, 487, 488, 3, 76, 38, 0, 488, 489, 5, 15, 0, 0, 489, 490, 3, 76, 38, 0, 490, 491, 5, 16, 0, 0, 491, 493, 1, 0, 0, 0, 492, 442, 1, 0, 0, 0, 492, 445, 1, 0, 0, 0, 492, 448, 1, 0, 0, 0, 492, 451, 1, 0, 0, 0, 492, 454, 1, 0, 0, 0, 492, 457, 1, 0, 0, 0, 492, 460, 1, 0, 0, 0, 492, 463, 1, 0, 0, 0, 492, 466, 1, 0, 0, 0, 492, 469, 1, 0, 0, 0, 492, 472, 1, 0, 0, 0, 492, 476, 1, 0, 0, 0, 492, 478, 1, 0, 0, 0, 492, 484, 1, 0, 0, 0, 493, 496, 1, 0, 0, 0, 494, 492, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 77, 1, 0, 0, 0, 496, 494, 1, 0, 0, 0, 497, 498, 7, 12, 0, 0, 498, 79, 1, 0, 0, 0, 499, 505, 5, 67, 0, 0, 500, 505, 3, 82, 41, 0, 501, 505, 5, 76, 0, 0, 502, 505, 5, 77, 0, 0, 503, 505, 5, 78, 0, 0, 504, 499, 1, 0, 0, 0, 504, 500, 1, 0, 0, 0, 504, 501, 1, 0, 0, 0, 504, 502, 1, 0, 0, 0, 504, 503, 1, 0, 0, 0, 505, 81, 1, 0, 0, 0, 506, 508, 5, 69, 0, 0, 507, 509, 5, 68, 0, 0, 508, 507, 1, 0, 0, 0, 508, 509, 1, 0, 0, 0, 509, 83, 1, 0, 0, 0, 510, 511, 7, 13, 0, 0, 511, 85, 1, 0, 0, 0, 512, 513, 7, 14, 0, 0, 513, 87, 1, 0, 0, 0, 44, 91, 97, 103, 117, 120, 133, 145, 150, 168, 182, 193, 197, 199, 207, 216, 221, 227, 237, 247, 252, 258, 273, 283, 292, 301, 315, 320, 348, 354, 362, 366, 368, 381, 385, 387, 400, 428, 432, 434, 440, 492, 494, 504, 508] \ No newline at end of file +[4, 1, 85, 534, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 1, 0, 5, 0, 92, 8, 0, 10, 0, 12, 0, 95, 9, 0, 1, 0, 5, 0, 98, 8, 0, 10, 0, 12, 0, 101, 9, 0, 1, 0, 5, 0, 104, 8, 0, 10, 0, 12, 0, 107, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 3, 3, 120, 8, 3, 1, 4, 3, 4, 123, 8, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 3, 7, 136, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 5, 8, 146, 8, 8, 10, 8, 12, 8, 149, 9, 8, 1, 8, 1, 8, 3, 8, 153, 8, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 5, 10, 169, 8, 10, 10, 10, 12, 10, 172, 9, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 5, 12, 183, 8, 12, 10, 12, 12, 12, 186, 9, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 5, 13, 194, 8, 13, 10, 13, 12, 13, 197, 9, 13, 1, 13, 3, 13, 200, 8, 13, 3, 13, 202, 8, 13, 1, 13, 1, 13, 1, 14, 1, 14, 5, 14, 208, 8, 14, 10, 14, 12, 14, 211, 9, 14, 1, 14, 1, 14, 1, 15, 1, 15, 5, 15, 217, 8, 15, 10, 15, 12, 15, 220, 9, 15, 1, 15, 1, 15, 3, 15, 224, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 3, 16, 230, 8, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 240, 8, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 5, 19, 248, 8, 19, 10, 19, 12, 19, 251, 9, 19, 1, 20, 1, 20, 3, 20, 255, 8, 20, 1, 21, 1, 21, 5, 21, 259, 8, 21, 10, 21, 12, 21, 262, 9, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 4, 22, 271, 8, 22, 11, 22, 12, 22, 272, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 4, 22, 282, 8, 22, 11, 22, 12, 22, 283, 1, 22, 1, 22, 1, 22, 1, 22, 3, 22, 290, 8, 22, 1, 23, 1, 23, 1, 23, 1, 23, 3, 23, 296, 8, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 3, 24, 303, 8, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 3, 25, 312, 8, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 321, 8, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 3, 28, 335, 8, 28, 1, 29, 1, 29, 1, 29, 3, 29, 340, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 3, 33, 368, 8, 33, 1, 34, 1, 34, 1, 35, 1, 35, 3, 35, 374, 8, 35, 1, 36, 1, 36, 1, 36, 1, 36, 5, 36, 380, 8, 36, 10, 36, 12, 36, 383, 9, 36, 1, 36, 3, 36, 386, 8, 36, 3, 36, 388, 8, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 399, 8, 38, 10, 38, 12, 38, 402, 9, 38, 1, 38, 3, 38, 405, 8, 38, 3, 38, 407, 8, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 420, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 5, 39, 446, 8, 39, 10, 39, 12, 39, 449, 9, 39, 1, 39, 3, 39, 452, 8, 39, 3, 39, 454, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 460, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 5, 39, 512, 8, 39, 10, 39, 12, 39, 515, 9, 39, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 524, 8, 41, 1, 42, 1, 42, 3, 42, 528, 8, 42, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 0, 1, 78, 45, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 0, 15, 1, 0, 4, 10, 2, 0, 10, 10, 22, 23, 1, 0, 24, 25, 1, 0, 37, 41, 2, 0, 37, 41, 43, 46, 2, 0, 5, 5, 51, 52, 1, 0, 53, 55, 2, 0, 52, 52, 56, 56, 1, 0, 57, 58, 1, 0, 6, 9, 1, 0, 59, 60, 1, 0, 47, 48, 2, 0, 17, 17, 65, 65, 1, 0, 72, 74, 2, 0, 72, 73, 80, 80, 567, 0, 93, 1, 0, 0, 0, 2, 110, 1, 0, 0, 0, 4, 115, 1, 0, 0, 0, 6, 117, 1, 0, 0, 0, 8, 122, 1, 0, 0, 0, 10, 126, 1, 0, 0, 0, 12, 128, 1, 0, 0, 0, 14, 135, 1, 0, 0, 0, 16, 137, 1, 0, 0, 0, 18, 156, 1, 0, 0, 0, 20, 163, 1, 0, 0, 0, 22, 175, 1, 0, 0, 0, 24, 180, 1, 0, 0, 0, 26, 189, 1, 0, 0, 0, 28, 205, 1, 0, 0, 0, 30, 223, 1, 0, 0, 0, 32, 229, 1, 0, 0, 0, 34, 239, 1, 0, 0, 0, 36, 241, 1, 0, 0, 0, 38, 243, 1, 0, 0, 0, 40, 254, 1, 0, 0, 0, 42, 256, 1, 0, 0, 0, 44, 289, 1, 0, 0, 0, 46, 295, 1, 0, 0, 0, 48, 302, 1, 0, 0, 0, 50, 304, 1, 0, 0, 0, 52, 315, 1, 0, 0, 0, 54, 324, 1, 0, 0, 0, 56, 327, 1, 0, 0, 0, 58, 339, 1, 0, 0, 0, 60, 341, 1, 0, 0, 0, 62, 349, 1, 0, 0, 0, 64, 355, 1, 0, 0, 0, 66, 367, 1, 0, 0, 0, 68, 369, 1, 0, 0, 0, 70, 373, 1, 0, 0, 0, 72, 375, 1, 0, 0, 0, 74, 391, 1, 0, 0, 0, 76, 394, 1, 0, 0, 0, 78, 459, 1, 0, 0, 0, 80, 516, 1, 0, 0, 0, 82, 523, 1, 0, 0, 0, 84, 525, 1, 0, 0, 0, 86, 529, 1, 0, 0, 0, 88, 531, 1, 0, 0, 0, 90, 92, 3, 2, 1, 0, 91, 90, 1, 0, 0, 0, 92, 95, 1, 0, 0, 0, 93, 91, 1, 0, 0, 0, 93, 94, 1, 0, 0, 0, 94, 99, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 96, 98, 3, 12, 6, 0, 97, 96, 1, 0, 0, 0, 98, 101, 1, 0, 0, 0, 99, 97, 1, 0, 0, 0, 99, 100, 1, 0, 0, 0, 100, 105, 1, 0, 0, 0, 101, 99, 1, 0, 0, 0, 102, 104, 3, 14, 7, 0, 103, 102, 1, 0, 0, 0, 104, 107, 1, 0, 0, 0, 105, 103, 1, 0, 0, 0, 105, 106, 1, 0, 0, 0, 106, 108, 1, 0, 0, 0, 107, 105, 1, 0, 0, 0, 108, 109, 5, 0, 0, 1, 109, 1, 1, 0, 0, 0, 110, 111, 5, 1, 0, 0, 111, 112, 3, 4, 2, 0, 112, 113, 3, 6, 3, 0, 113, 114, 5, 2, 0, 0, 114, 3, 1, 0, 0, 0, 115, 116, 5, 3, 0, 0, 116, 5, 1, 0, 0, 0, 117, 119, 3, 8, 4, 0, 118, 120, 3, 8, 4, 0, 119, 118, 1, 0, 0, 0, 119, 120, 1, 0, 0, 0, 120, 7, 1, 0, 0, 0, 121, 123, 3, 10, 5, 0, 122, 121, 1, 0, 0, 0, 122, 123, 1, 0, 0, 0, 123, 124, 1, 0, 0, 0, 124, 125, 5, 66, 0, 0, 125, 9, 1, 0, 0, 0, 126, 127, 7, 0, 0, 0, 127, 11, 1, 0, 0, 0, 128, 129, 5, 11, 0, 0, 129, 130, 5, 76, 0, 0, 130, 131, 5, 2, 0, 0, 131, 13, 1, 0, 0, 0, 132, 136, 3, 16, 8, 0, 133, 136, 3, 18, 9, 0, 134, 136, 3, 20, 10, 0, 135, 132, 1, 0, 0, 0, 135, 133, 1, 0, 0, 0, 135, 134, 1, 0, 0, 0, 136, 15, 1, 0, 0, 0, 137, 138, 5, 12, 0, 0, 138, 139, 5, 82, 0, 0, 139, 152, 3, 26, 13, 0, 140, 141, 5, 13, 0, 0, 141, 142, 5, 14, 0, 0, 142, 147, 3, 86, 43, 0, 143, 144, 5, 15, 0, 0, 144, 146, 3, 86, 43, 0, 145, 143, 1, 0, 0, 0, 146, 149, 1, 0, 0, 0, 147, 145, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 150, 1, 0, 0, 0, 149, 147, 1, 0, 0, 0, 150, 151, 5, 16, 0, 0, 151, 153, 1, 0, 0, 0, 152, 140, 1, 0, 0, 0, 152, 153, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 155, 3, 24, 12, 0, 155, 17, 1, 0, 0, 0, 156, 157, 3, 86, 43, 0, 157, 158, 5, 17, 0, 0, 158, 159, 5, 82, 0, 0, 159, 160, 5, 10, 0, 0, 160, 161, 3, 78, 39, 0, 161, 162, 5, 2, 0, 0, 162, 19, 1, 0, 0, 0, 163, 164, 5, 18, 0, 0, 164, 165, 5, 82, 0, 0, 165, 166, 3, 26, 13, 0, 166, 170, 5, 19, 0, 0, 167, 169, 3, 22, 11, 0, 168, 167, 1, 0, 0, 0, 169, 172, 1, 0, 0, 0, 170, 168, 1, 0, 0, 0, 170, 171, 1, 0, 0, 0, 171, 173, 1, 0, 0, 0, 172, 170, 1, 0, 0, 0, 173, 174, 5, 20, 0, 0, 174, 21, 1, 0, 0, 0, 175, 176, 5, 12, 0, 0, 176, 177, 5, 82, 0, 0, 177, 178, 3, 26, 13, 0, 178, 179, 3, 24, 12, 0, 179, 23, 1, 0, 0, 0, 180, 184, 5, 19, 0, 0, 181, 183, 3, 32, 16, 0, 182, 181, 1, 0, 0, 0, 183, 186, 1, 0, 0, 0, 184, 182, 1, 0, 0, 0, 184, 185, 1, 0, 0, 0, 185, 187, 1, 0, 0, 0, 186, 184, 1, 0, 0, 0, 187, 188, 5, 20, 0, 0, 188, 25, 1, 0, 0, 0, 189, 201, 5, 14, 0, 0, 190, 195, 3, 28, 14, 0, 191, 192, 5, 15, 0, 0, 192, 194, 3, 28, 14, 0, 193, 191, 1, 0, 0, 0, 194, 197, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 195, 196, 1, 0, 0, 0, 196, 199, 1, 0, 0, 0, 197, 195, 1, 0, 0, 0, 198, 200, 5, 15, 0, 0, 199, 198, 1, 0, 0, 0, 199, 200, 1, 0, 0, 0, 200, 202, 1, 0, 0, 0, 201, 190, 1, 0, 0, 0, 201, 202, 1, 0, 0, 0, 202, 203, 1, 0, 0, 0, 203, 204, 5, 16, 0, 0, 204, 27, 1, 0, 0, 0, 205, 209, 3, 86, 43, 0, 206, 208, 3, 80, 40, 0, 207, 206, 1, 0, 0, 0, 208, 211, 1, 0, 0, 0, 209, 207, 1, 0, 0, 0, 209, 210, 1, 0, 0, 0, 210, 212, 1, 0, 0, 0, 211, 209, 1, 0, 0, 0, 212, 213, 5, 82, 0, 0, 213, 29, 1, 0, 0, 0, 214, 218, 5, 19, 0, 0, 215, 217, 3, 32, 16, 0, 216, 215, 1, 0, 0, 0, 217, 220, 1, 0, 0, 0, 218, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 221, 1, 0, 0, 0, 220, 218, 1, 0, 0, 0, 221, 224, 5, 20, 0, 0, 222, 224, 3, 32, 16, 0, 223, 214, 1, 0, 0, 0, 223, 222, 1, 0, 0, 0, 224, 31, 1, 0, 0, 0, 225, 230, 3, 40, 20, 0, 226, 227, 3, 34, 17, 0, 227, 228, 5, 2, 0, 0, 228, 230, 1, 0, 0, 0, 229, 225, 1, 0, 0, 0, 229, 226, 1, 0, 0, 0, 230, 33, 1, 0, 0, 0, 231, 240, 3, 42, 21, 0, 232, 240, 3, 44, 22, 0, 233, 240, 3, 48, 24, 0, 234, 240, 3, 50, 25, 0, 235, 240, 3, 52, 26, 0, 236, 240, 3, 36, 18, 0, 237, 240, 3, 54, 27, 0, 238, 240, 3, 38, 19, 0, 239, 231, 1, 0, 0, 0, 239, 232, 1, 0, 0, 0, 239, 233, 1, 0, 0, 0, 239, 234, 1, 0, 0, 0, 239, 235, 1, 0, 0, 0, 239, 236, 1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 238, 1, 0, 0, 0, 240, 35, 1, 0, 0, 0, 241, 242, 3, 74, 37, 0, 242, 37, 1, 0, 0, 0, 243, 244, 5, 21, 0, 0, 244, 249, 3, 78, 39, 0, 245, 246, 5, 15, 0, 0, 246, 248, 3, 78, 39, 0, 247, 245, 1, 0, 0, 0, 248, 251, 1, 0, 0, 0, 249, 247, 1, 0, 0, 0, 249, 250, 1, 0, 0, 0, 250, 39, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 252, 255, 3, 56, 28, 0, 253, 255, 3, 58, 29, 0, 254, 252, 1, 0, 0, 0, 254, 253, 1, 0, 0, 0, 255, 41, 1, 0, 0, 0, 256, 260, 3, 86, 43, 0, 257, 259, 3, 80, 40, 0, 258, 257, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 264, 5, 82, 0, 0, 264, 265, 5, 10, 0, 0, 265, 266, 3, 78, 39, 0, 266, 43, 1, 0, 0, 0, 267, 270, 3, 46, 23, 0, 268, 269, 5, 15, 0, 0, 269, 271, 3, 46, 23, 0, 270, 268, 1, 0, 0, 0, 271, 272, 1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 1, 0, 0, 0, 274, 275, 5, 10, 0, 0, 275, 276, 3, 78, 39, 0, 276, 290, 1, 0, 0, 0, 277, 278, 5, 14, 0, 0, 278, 281, 3, 46, 23, 0, 279, 280, 5, 15, 0, 0, 280, 282, 3, 46, 23, 0, 281, 279, 1, 0, 0, 0, 282, 283, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 283, 284, 1, 0, 0, 0, 284, 285, 1, 0, 0, 0, 285, 286, 5, 16, 0, 0, 286, 287, 5, 10, 0, 0, 287, 288, 3, 78, 39, 0, 288, 290, 1, 0, 0, 0, 289, 267, 1, 0, 0, 0, 289, 277, 1, 0, 0, 0, 290, 45, 1, 0, 0, 0, 291, 292, 3, 86, 43, 0, 292, 293, 5, 82, 0, 0, 293, 296, 1, 0, 0, 0, 294, 296, 5, 82, 0, 0, 295, 291, 1, 0, 0, 0, 295, 294, 1, 0, 0, 0, 296, 47, 1, 0, 0, 0, 297, 298, 5, 82, 0, 0, 298, 299, 7, 1, 0, 0, 299, 303, 3, 78, 39, 0, 300, 301, 5, 82, 0, 0, 301, 303, 7, 2, 0, 0, 302, 297, 1, 0, 0, 0, 302, 300, 1, 0, 0, 0, 303, 49, 1, 0, 0, 0, 304, 305, 5, 26, 0, 0, 305, 306, 5, 14, 0, 0, 306, 307, 5, 79, 0, 0, 307, 308, 5, 6, 0, 0, 308, 311, 3, 78, 39, 0, 309, 310, 5, 15, 0, 0, 310, 312, 3, 68, 34, 0, 311, 309, 1, 0, 0, 0, 311, 312, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0, 313, 314, 5, 16, 0, 0, 314, 51, 1, 0, 0, 0, 315, 316, 5, 26, 0, 0, 316, 317, 5, 14, 0, 0, 317, 320, 3, 78, 39, 0, 318, 319, 5, 15, 0, 0, 319, 321, 3, 68, 34, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 322, 1, 0, 0, 0, 322, 323, 5, 16, 0, 0, 323, 53, 1, 0, 0, 0, 324, 325, 5, 27, 0, 0, 325, 326, 3, 72, 36, 0, 326, 55, 1, 0, 0, 0, 327, 328, 5, 28, 0, 0, 328, 329, 5, 14, 0, 0, 329, 330, 3, 78, 39, 0, 330, 331, 5, 16, 0, 0, 331, 334, 3, 30, 15, 0, 332, 333, 5, 29, 0, 0, 333, 335, 3, 30, 15, 0, 334, 332, 1, 0, 0, 0, 334, 335, 1, 0, 0, 0, 335, 57, 1, 0, 0, 0, 336, 340, 3, 60, 30, 0, 337, 340, 3, 62, 31, 0, 338, 340, 3, 64, 32, 0, 339, 336, 1, 0, 0, 0, 339, 337, 1, 0, 0, 0, 339, 338, 1, 0, 0, 0, 340, 59, 1, 0, 0, 0, 341, 342, 5, 30, 0, 0, 342, 343, 3, 30, 15, 0, 343, 344, 5, 31, 0, 0, 344, 345, 5, 14, 0, 0, 345, 346, 3, 78, 39, 0, 346, 347, 5, 16, 0, 0, 347, 348, 5, 2, 0, 0, 348, 61, 1, 0, 0, 0, 349, 350, 5, 31, 0, 0, 350, 351, 5, 14, 0, 0, 351, 352, 3, 78, 39, 0, 352, 353, 5, 16, 0, 0, 353, 354, 3, 30, 15, 0, 354, 63, 1, 0, 0, 0, 355, 356, 5, 32, 0, 0, 356, 357, 5, 14, 0, 0, 357, 358, 3, 66, 33, 0, 358, 359, 5, 2, 0, 0, 359, 360, 3, 78, 39, 0, 360, 361, 5, 2, 0, 0, 361, 362, 3, 48, 24, 0, 362, 363, 5, 16, 0, 0, 363, 364, 3, 30, 15, 0, 364, 65, 1, 0, 0, 0, 365, 368, 3, 42, 21, 0, 366, 368, 3, 48, 24, 0, 367, 365, 1, 0, 0, 0, 367, 366, 1, 0, 0, 0, 368, 67, 1, 0, 0, 0, 369, 370, 5, 76, 0, 0, 370, 69, 1, 0, 0, 0, 371, 374, 5, 82, 0, 0, 372, 374, 3, 82, 41, 0, 373, 371, 1, 0, 0, 0, 373, 372, 1, 0, 0, 0, 374, 71, 1, 0, 0, 0, 375, 387, 5, 14, 0, 0, 376, 381, 3, 70, 35, 0, 377, 378, 5, 15, 0, 0, 378, 380, 3, 70, 35, 0, 379, 377, 1, 0, 0, 0, 380, 383, 1, 0, 0, 0, 381, 379, 1, 0, 0, 0, 381, 382, 1, 0, 0, 0, 382, 385, 1, 0, 0, 0, 383, 381, 1, 0, 0, 0, 384, 386, 5, 15, 0, 0, 385, 384, 1, 0, 0, 0, 385, 386, 1, 0, 0, 0, 386, 388, 1, 0, 0, 0, 387, 376, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 389, 1, 0, 0, 0, 389, 390, 5, 16, 0, 0, 390, 73, 1, 0, 0, 0, 391, 392, 5, 82, 0, 0, 392, 393, 3, 76, 38, 0, 393, 75, 1, 0, 0, 0, 394, 406, 5, 14, 0, 0, 395, 400, 3, 78, 39, 0, 396, 397, 5, 15, 0, 0, 397, 399, 3, 78, 39, 0, 398, 396, 1, 0, 0, 0, 399, 402, 1, 0, 0, 0, 400, 398, 1, 0, 0, 0, 400, 401, 1, 0, 0, 0, 401, 404, 1, 0, 0, 0, 402, 400, 1, 0, 0, 0, 403, 405, 5, 15, 0, 0, 404, 403, 1, 0, 0, 0, 404, 405, 1, 0, 0, 0, 405, 407, 1, 0, 0, 0, 406, 395, 1, 0, 0, 0, 406, 407, 1, 0, 0, 0, 407, 408, 1, 0, 0, 0, 408, 409, 5, 16, 0, 0, 409, 77, 1, 0, 0, 0, 410, 411, 6, 39, -1, 0, 411, 412, 5, 14, 0, 0, 412, 413, 3, 78, 39, 0, 413, 414, 5, 16, 0, 0, 414, 460, 1, 0, 0, 0, 415, 416, 3, 88, 44, 0, 416, 417, 5, 14, 0, 0, 417, 419, 3, 78, 39, 0, 418, 420, 5, 15, 0, 0, 419, 418, 1, 0, 0, 0, 419, 420, 1, 0, 0, 0, 420, 421, 1, 0, 0, 0, 421, 422, 5, 16, 0, 0, 422, 460, 1, 0, 0, 0, 423, 460, 3, 74, 37, 0, 424, 425, 5, 33, 0, 0, 425, 426, 5, 82, 0, 0, 426, 460, 3, 76, 38, 0, 427, 428, 5, 36, 0, 0, 428, 429, 5, 34, 0, 0, 429, 430, 3, 78, 39, 0, 430, 431, 5, 35, 0, 0, 431, 432, 7, 3, 0, 0, 432, 460, 1, 0, 0, 0, 433, 434, 5, 42, 0, 0, 434, 435, 5, 34, 0, 0, 435, 436, 3, 78, 39, 0, 436, 437, 5, 35, 0, 0, 437, 438, 7, 4, 0, 0, 438, 460, 1, 0, 0, 0, 439, 440, 7, 5, 0, 0, 440, 460, 3, 78, 39, 15, 441, 453, 5, 34, 0, 0, 442, 447, 3, 78, 39, 0, 443, 444, 5, 15, 0, 0, 444, 446, 3, 78, 39, 0, 445, 443, 1, 0, 0, 0, 446, 449, 1, 0, 0, 0, 447, 445, 1, 0, 0, 0, 447, 448, 1, 0, 0, 0, 448, 451, 1, 0, 0, 0, 449, 447, 1, 0, 0, 0, 450, 452, 5, 15, 0, 0, 451, 450, 1, 0, 0, 0, 451, 452, 1, 0, 0, 0, 452, 454, 1, 0, 0, 0, 453, 442, 1, 0, 0, 0, 453, 454, 1, 0, 0, 0, 454, 455, 1, 0, 0, 0, 455, 460, 5, 35, 0, 0, 456, 460, 5, 81, 0, 0, 457, 460, 5, 82, 0, 0, 458, 460, 3, 82, 41, 0, 459, 410, 1, 0, 0, 0, 459, 415, 1, 0, 0, 0, 459, 423, 1, 0, 0, 0, 459, 424, 1, 0, 0, 0, 459, 427, 1, 0, 0, 0, 459, 433, 1, 0, 0, 0, 459, 439, 1, 0, 0, 0, 459, 441, 1, 0, 0, 0, 459, 456, 1, 0, 0, 0, 459, 457, 1, 0, 0, 0, 459, 458, 1, 0, 0, 0, 460, 513, 1, 0, 0, 0, 461, 462, 10, 14, 0, 0, 462, 463, 7, 6, 0, 0, 463, 512, 3, 78, 39, 15, 464, 465, 10, 13, 0, 0, 465, 466, 7, 7, 0, 0, 466, 512, 3, 78, 39, 14, 467, 468, 10, 12, 0, 0, 468, 469, 7, 8, 0, 0, 469, 512, 3, 78, 39, 13, 470, 471, 10, 11, 0, 0, 471, 472, 7, 9, 0, 0, 472, 512, 3, 78, 39, 12, 473, 474, 10, 10, 0, 0, 474, 475, 7, 10, 0, 0, 475, 512, 3, 78, 39, 11, 476, 477, 10, 9, 0, 0, 477, 478, 5, 61, 0, 0, 478, 512, 3, 78, 39, 10, 479, 480, 10, 8, 0, 0, 480, 481, 5, 4, 0, 0, 481, 512, 3, 78, 39, 9, 482, 483, 10, 7, 0, 0, 483, 484, 5, 62, 0, 0, 484, 512, 3, 78, 39, 8, 485, 486, 10, 6, 0, 0, 486, 487, 5, 63, 0, 0, 487, 512, 3, 78, 39, 7, 488, 489, 10, 5, 0, 0, 489, 490, 5, 64, 0, 0, 490, 512, 3, 78, 39, 6, 491, 492, 10, 21, 0, 0, 492, 493, 5, 34, 0, 0, 493, 494, 5, 69, 0, 0, 494, 512, 5, 35, 0, 0, 495, 496, 10, 18, 0, 0, 496, 512, 7, 11, 0, 0, 497, 498, 10, 17, 0, 0, 498, 499, 5, 49, 0, 0, 499, 500, 5, 14, 0, 0, 500, 501, 3, 78, 39, 0, 501, 502, 5, 16, 0, 0, 502, 512, 1, 0, 0, 0, 503, 504, 10, 16, 0, 0, 504, 505, 5, 50, 0, 0, 505, 506, 5, 14, 0, 0, 506, 507, 3, 78, 39, 0, 507, 508, 5, 15, 0, 0, 508, 509, 3, 78, 39, 0, 509, 510, 5, 16, 0, 0, 510, 512, 1, 0, 0, 0, 511, 461, 1, 0, 0, 0, 511, 464, 1, 0, 0, 0, 511, 467, 1, 0, 0, 0, 511, 470, 1, 0, 0, 0, 511, 473, 1, 0, 0, 0, 511, 476, 1, 0, 0, 0, 511, 479, 1, 0, 0, 0, 511, 482, 1, 0, 0, 0, 511, 485, 1, 0, 0, 0, 511, 488, 1, 0, 0, 0, 511, 491, 1, 0, 0, 0, 511, 495, 1, 0, 0, 0, 511, 497, 1, 0, 0, 0, 511, 503, 1, 0, 0, 0, 512, 515, 1, 0, 0, 0, 513, 511, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 79, 1, 0, 0, 0, 515, 513, 1, 0, 0, 0, 516, 517, 7, 12, 0, 0, 517, 81, 1, 0, 0, 0, 518, 524, 5, 67, 0, 0, 519, 524, 3, 84, 42, 0, 520, 524, 5, 76, 0, 0, 521, 524, 5, 77, 0, 0, 522, 524, 5, 78, 0, 0, 523, 518, 1, 0, 0, 0, 523, 519, 1, 0, 0, 0, 523, 520, 1, 0, 0, 0, 523, 521, 1, 0, 0, 0, 523, 522, 1, 0, 0, 0, 524, 83, 1, 0, 0, 0, 525, 527, 5, 69, 0, 0, 526, 528, 5, 68, 0, 0, 527, 526, 1, 0, 0, 0, 527, 528, 1, 0, 0, 0, 528, 85, 1, 0, 0, 0, 529, 530, 7, 13, 0, 0, 530, 87, 1, 0, 0, 0, 531, 532, 7, 14, 0, 0, 532, 89, 1, 0, 0, 0, 47, 93, 99, 105, 119, 122, 135, 147, 152, 170, 184, 195, 199, 201, 209, 218, 223, 229, 239, 249, 254, 260, 272, 283, 289, 295, 302, 311, 320, 334, 339, 367, 373, 381, 385, 387, 400, 404, 406, 419, 447, 451, 453, 459, 511, 513, 523, 527] \ No newline at end of file diff --git a/packages/cashc/src/grammar/CashScriptParser.ts b/packages/cashc/src/grammar/CashScriptParser.ts index c94f6b254..d08aa12af 100644 --- a/packages/cashc/src/grammar/CashScriptParser.ts +++ b/packages/cashc/src/grammar/CashScriptParser.ts @@ -127,27 +127,28 @@ export default class CashScriptParser extends Parser { public static readonly RULE_controlStatement = 20; public static readonly RULE_variableDefinition = 21; public static readonly RULE_tupleAssignment = 22; - public static readonly RULE_assignStatement = 23; - public static readonly RULE_timeOpStatement = 24; - public static readonly RULE_requireStatement = 25; - public static readonly RULE_consoleStatement = 26; - public static readonly RULE_ifStatement = 27; - public static readonly RULE_loopStatement = 28; - public static readonly RULE_doWhileStatement = 29; - public static readonly RULE_whileStatement = 30; - public static readonly RULE_forStatement = 31; - public static readonly RULE_forInit = 32; - public static readonly RULE_requireMessage = 33; - public static readonly RULE_consoleParameter = 34; - public static readonly RULE_consoleParameterList = 35; - public static readonly RULE_functionCall = 36; - public static readonly RULE_expressionList = 37; - public static readonly RULE_expression = 38; - public static readonly RULE_modifier = 39; - public static readonly RULE_literal = 40; - public static readonly RULE_numberLiteral = 41; - public static readonly RULE_typeName = 42; - public static readonly RULE_typeCast = 43; + public static readonly RULE_tupleTarget = 23; + public static readonly RULE_assignStatement = 24; + public static readonly RULE_timeOpStatement = 25; + public static readonly RULE_requireStatement = 26; + public static readonly RULE_consoleStatement = 27; + public static readonly RULE_ifStatement = 28; + public static readonly RULE_loopStatement = 29; + public static readonly RULE_doWhileStatement = 30; + public static readonly RULE_whileStatement = 31; + public static readonly RULE_forStatement = 32; + public static readonly RULE_forInit = 33; + public static readonly RULE_requireMessage = 34; + public static readonly RULE_consoleParameter = 35; + public static readonly RULE_consoleParameterList = 36; + public static readonly RULE_functionCall = 37; + public static readonly RULE_expressionList = 38; + public static readonly RULE_expression = 39; + public static readonly RULE_modifier = 40; + public static readonly RULE_literal = 41; + public static readonly RULE_numberLiteral = 42; + public static readonly RULE_typeName = 43; + public static readonly RULE_typeCast = 44; public static readonly literalNames: (string | null)[] = [ null, "'pragma'", "';'", "'cashscript'", "'^'", "'~'", @@ -254,11 +255,11 @@ export default class CashScriptParser extends Parser { "constantDefinition", "contractDefinition", "contractFunctionDefinition", "functionBody", "parameterList", "parameter", "block", "statement", "nonControlStatement", "functionCallStatement", "returnStatement", "controlStatement", "variableDefinition", - "tupleAssignment", "assignStatement", "timeOpStatement", "requireStatement", - "consoleStatement", "ifStatement", "loopStatement", "doWhileStatement", - "whileStatement", "forStatement", "forInit", "requireMessage", "consoleParameter", - "consoleParameterList", "functionCall", "expressionList", "expression", - "modifier", "literal", "numberLiteral", "typeName", "typeCast", + "tupleAssignment", "tupleTarget", "assignStatement", "timeOpStatement", + "requireStatement", "consoleStatement", "ifStatement", "loopStatement", + "doWhileStatement", "whileStatement", "forStatement", "forInit", "requireMessage", + "consoleParameter", "consoleParameterList", "functionCall", "expressionList", + "expression", "modifier", "literal", "numberLiteral", "typeName", "typeCast", ]; public get grammarFileName(): string { return "CashScript.g4"; } public get literalNames(): (string | null)[] { return CashScriptParser.literalNames; } @@ -282,49 +283,49 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 91; + this.state = 93; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===1) { { { - this.state = 88; + this.state = 90; this.pragmaDirective(); } } - this.state = 93; + this.state = 95; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 97; + this.state = 99; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===11) { { { - this.state = 94; + this.state = 96; this.importDirective(); } } - this.state = 99; + this.state = 101; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 103; + this.state = 105; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===12 || _la===18 || ((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 7) !== 0)) { { { - this.state = 100; + this.state = 102; this.topLevelDefinition(); } } - this.state = 105; + this.state = 107; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 106; + this.state = 108; this.match(CashScriptParser.EOF); } } @@ -349,13 +350,13 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 108; + this.state = 110; this.match(CashScriptParser.T__0); - this.state = 109; + this.state = 111; this.pragmaName(); - this.state = 110; + this.state = 112; this.pragmaValue(); - this.state = 111; + this.state = 113; this.match(CashScriptParser.T__1); } } @@ -380,7 +381,7 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 113; + this.state = 115; this.match(CashScriptParser.T__2); } } @@ -406,14 +407,14 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 115; - this.versionConstraint(); this.state = 117; + this.versionConstraint(); + this.state = 119; this._errHandler.sync(this); _la = this._input.LA(1); if ((((_la) & ~0x1F) === 0 && ((1 << _la) & 2032) !== 0) || _la===66) { { - this.state = 116; + this.state = 118; this.versionConstraint(); } } @@ -442,17 +443,17 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 120; + this.state = 122; this._errHandler.sync(this); _la = this._input.LA(1); if ((((_la) & ~0x1F) === 0 && ((1 << _la) & 2032) !== 0)) { { - this.state = 119; + this.state = 121; this.versionOperator(); } } - this.state = 122; + this.state = 124; this.match(CashScriptParser.VersionLiteral); } } @@ -478,7 +479,7 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 124; + this.state = 126; _la = this._input.LA(1); if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 2032) !== 0))) { this._errHandler.recoverInline(this); @@ -510,11 +511,11 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 126; + this.state = 128; this.match(CashScriptParser.T__10); - this.state = 127; + this.state = 129; this.match(CashScriptParser.StringLiteral); - this.state = 128; + this.state = 130; this.match(CashScriptParser.T__1); } } @@ -537,13 +538,13 @@ export default class CashScriptParser extends Parser { let localctx: TopLevelDefinitionContext = new TopLevelDefinitionContext(this, this._ctx, this.state); this.enterRule(localctx, 14, CashScriptParser.RULE_topLevelDefinition); try { - this.state = 133; + this.state = 135; this._errHandler.sync(this); switch (this._input.LA(1)) { case 12: this.enterOuterAlt(localctx, 1); { - this.state = 130; + this.state = 132; this.globalFunctionDefinition(); } break; @@ -552,14 +553,14 @@ export default class CashScriptParser extends Parser { case 74: this.enterOuterAlt(localctx, 2); { - this.state = 131; + this.state = 133; this.constantDefinition(); } break; case 18: this.enterOuterAlt(localctx, 3); { - this.state = 132; + this.state = 134; this.contractDefinition(); } break; @@ -589,45 +590,45 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 135; + this.state = 137; this.match(CashScriptParser.T__11); - this.state = 136; + this.state = 138; this.match(CashScriptParser.Identifier); - this.state = 137; + this.state = 139; this.parameterList(); - this.state = 150; + this.state = 152; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===13) { { - this.state = 138; + this.state = 140; this.match(CashScriptParser.T__12); - this.state = 139; + this.state = 141; this.match(CashScriptParser.T__13); - this.state = 140; + this.state = 142; this.typeName(); - this.state = 145; + this.state = 147; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===15) { { { - this.state = 141; + this.state = 143; this.match(CashScriptParser.T__14); - this.state = 142; + this.state = 144; this.typeName(); } } - this.state = 147; + this.state = 149; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 148; + this.state = 150; this.match(CashScriptParser.T__15); } } - this.state = 152; + this.state = 154; this.functionBody(); } } @@ -652,17 +653,17 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 154; + this.state = 156; this.typeName(); - this.state = 155; + this.state = 157; this.match(CashScriptParser.T__16); - this.state = 156; + this.state = 158; this.match(CashScriptParser.Identifier); - this.state = 157; + this.state = 159; this.match(CashScriptParser.T__9); - this.state = 158; + this.state = 160; this.expression(0); - this.state = 159; + this.state = 161; this.match(CashScriptParser.T__1); } } @@ -688,29 +689,29 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 161; + this.state = 163; this.match(CashScriptParser.T__17); - this.state = 162; + this.state = 164; this.match(CashScriptParser.Identifier); - this.state = 163; + this.state = 165; this.parameterList(); - this.state = 164; + this.state = 166; this.match(CashScriptParser.T__18); - this.state = 168; + this.state = 170; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===12) { { { - this.state = 165; + this.state = 167; this.contractFunctionDefinition(); } } - this.state = 170; + this.state = 172; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 171; + this.state = 173; this.match(CashScriptParser.T__19); } } @@ -735,13 +736,13 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 173; + this.state = 175; this.match(CashScriptParser.T__11); - this.state = 174; + this.state = 176; this.match(CashScriptParser.Identifier); - this.state = 175; + this.state = 177; this.parameterList(); - this.state = 176; + this.state = 178; this.functionBody(); } } @@ -767,23 +768,23 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 178; + this.state = 180; this.match(CashScriptParser.T__18); - this.state = 182; + this.state = 184; this._errHandler.sync(this); _la = this._input.LA(1); - while (((((_la - 21)) & ~0x1F) === 0 && ((1 << (_la - 21)) & 3809) !== 0) || ((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 1031) !== 0)) { + while (((((_la - 14)) & ~0x1F) === 0 && ((1 << (_la - 14)) & 487553) !== 0) || ((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 1031) !== 0)) { { { - this.state = 179; + this.state = 181; this.statement(); } } - this.state = 184; + this.state = 186; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 185; + this.state = 187; this.match(CashScriptParser.T__19); } } @@ -810,39 +811,39 @@ export default class CashScriptParser extends Parser { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 187; + this.state = 189; this.match(CashScriptParser.T__13); - this.state = 199; + this.state = 201; this._errHandler.sync(this); _la = this._input.LA(1); if (((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 7) !== 0)) { { - this.state = 188; + this.state = 190; this.parameter(); - this.state = 193; + this.state = 195; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 10, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 189; + this.state = 191; this.match(CashScriptParser.T__14); - this.state = 190; + this.state = 192; this.parameter(); } } } - this.state = 195; + this.state = 197; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 10, this._ctx); } - this.state = 197; + this.state = 199; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 196; + this.state = 198; this.match(CashScriptParser.T__14); } } @@ -850,7 +851,7 @@ export default class CashScriptParser extends Parser { } } - this.state = 201; + this.state = 203; this.match(CashScriptParser.T__15); } } @@ -876,23 +877,23 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 203; + this.state = 205; this.typeName(); - this.state = 207; + this.state = 209; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===17 || _la===65) { { { - this.state = 204; + this.state = 206; this.modifier(); } } - this.state = 209; + this.state = 211; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 210; + this.state = 212; this.match(CashScriptParser.Identifier); } } @@ -916,32 +917,33 @@ export default class CashScriptParser extends Parser { this.enterRule(localctx, 30, CashScriptParser.RULE_block); let _la: number; try { - this.state = 221; + this.state = 223; this._errHandler.sync(this); switch (this._input.LA(1)) { case 19: this.enterOuterAlt(localctx, 1); { - this.state = 212; + this.state = 214; this.match(CashScriptParser.T__18); - this.state = 216; + this.state = 218; this._errHandler.sync(this); _la = this._input.LA(1); - while (((((_la - 21)) & ~0x1F) === 0 && ((1 << (_la - 21)) & 3809) !== 0) || ((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 1031) !== 0)) { + while (((((_la - 14)) & ~0x1F) === 0 && ((1 << (_la - 14)) & 487553) !== 0) || ((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 1031) !== 0)) { { { - this.state = 213; + this.state = 215; this.statement(); } } - this.state = 218; + this.state = 220; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 219; + this.state = 221; this.match(CashScriptParser.T__19); } break; + case 14: case 21: case 26: case 27: @@ -955,7 +957,7 @@ export default class CashScriptParser extends Parser { case 82: this.enterOuterAlt(localctx, 2); { - this.state = 220; + this.state = 222; this.statement(); } break; @@ -982,7 +984,7 @@ export default class CashScriptParser extends Parser { let localctx: StatementContext = new StatementContext(this, this._ctx, this.state); this.enterRule(localctx, 32, CashScriptParser.RULE_statement); try { - this.state = 227; + this.state = 229; this._errHandler.sync(this); switch (this._input.LA(1)) { case 28: @@ -991,10 +993,11 @@ export default class CashScriptParser extends Parser { case 32: this.enterOuterAlt(localctx, 1); { - this.state = 223; + this.state = 225; this.controlStatement(); } break; + case 14: case 21: case 26: case 27: @@ -1004,9 +1007,9 @@ export default class CashScriptParser extends Parser { case 82: this.enterOuterAlt(localctx, 2); { - this.state = 224; + this.state = 226; this.nonControlStatement(); - this.state = 225; + this.state = 227; this.match(CashScriptParser.T__1); } break; @@ -1033,62 +1036,62 @@ export default class CashScriptParser extends Parser { let localctx: NonControlStatementContext = new NonControlStatementContext(this, this._ctx, this.state); this.enterRule(localctx, 34, CashScriptParser.RULE_nonControlStatement); try { - this.state = 237; + this.state = 239; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 17, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 229; + this.state = 231; this.variableDefinition(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 230; + this.state = 232; this.tupleAssignment(); } break; case 3: this.enterOuterAlt(localctx, 3); { - this.state = 231; + this.state = 233; this.assignStatement(); } break; case 4: this.enterOuterAlt(localctx, 4); { - this.state = 232; + this.state = 234; this.timeOpStatement(); } break; case 5: this.enterOuterAlt(localctx, 5); { - this.state = 233; + this.state = 235; this.requireStatement(); } break; case 6: this.enterOuterAlt(localctx, 6); { - this.state = 234; + this.state = 236; this.functionCallStatement(); } break; case 7: this.enterOuterAlt(localctx, 7); { - this.state = 235; + this.state = 237; this.consoleStatement(); } break; case 8: this.enterOuterAlt(localctx, 8); { - this.state = 236; + this.state = 238; this.returnStatement(); } break; @@ -1115,7 +1118,7 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 239; + this.state = 241; this.functionCall(); } } @@ -1141,23 +1144,23 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 241; + this.state = 243; this.match(CashScriptParser.T__20); - this.state = 242; + this.state = 244; this.expression(0); - this.state = 247; + this.state = 249; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===15) { { { - this.state = 243; + this.state = 245; this.match(CashScriptParser.T__14); - this.state = 244; + this.state = 246; this.expression(0); } } - this.state = 249; + this.state = 251; this._errHandler.sync(this); _la = this._input.LA(1); } @@ -1182,13 +1185,13 @@ export default class CashScriptParser extends Parser { let localctx: ControlStatementContext = new ControlStatementContext(this, this._ctx, this.state); this.enterRule(localctx, 40, CashScriptParser.RULE_controlStatement); try { - this.state = 252; + this.state = 254; this._errHandler.sync(this); switch (this._input.LA(1)) { case 28: this.enterOuterAlt(localctx, 1); { - this.state = 250; + this.state = 252; this.ifStatement(); } break; @@ -1197,7 +1200,7 @@ export default class CashScriptParser extends Parser { case 32: this.enterOuterAlt(localctx, 2); { - this.state = 251; + this.state = 253; this.loopStatement(); } break; @@ -1227,27 +1230,27 @@ export default class CashScriptParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 254; + this.state = 256; this.typeName(); - this.state = 258; + this.state = 260; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===17 || _la===65) { { { - this.state = 255; + this.state = 257; this.modifier(); } } - this.state = 260; + this.state = 262; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 261; + this.state = 263; this.match(CashScriptParser.Identifier); - this.state = 262; + this.state = 264; this.match(CashScriptParser.T__9); - this.state = 263; + this.state = 265; this.expression(0); } } @@ -1271,34 +1274,116 @@ export default class CashScriptParser extends Parser { this.enterRule(localctx, 44, CashScriptParser.RULE_tupleAssignment); let _la: number; try { - this.enterOuterAlt(localctx, 1); - { - this.state = 265; - this.typeName(); - this.state = 266; - this.match(CashScriptParser.Identifier); - this.state = 271; + this.state = 289; this._errHandler.sync(this); - _la = this._input.LA(1); - do { - { + switch (this._input.LA(1)) { + case 72: + case 73: + case 74: + case 82: + this.enterOuterAlt(localctx, 1); { this.state = 267; - this.match(CashScriptParser.T__14); - this.state = 268; + this.tupleTarget(); + this.state = 270; + this._errHandler.sync(this); + _la = this._input.LA(1); + do { + { + { + this.state = 268; + this.match(CashScriptParser.T__14); + this.state = 269; + this.tupleTarget(); + } + } + this.state = 272; + this._errHandler.sync(this); + _la = this._input.LA(1); + } while (_la===15); + this.state = 274; + this.match(CashScriptParser.T__9); + this.state = 275; + this.expression(0); + } + break; + case 14: + this.enterOuterAlt(localctx, 2); + { + this.state = 277; + this.match(CashScriptParser.T__13); + this.state = 278; + this.tupleTarget(); + this.state = 281; + this._errHandler.sync(this); + _la = this._input.LA(1); + do { + { + { + this.state = 279; + this.match(CashScriptParser.T__14); + this.state = 280; + this.tupleTarget(); + } + } + this.state = 283; + this._errHandler.sync(this); + _la = this._input.LA(1); + } while (_la===15); + this.state = 285; + this.match(CashScriptParser.T__15); + this.state = 286; + this.match(CashScriptParser.T__9); + this.state = 287; + this.expression(0); + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public tupleTarget(): TupleTargetContext { + let localctx: TupleTargetContext = new TupleTargetContext(this, this._ctx, this.state); + this.enterRule(localctx, 46, CashScriptParser.RULE_tupleTarget); + try { + this.state = 295; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case 72: + case 73: + case 74: + this.enterOuterAlt(localctx, 1); + { + this.state = 291; this.typeName(); - this.state = 269; + this.state = 292; this.match(CashScriptParser.Identifier); } + break; + case 82: + this.enterOuterAlt(localctx, 2); + { + this.state = 294; + this.match(CashScriptParser.Identifier); } - this.state = 273; - this._errHandler.sync(this); - _la = this._input.LA(1); - } while (_la===15); - this.state = 275; - this.match(CashScriptParser.T__9); - this.state = 276; - this.expression(0); + break; + default: + throw new NoViableAltException(this); } } catch (re) { @@ -1318,18 +1403,18 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public assignStatement(): AssignStatementContext { let localctx: AssignStatementContext = new AssignStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 46, CashScriptParser.RULE_assignStatement); + this.enterRule(localctx, 48, CashScriptParser.RULE_assignStatement); let _la: number; try { - this.state = 283; + this.state = 302; this._errHandler.sync(this); - switch ( this._interp.adaptivePredict(this._input, 22, this._ctx) ) { + switch ( this._interp.adaptivePredict(this._input, 25, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 278; + this.state = 297; this.match(CashScriptParser.Identifier); - this.state = 279; + this.state = 298; localctx._op = this._input.LT(1); _la = this._input.LA(1); if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 12583936) !== 0))) { @@ -1339,16 +1424,16 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 280; + this.state = 299; this.expression(0); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 281; + this.state = 300; this.match(CashScriptParser.Identifier); - this.state = 282; + this.state = 301; localctx._op = this._input.LT(1); _la = this._input.LA(1); if(!(_la===24 || _la===25)) { @@ -1379,34 +1464,34 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public timeOpStatement(): TimeOpStatementContext { let localctx: TimeOpStatementContext = new TimeOpStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 48, CashScriptParser.RULE_timeOpStatement); + this.enterRule(localctx, 50, CashScriptParser.RULE_timeOpStatement); let _la: number; try { this.enterOuterAlt(localctx, 1); { - this.state = 285; + this.state = 304; this.match(CashScriptParser.T__25); - this.state = 286; + this.state = 305; this.match(CashScriptParser.T__13); - this.state = 287; + this.state = 306; this.match(CashScriptParser.TxVar); - this.state = 288; + this.state = 307; this.match(CashScriptParser.T__5); - this.state = 289; + this.state = 308; this.expression(0); - this.state = 292; + this.state = 311; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 290; + this.state = 309; this.match(CashScriptParser.T__14); - this.state = 291; + this.state = 310; this.requireMessage(); } } - this.state = 294; + this.state = 313; this.match(CashScriptParser.T__15); } } @@ -1427,30 +1512,30 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public requireStatement(): RequireStatementContext { let localctx: RequireStatementContext = new RequireStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 50, CashScriptParser.RULE_requireStatement); + this.enterRule(localctx, 52, CashScriptParser.RULE_requireStatement); let _la: number; try { this.enterOuterAlt(localctx, 1); { - this.state = 296; + this.state = 315; this.match(CashScriptParser.T__25); - this.state = 297; + this.state = 316; this.match(CashScriptParser.T__13); - this.state = 298; + this.state = 317; this.expression(0); - this.state = 301; + this.state = 320; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 299; + this.state = 318; this.match(CashScriptParser.T__14); - this.state = 300; + this.state = 319; this.requireMessage(); } } - this.state = 303; + this.state = 322; this.match(CashScriptParser.T__15); } } @@ -1471,13 +1556,13 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public consoleStatement(): ConsoleStatementContext { let localctx: ConsoleStatementContext = new ConsoleStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 52, CashScriptParser.RULE_consoleStatement); + this.enterRule(localctx, 54, CashScriptParser.RULE_consoleStatement); try { this.enterOuterAlt(localctx, 1); { - this.state = 305; + this.state = 324; this.match(CashScriptParser.T__26); - this.state = 306; + this.state = 325; this.consoleParameterList(); } } @@ -1498,28 +1583,28 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public ifStatement(): IfStatementContext { let localctx: IfStatementContext = new IfStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 54, CashScriptParser.RULE_ifStatement); + this.enterRule(localctx, 56, CashScriptParser.RULE_ifStatement); try { this.enterOuterAlt(localctx, 1); { - this.state = 308; + this.state = 327; this.match(CashScriptParser.T__27); - this.state = 309; + this.state = 328; this.match(CashScriptParser.T__13); - this.state = 310; + this.state = 329; this.expression(0); - this.state = 311; + this.state = 330; this.match(CashScriptParser.T__15); - this.state = 312; + this.state = 331; localctx._ifBlock = this.block(); - this.state = 315; + this.state = 334; this._errHandler.sync(this); - switch ( this._interp.adaptivePredict(this._input, 25, this._ctx) ) { + switch ( this._interp.adaptivePredict(this._input, 28, this._ctx) ) { case 1: { - this.state = 313; + this.state = 332; this.match(CashScriptParser.T__28); - this.state = 314; + this.state = 333; localctx._elseBlock = this.block(); } break; @@ -1543,29 +1628,29 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public loopStatement(): LoopStatementContext { let localctx: LoopStatementContext = new LoopStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 56, CashScriptParser.RULE_loopStatement); + this.enterRule(localctx, 58, CashScriptParser.RULE_loopStatement); try { - this.state = 320; + this.state = 339; this._errHandler.sync(this); switch (this._input.LA(1)) { case 30: this.enterOuterAlt(localctx, 1); { - this.state = 317; + this.state = 336; this.doWhileStatement(); } break; case 31: this.enterOuterAlt(localctx, 2); { - this.state = 318; + this.state = 337; this.whileStatement(); } break; case 32: this.enterOuterAlt(localctx, 3); { - this.state = 319; + this.state = 338; this.forStatement(); } break; @@ -1590,23 +1675,23 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public doWhileStatement(): DoWhileStatementContext { let localctx: DoWhileStatementContext = new DoWhileStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 58, CashScriptParser.RULE_doWhileStatement); + this.enterRule(localctx, 60, CashScriptParser.RULE_doWhileStatement); try { this.enterOuterAlt(localctx, 1); { - this.state = 322; + this.state = 341; this.match(CashScriptParser.T__29); - this.state = 323; + this.state = 342; this.block(); - this.state = 324; + this.state = 343; this.match(CashScriptParser.T__30); - this.state = 325; + this.state = 344; this.match(CashScriptParser.T__13); - this.state = 326; + this.state = 345; this.expression(0); - this.state = 327; + this.state = 346; this.match(CashScriptParser.T__15); - this.state = 328; + this.state = 347; this.match(CashScriptParser.T__1); } } @@ -1627,19 +1712,19 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public whileStatement(): WhileStatementContext { let localctx: WhileStatementContext = new WhileStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 60, CashScriptParser.RULE_whileStatement); + this.enterRule(localctx, 62, CashScriptParser.RULE_whileStatement); try { this.enterOuterAlt(localctx, 1); { - this.state = 330; + this.state = 349; this.match(CashScriptParser.T__30); - this.state = 331; + this.state = 350; this.match(CashScriptParser.T__13); - this.state = 332; + this.state = 351; this.expression(0); - this.state = 333; + this.state = 352; this.match(CashScriptParser.T__15); - this.state = 334; + this.state = 353; this.block(); } } @@ -1660,27 +1745,27 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public forStatement(): ForStatementContext { let localctx: ForStatementContext = new ForStatementContext(this, this._ctx, this.state); - this.enterRule(localctx, 62, CashScriptParser.RULE_forStatement); + this.enterRule(localctx, 64, CashScriptParser.RULE_forStatement); try { this.enterOuterAlt(localctx, 1); { - this.state = 336; + this.state = 355; this.match(CashScriptParser.T__31); - this.state = 337; + this.state = 356; this.match(CashScriptParser.T__13); - this.state = 338; + this.state = 357; this.forInit(); - this.state = 339; + this.state = 358; this.match(CashScriptParser.T__1); - this.state = 340; + this.state = 359; this.expression(0); - this.state = 341; + this.state = 360; this.match(CashScriptParser.T__1); - this.state = 342; + this.state = 361; this.assignStatement(); - this.state = 343; + this.state = 362; this.match(CashScriptParser.T__15); - this.state = 344; + this.state = 363; this.block(); } } @@ -1701,9 +1786,9 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public forInit(): ForInitContext { let localctx: ForInitContext = new ForInitContext(this, this._ctx, this.state); - this.enterRule(localctx, 64, CashScriptParser.RULE_forInit); + this.enterRule(localctx, 66, CashScriptParser.RULE_forInit); try { - this.state = 348; + this.state = 367; this._errHandler.sync(this); switch (this._input.LA(1)) { case 72: @@ -1711,14 +1796,14 @@ export default class CashScriptParser extends Parser { case 74: this.enterOuterAlt(localctx, 1); { - this.state = 346; + this.state = 365; this.variableDefinition(); } break; case 82: this.enterOuterAlt(localctx, 2); { - this.state = 347; + this.state = 366; this.assignStatement(); } break; @@ -1743,11 +1828,11 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public requireMessage(): RequireMessageContext { let localctx: RequireMessageContext = new RequireMessageContext(this, this._ctx, this.state); - this.enterRule(localctx, 66, CashScriptParser.RULE_requireMessage); + this.enterRule(localctx, 68, CashScriptParser.RULE_requireMessage); try { this.enterOuterAlt(localctx, 1); { - this.state = 350; + this.state = 369; this.match(CashScriptParser.StringLiteral); } } @@ -1768,15 +1853,15 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public consoleParameter(): ConsoleParameterContext { let localctx: ConsoleParameterContext = new ConsoleParameterContext(this, this._ctx, this.state); - this.enterRule(localctx, 68, CashScriptParser.RULE_consoleParameter); + this.enterRule(localctx, 70, CashScriptParser.RULE_consoleParameter); try { - this.state = 354; + this.state = 373; this._errHandler.sync(this); switch (this._input.LA(1)) { case 82: this.enterOuterAlt(localctx, 1); { - this.state = 352; + this.state = 371; this.match(CashScriptParser.Identifier); } break; @@ -1787,7 +1872,7 @@ export default class CashScriptParser extends Parser { case 78: this.enterOuterAlt(localctx, 2); { - this.state = 353; + this.state = 372; this.literal(); } break; @@ -1812,45 +1897,45 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public consoleParameterList(): ConsoleParameterListContext { let localctx: ConsoleParameterListContext = new ConsoleParameterListContext(this, this._ctx, this.state); - this.enterRule(localctx, 70, CashScriptParser.RULE_consoleParameterList); + this.enterRule(localctx, 72, CashScriptParser.RULE_consoleParameterList); let _la: number; try { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 356; + this.state = 375; this.match(CashScriptParser.T__13); - this.state = 368; + this.state = 387; this._errHandler.sync(this); _la = this._input.LA(1); if (((((_la - 67)) & ~0x1F) === 0 && ((1 << (_la - 67)) & 36357) !== 0)) { { - this.state = 357; + this.state = 376; this.consoleParameter(); - this.state = 362; + this.state = 381; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 29, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 32, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 358; + this.state = 377; this.match(CashScriptParser.T__14); - this.state = 359; + this.state = 378; this.consoleParameter(); } } } - this.state = 364; + this.state = 383; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 29, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 32, this._ctx); } - this.state = 366; + this.state = 385; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 365; + this.state = 384; this.match(CashScriptParser.T__14); } } @@ -1858,7 +1943,7 @@ export default class CashScriptParser extends Parser { } } - this.state = 370; + this.state = 389; this.match(CashScriptParser.T__15); } } @@ -1879,13 +1964,13 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public functionCall(): FunctionCallContext { let localctx: FunctionCallContext = new FunctionCallContext(this, this._ctx, this.state); - this.enterRule(localctx, 72, CashScriptParser.RULE_functionCall); + this.enterRule(localctx, 74, CashScriptParser.RULE_functionCall); try { this.enterOuterAlt(localctx, 1); { - this.state = 372; + this.state = 391; this.match(CashScriptParser.Identifier); - this.state = 373; + this.state = 392; this.expressionList(); } } @@ -1906,45 +1991,45 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public expressionList(): ExpressionListContext { let localctx: ExpressionListContext = new ExpressionListContext(this, this._ctx, this.state); - this.enterRule(localctx, 74, CashScriptParser.RULE_expressionList); + this.enterRule(localctx, 76, CashScriptParser.RULE_expressionList); let _la: number; try { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 375; + this.state = 394; this.match(CashScriptParser.T__13); - this.state = 387; + this.state = 406; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===5 || _la===14 || ((((_la - 33)) & ~0x1F) === 0 && ((1 << (_la - 33)) & 786955) !== 0) || ((((_la - 67)) & ~0x1F) === 0 && ((1 << (_la - 67)) & 61029) !== 0)) { { - this.state = 376; + this.state = 395; this.expression(0); - this.state = 381; + this.state = 400; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 32, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 35, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 377; + this.state = 396; this.match(CashScriptParser.T__14); - this.state = 378; + this.state = 397; this.expression(0); } } } - this.state = 383; + this.state = 402; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 32, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 35, this._ctx); } - this.state = 385; + this.state = 404; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 384; + this.state = 403; this.match(CashScriptParser.T__14); } } @@ -1952,7 +2037,7 @@ export default class CashScriptParser extends Parser { } } - this.state = 389; + this.state = 408; this.match(CashScriptParser.T__15); } } @@ -1983,27 +2068,27 @@ export default class CashScriptParser extends Parser { let _parentState: number = this.state; let localctx: ExpressionContext = new ExpressionContext(this, this._ctx, _parentState); let _prevctx: ExpressionContext = localctx; - let _startState: number = 76; - this.enterRecursionRule(localctx, 76, CashScriptParser.RULE_expression, _p); + let _startState: number = 78; + this.enterRecursionRule(localctx, 78, CashScriptParser.RULE_expression, _p); let _la: number; try { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 440; + this.state = 459; this._errHandler.sync(this); - switch ( this._interp.adaptivePredict(this._input, 39, this._ctx) ) { + switch ( this._interp.adaptivePredict(this._input, 42, this._ctx) ) { case 1: { localctx = new ParenthesisedContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 392; + this.state = 411; this.match(CashScriptParser.T__13); - this.state = 393; + this.state = 412; this.expression(0); - this.state = 394; + this.state = 413; this.match(CashScriptParser.T__15); } break; @@ -2012,23 +2097,23 @@ export default class CashScriptParser extends Parser { localctx = new CastContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 396; + this.state = 415; this.typeCast(); - this.state = 397; + this.state = 416; this.match(CashScriptParser.T__13); - this.state = 398; + this.state = 417; (localctx as CastContext)._castable = this.expression(0); - this.state = 400; + this.state = 419; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 399; + this.state = 418; this.match(CashScriptParser.T__14); } } - this.state = 402; + this.state = 421; this.match(CashScriptParser.T__15); } break; @@ -2037,7 +2122,7 @@ export default class CashScriptParser extends Parser { localctx = new FunctionCallExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 404; + this.state = 423; this.functionCall(); } break; @@ -2046,11 +2131,11 @@ export default class CashScriptParser extends Parser { localctx = new InstantiationContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 405; + this.state = 424; this.match(CashScriptParser.T__32); - this.state = 406; + this.state = 425; this.match(CashScriptParser.Identifier); - this.state = 407; + this.state = 426; this.expressionList(); } break; @@ -2059,15 +2144,15 @@ export default class CashScriptParser extends Parser { localctx = new UnaryIntrospectionOpContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 408; + this.state = 427; (localctx as UnaryIntrospectionOpContext)._scope = this.match(CashScriptParser.T__35); - this.state = 409; + this.state = 428; this.match(CashScriptParser.T__33); - this.state = 410; + this.state = 429; this.expression(0); - this.state = 411; + this.state = 430; this.match(CashScriptParser.T__34); - this.state = 412; + this.state = 431; (localctx as UnaryIntrospectionOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(((((_la - 37)) & ~0x1F) === 0 && ((1 << (_la - 37)) & 31) !== 0))) { @@ -2084,15 +2169,15 @@ export default class CashScriptParser extends Parser { localctx = new UnaryIntrospectionOpContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 414; + this.state = 433; (localctx as UnaryIntrospectionOpContext)._scope = this.match(CashScriptParser.T__41); - this.state = 415; + this.state = 434; this.match(CashScriptParser.T__33); - this.state = 416; + this.state = 435; this.expression(0); - this.state = 417; + this.state = 436; this.match(CashScriptParser.T__34); - this.state = 418; + this.state = 437; (localctx as UnaryIntrospectionOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(((((_la - 37)) & ~0x1F) === 0 && ((1 << (_la - 37)) & 991) !== 0))) { @@ -2109,7 +2194,7 @@ export default class CashScriptParser extends Parser { localctx = new UnaryOpContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 420; + this.state = 439; (localctx as UnaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(_la===5 || _la===51 || _la===52)) { @@ -2119,7 +2204,7 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 421; + this.state = 440; this.expression(15); } break; @@ -2128,39 +2213,39 @@ export default class CashScriptParser extends Parser { localctx = new ArrayContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 422; + this.state = 441; this.match(CashScriptParser.T__33); - this.state = 434; + this.state = 453; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===5 || _la===14 || ((((_la - 33)) & ~0x1F) === 0 && ((1 << (_la - 33)) & 786955) !== 0) || ((((_la - 67)) & ~0x1F) === 0 && ((1 << (_la - 67)) & 61029) !== 0)) { { - this.state = 423; + this.state = 442; this.expression(0); - this.state = 428; + this.state = 447; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 36, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 39, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 424; + this.state = 443; this.match(CashScriptParser.T__14); - this.state = 425; + this.state = 444; this.expression(0); } } } - this.state = 430; + this.state = 449; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 36, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 39, this._ctx); } - this.state = 432; + this.state = 451; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===15) { { - this.state = 431; + this.state = 450; this.match(CashScriptParser.T__14); } } @@ -2168,7 +2253,7 @@ export default class CashScriptParser extends Parser { } } - this.state = 436; + this.state = 455; this.match(CashScriptParser.T__34); } break; @@ -2177,7 +2262,7 @@ export default class CashScriptParser extends Parser { localctx = new NullaryOpContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 437; + this.state = 456; this.match(CashScriptParser.NullaryOp); } break; @@ -2186,7 +2271,7 @@ export default class CashScriptParser extends Parser { localctx = new IdentifierContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 438; + this.state = 457; this.match(CashScriptParser.Identifier); } break; @@ -2195,15 +2280,15 @@ export default class CashScriptParser extends Parser { localctx = new LiteralExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 439; + this.state = 458; this.literal(); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 494; + this.state = 513; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 41, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 44, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { if (this._parseListeners != null) { @@ -2211,19 +2296,19 @@ export default class CashScriptParser extends Parser { } _prevctx = localctx; { - this.state = 492; + this.state = 511; this._errHandler.sync(this); - switch ( this._interp.adaptivePredict(this._input, 40, this._ctx) ) { + switch ( this._interp.adaptivePredict(this._input, 43, this._ctx) ) { case 1: { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 442; + this.state = 461; if (!(this.precpred(this._ctx, 14))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 14)"); } - this.state = 443; + this.state = 462; (localctx as BinaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(((((_la - 53)) & ~0x1F) === 0 && ((1 << (_la - 53)) & 7) !== 0))) { @@ -2233,7 +2318,7 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 444; + this.state = 463; (localctx as BinaryOpContext)._right = this.expression(15); } break; @@ -2242,11 +2327,11 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 445; + this.state = 464; if (!(this.precpred(this._ctx, 13))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 13)"); } - this.state = 446; + this.state = 465; (localctx as BinaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(_la===52 || _la===56)) { @@ -2256,7 +2341,7 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 447; + this.state = 466; (localctx as BinaryOpContext)._right = this.expression(14); } break; @@ -2265,11 +2350,11 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 448; + this.state = 467; if (!(this.precpred(this._ctx, 12))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 12)"); } - this.state = 449; + this.state = 468; (localctx as BinaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(_la===57 || _la===58)) { @@ -2279,7 +2364,7 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 450; + this.state = 469; (localctx as BinaryOpContext)._right = this.expression(13); } break; @@ -2288,11 +2373,11 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 451; + this.state = 470; if (!(this.precpred(this._ctx, 11))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 11)"); } - this.state = 452; + this.state = 471; (localctx as BinaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 960) !== 0))) { @@ -2302,7 +2387,7 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 453; + this.state = 472; (localctx as BinaryOpContext)._right = this.expression(12); } break; @@ -2311,11 +2396,11 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 454; + this.state = 473; if (!(this.precpred(this._ctx, 10))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 10)"); } - this.state = 455; + this.state = 474; (localctx as BinaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(_la===59 || _la===60)) { @@ -2325,7 +2410,7 @@ export default class CashScriptParser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 456; + this.state = 475; (localctx as BinaryOpContext)._right = this.expression(11); } break; @@ -2334,13 +2419,13 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 457; + this.state = 476; if (!(this.precpred(this._ctx, 9))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 9)"); } - this.state = 458; + this.state = 477; (localctx as BinaryOpContext)._op = this.match(CashScriptParser.T__60); - this.state = 459; + this.state = 478; (localctx as BinaryOpContext)._right = this.expression(10); } break; @@ -2349,13 +2434,13 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 460; + this.state = 479; if (!(this.precpred(this._ctx, 8))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 8)"); } - this.state = 461; + this.state = 480; (localctx as BinaryOpContext)._op = this.match(CashScriptParser.T__3); - this.state = 462; + this.state = 481; (localctx as BinaryOpContext)._right = this.expression(9); } break; @@ -2364,13 +2449,13 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 463; + this.state = 482; if (!(this.precpred(this._ctx, 7))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 7)"); } - this.state = 464; + this.state = 483; (localctx as BinaryOpContext)._op = this.match(CashScriptParser.T__61); - this.state = 465; + this.state = 484; (localctx as BinaryOpContext)._right = this.expression(8); } break; @@ -2379,13 +2464,13 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 466; + this.state = 485; if (!(this.precpred(this._ctx, 6))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 6)"); } - this.state = 467; + this.state = 486; (localctx as BinaryOpContext)._op = this.match(CashScriptParser.T__62); - this.state = 468; + this.state = 487; (localctx as BinaryOpContext)._right = this.expression(7); } break; @@ -2394,13 +2479,13 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 469; + this.state = 488; if (!(this.precpred(this._ctx, 5))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 5)"); } - this.state = 470; + this.state = 489; (localctx as BinaryOpContext)._op = this.match(CashScriptParser.T__63); - this.state = 471; + this.state = 490; (localctx as BinaryOpContext)._right = this.expression(6); } break; @@ -2408,15 +2493,15 @@ export default class CashScriptParser extends Parser { { localctx = new TupleIndexOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 472; + this.state = 491; if (!(this.precpred(this._ctx, 21))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 21)"); } - this.state = 473; + this.state = 492; this.match(CashScriptParser.T__33); - this.state = 474; + this.state = 493; (localctx as TupleIndexOpContext)._index = this.match(CashScriptParser.NumberLiteral); - this.state = 475; + this.state = 494; this.match(CashScriptParser.T__34); } break; @@ -2424,11 +2509,11 @@ export default class CashScriptParser extends Parser { { localctx = new UnaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 476; + this.state = 495; if (!(this.precpred(this._ctx, 18))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 18)"); } - this.state = 477; + this.state = 496; (localctx as UnaryOpContext)._op = this._input.LT(1); _la = this._input.LA(1); if(!(_la===47 || _la===48)) { @@ -2445,17 +2530,17 @@ export default class CashScriptParser extends Parser { localctx = new BinaryOpContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as BinaryOpContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 478; + this.state = 497; if (!(this.precpred(this._ctx, 17))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 17)"); } - this.state = 479; + this.state = 498; (localctx as BinaryOpContext)._op = this.match(CashScriptParser.T__48); - this.state = 480; + this.state = 499; this.match(CashScriptParser.T__13); - this.state = 481; + this.state = 500; (localctx as BinaryOpContext)._right = this.expression(0); - this.state = 482; + this.state = 501; this.match(CashScriptParser.T__15); } break; @@ -2464,30 +2549,30 @@ export default class CashScriptParser extends Parser { localctx = new SliceContext(this, new ExpressionContext(this, _parentctx, _parentState)); (localctx as SliceContext)._element = _prevctx; this.pushNewRecursionContext(localctx, _startState, CashScriptParser.RULE_expression); - this.state = 484; + this.state = 503; if (!(this.precpred(this._ctx, 16))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 16)"); } - this.state = 485; + this.state = 504; this.match(CashScriptParser.T__49); - this.state = 486; + this.state = 505; this.match(CashScriptParser.T__13); - this.state = 487; + this.state = 506; (localctx as SliceContext)._start = this.expression(0); - this.state = 488; + this.state = 507; this.match(CashScriptParser.T__14); - this.state = 489; + this.state = 508; (localctx as SliceContext)._end = this.expression(0); - this.state = 490; + this.state = 509; this.match(CashScriptParser.T__15); } break; } } } - this.state = 496; + this.state = 515; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 41, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 44, this._ctx); } } } @@ -2508,12 +2593,12 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public modifier(): ModifierContext { let localctx: ModifierContext = new ModifierContext(this, this._ctx, this.state); - this.enterRule(localctx, 78, CashScriptParser.RULE_modifier); + this.enterRule(localctx, 80, CashScriptParser.RULE_modifier); let _la: number; try { this.enterOuterAlt(localctx, 1); { - this.state = 497; + this.state = 516; _la = this._input.LA(1); if(!(_la===17 || _la===65)) { this._errHandler.recoverInline(this); @@ -2541,43 +2626,43 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public literal(): LiteralContext { let localctx: LiteralContext = new LiteralContext(this, this._ctx, this.state); - this.enterRule(localctx, 80, CashScriptParser.RULE_literal); + this.enterRule(localctx, 82, CashScriptParser.RULE_literal); try { - this.state = 504; + this.state = 523; this._errHandler.sync(this); switch (this._input.LA(1)) { case 67: this.enterOuterAlt(localctx, 1); { - this.state = 499; + this.state = 518; this.match(CashScriptParser.BooleanLiteral); } break; case 69: this.enterOuterAlt(localctx, 2); { - this.state = 500; + this.state = 519; this.numberLiteral(); } break; case 76: this.enterOuterAlt(localctx, 3); { - this.state = 501; + this.state = 520; this.match(CashScriptParser.StringLiteral); } break; case 77: this.enterOuterAlt(localctx, 4); { - this.state = 502; + this.state = 521; this.match(CashScriptParser.DateLiteral); } break; case 78: this.enterOuterAlt(localctx, 5); { - this.state = 503; + this.state = 522; this.match(CashScriptParser.HexLiteral); } break; @@ -2602,18 +2687,18 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public numberLiteral(): NumberLiteralContext { let localctx: NumberLiteralContext = new NumberLiteralContext(this, this._ctx, this.state); - this.enterRule(localctx, 82, CashScriptParser.RULE_numberLiteral); + this.enterRule(localctx, 84, CashScriptParser.RULE_numberLiteral); try { this.enterOuterAlt(localctx, 1); { - this.state = 506; + this.state = 525; this.match(CashScriptParser.NumberLiteral); - this.state = 508; + this.state = 527; this._errHandler.sync(this); - switch ( this._interp.adaptivePredict(this._input, 43, this._ctx) ) { + switch ( this._interp.adaptivePredict(this._input, 46, this._ctx) ) { case 1: { - this.state = 507; + this.state = 526; this.match(CashScriptParser.NumberUnit); } break; @@ -2637,12 +2722,12 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public typeName(): TypeNameContext { let localctx: TypeNameContext = new TypeNameContext(this, this._ctx, this.state); - this.enterRule(localctx, 84, CashScriptParser.RULE_typeName); + this.enterRule(localctx, 86, CashScriptParser.RULE_typeName); let _la: number; try { this.enterOuterAlt(localctx, 1); { - this.state = 510; + this.state = 529; _la = this._input.LA(1); if(!(((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 7) !== 0))) { this._errHandler.recoverInline(this); @@ -2670,12 +2755,12 @@ export default class CashScriptParser extends Parser { // @RuleVersion(0) public typeCast(): TypeCastContext { let localctx: TypeCastContext = new TypeCastContext(this, this._ctx, this.state); - this.enterRule(localctx, 86, CashScriptParser.RULE_typeCast); + this.enterRule(localctx, 88, CashScriptParser.RULE_typeCast); let _la: number; try { this.enterOuterAlt(localctx, 1); { - this.state = 512; + this.state = 531; _la = this._input.LA(1); if(!(((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & 259) !== 0))) { this._errHandler.recoverInline(this); @@ -2703,7 +2788,7 @@ export default class CashScriptParser extends Parser { public sempred(localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { switch (ruleIndex) { - case 38: + case 39: return this.expression_sempred(localctx as ExpressionContext, predIndex); } return true; @@ -2742,177 +2827,183 @@ export default class CashScriptParser extends Parser { return true; } - public static readonly _serializedATN: number[] = [4,1,85,515,2,0,7,0,2, + public static readonly _serializedATN: number[] = [4,1,85,534,2,0,7,0,2, 1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2, 10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17, 7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7, 24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31, 2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2, - 39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,1,0,5,0,90,8,0,10,0,12, - 0,93,9,0,1,0,5,0,96,8,0,10,0,12,0,99,9,0,1,0,5,0,102,8,0,10,0,12,0,105, - 9,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,3,1,3,3,3,118,8,3,1,4,3,4,121, - 8,4,1,4,1,4,1,5,1,5,1,6,1,6,1,6,1,6,1,7,1,7,1,7,3,7,134,8,7,1,8,1,8,1,8, - 1,8,1,8,1,8,1,8,1,8,5,8,144,8,8,10,8,12,8,147,9,8,1,8,1,8,3,8,151,8,8,1, - 8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,5,10,167,8,10, - 10,10,12,10,170,9,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,12,1,12,5,12, - 181,8,12,10,12,12,12,184,9,12,1,12,1,12,1,13,1,13,1,13,1,13,5,13,192,8, - 13,10,13,12,13,195,9,13,1,13,3,13,198,8,13,3,13,200,8,13,1,13,1,13,1,14, - 1,14,5,14,206,8,14,10,14,12,14,209,9,14,1,14,1,14,1,15,1,15,5,15,215,8, - 15,10,15,12,15,218,9,15,1,15,1,15,3,15,222,8,15,1,16,1,16,1,16,1,16,3,16, - 228,8,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,238,8,17,1,18,1,18, - 1,19,1,19,1,19,1,19,5,19,246,8,19,10,19,12,19,249,9,19,1,20,1,20,3,20,253, - 8,20,1,21,1,21,5,21,257,8,21,10,21,12,21,260,9,21,1,21,1,21,1,21,1,21,1, - 22,1,22,1,22,1,22,1,22,1,22,4,22,272,8,22,11,22,12,22,273,1,22,1,22,1,22, - 1,23,1,23,1,23,1,23,1,23,3,23,284,8,23,1,24,1,24,1,24,1,24,1,24,1,24,1, - 24,3,24,293,8,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,3,25,302,8,25,1,25, - 1,25,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,316,8,27,1, - 28,1,28,1,28,3,28,321,8,28,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,30, - 1,30,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1, - 31,1,32,1,32,3,32,349,8,32,1,33,1,33,1,34,1,34,3,34,355,8,34,1,35,1,35, - 1,35,1,35,5,35,361,8,35,10,35,12,35,364,9,35,1,35,3,35,367,8,35,3,35,369, - 8,35,1,35,1,35,1,36,1,36,1,36,1,37,1,37,1,37,1,37,5,37,380,8,37,10,37,12, - 37,383,9,37,1,37,3,37,386,8,37,3,37,388,8,37,1,37,1,37,1,38,1,38,1,38,1, - 38,1,38,1,38,1,38,1,38,1,38,3,38,401,8,38,1,38,1,38,1,38,1,38,1,38,1,38, - 1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, - 38,1,38,1,38,1,38,5,38,427,8,38,10,38,12,38,430,9,38,1,38,3,38,433,8,38, - 3,38,435,8,38,1,38,1,38,1,38,1,38,3,38,441,8,38,1,38,1,38,1,38,1,38,1,38, - 1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, - 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38, - 1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, - 38,1,38,5,38,493,8,38,10,38,12,38,496,9,38,1,39,1,39,1,40,1,40,1,40,1,40, - 1,40,3,40,505,8,40,1,41,1,41,3,41,509,8,41,1,42,1,42,1,43,1,43,1,43,0,1, - 76,44,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46, - 48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,0,15,1,0,4, - 10,2,0,10,10,22,23,1,0,24,25,1,0,37,41,2,0,37,41,43,46,2,0,5,5,51,52,1, - 0,53,55,2,0,52,52,56,56,1,0,57,58,1,0,6,9,1,0,59,60,1,0,47,48,2,0,17,17, - 65,65,1,0,72,74,2,0,72,73,80,80,546,0,91,1,0,0,0,2,108,1,0,0,0,4,113,1, - 0,0,0,6,115,1,0,0,0,8,120,1,0,0,0,10,124,1,0,0,0,12,126,1,0,0,0,14,133, - 1,0,0,0,16,135,1,0,0,0,18,154,1,0,0,0,20,161,1,0,0,0,22,173,1,0,0,0,24, - 178,1,0,0,0,26,187,1,0,0,0,28,203,1,0,0,0,30,221,1,0,0,0,32,227,1,0,0,0, - 34,237,1,0,0,0,36,239,1,0,0,0,38,241,1,0,0,0,40,252,1,0,0,0,42,254,1,0, - 0,0,44,265,1,0,0,0,46,283,1,0,0,0,48,285,1,0,0,0,50,296,1,0,0,0,52,305, - 1,0,0,0,54,308,1,0,0,0,56,320,1,0,0,0,58,322,1,0,0,0,60,330,1,0,0,0,62, - 336,1,0,0,0,64,348,1,0,0,0,66,350,1,0,0,0,68,354,1,0,0,0,70,356,1,0,0,0, - 72,372,1,0,0,0,74,375,1,0,0,0,76,440,1,0,0,0,78,497,1,0,0,0,80,504,1,0, - 0,0,82,506,1,0,0,0,84,510,1,0,0,0,86,512,1,0,0,0,88,90,3,2,1,0,89,88,1, - 0,0,0,90,93,1,0,0,0,91,89,1,0,0,0,91,92,1,0,0,0,92,97,1,0,0,0,93,91,1,0, - 0,0,94,96,3,12,6,0,95,94,1,0,0,0,96,99,1,0,0,0,97,95,1,0,0,0,97,98,1,0, - 0,0,98,103,1,0,0,0,99,97,1,0,0,0,100,102,3,14,7,0,101,100,1,0,0,0,102,105, - 1,0,0,0,103,101,1,0,0,0,103,104,1,0,0,0,104,106,1,0,0,0,105,103,1,0,0,0, - 106,107,5,0,0,1,107,1,1,0,0,0,108,109,5,1,0,0,109,110,3,4,2,0,110,111,3, - 6,3,0,111,112,5,2,0,0,112,3,1,0,0,0,113,114,5,3,0,0,114,5,1,0,0,0,115,117, - 3,8,4,0,116,118,3,8,4,0,117,116,1,0,0,0,117,118,1,0,0,0,118,7,1,0,0,0,119, - 121,3,10,5,0,120,119,1,0,0,0,120,121,1,0,0,0,121,122,1,0,0,0,122,123,5, - 66,0,0,123,9,1,0,0,0,124,125,7,0,0,0,125,11,1,0,0,0,126,127,5,11,0,0,127, - 128,5,76,0,0,128,129,5,2,0,0,129,13,1,0,0,0,130,134,3,16,8,0,131,134,3, - 18,9,0,132,134,3,20,10,0,133,130,1,0,0,0,133,131,1,0,0,0,133,132,1,0,0, - 0,134,15,1,0,0,0,135,136,5,12,0,0,136,137,5,82,0,0,137,150,3,26,13,0,138, - 139,5,13,0,0,139,140,5,14,0,0,140,145,3,84,42,0,141,142,5,15,0,0,142,144, - 3,84,42,0,143,141,1,0,0,0,144,147,1,0,0,0,145,143,1,0,0,0,145,146,1,0,0, - 0,146,148,1,0,0,0,147,145,1,0,0,0,148,149,5,16,0,0,149,151,1,0,0,0,150, - 138,1,0,0,0,150,151,1,0,0,0,151,152,1,0,0,0,152,153,3,24,12,0,153,17,1, - 0,0,0,154,155,3,84,42,0,155,156,5,17,0,0,156,157,5,82,0,0,157,158,5,10, - 0,0,158,159,3,76,38,0,159,160,5,2,0,0,160,19,1,0,0,0,161,162,5,18,0,0,162, - 163,5,82,0,0,163,164,3,26,13,0,164,168,5,19,0,0,165,167,3,22,11,0,166,165, - 1,0,0,0,167,170,1,0,0,0,168,166,1,0,0,0,168,169,1,0,0,0,169,171,1,0,0,0, - 170,168,1,0,0,0,171,172,5,20,0,0,172,21,1,0,0,0,173,174,5,12,0,0,174,175, - 5,82,0,0,175,176,3,26,13,0,176,177,3,24,12,0,177,23,1,0,0,0,178,182,5,19, - 0,0,179,181,3,32,16,0,180,179,1,0,0,0,181,184,1,0,0,0,182,180,1,0,0,0,182, - 183,1,0,0,0,183,185,1,0,0,0,184,182,1,0,0,0,185,186,5,20,0,0,186,25,1,0, - 0,0,187,199,5,14,0,0,188,193,3,28,14,0,189,190,5,15,0,0,190,192,3,28,14, - 0,191,189,1,0,0,0,192,195,1,0,0,0,193,191,1,0,0,0,193,194,1,0,0,0,194,197, - 1,0,0,0,195,193,1,0,0,0,196,198,5,15,0,0,197,196,1,0,0,0,197,198,1,0,0, - 0,198,200,1,0,0,0,199,188,1,0,0,0,199,200,1,0,0,0,200,201,1,0,0,0,201,202, - 5,16,0,0,202,27,1,0,0,0,203,207,3,84,42,0,204,206,3,78,39,0,205,204,1,0, - 0,0,206,209,1,0,0,0,207,205,1,0,0,0,207,208,1,0,0,0,208,210,1,0,0,0,209, - 207,1,0,0,0,210,211,5,82,0,0,211,29,1,0,0,0,212,216,5,19,0,0,213,215,3, - 32,16,0,214,213,1,0,0,0,215,218,1,0,0,0,216,214,1,0,0,0,216,217,1,0,0,0, - 217,219,1,0,0,0,218,216,1,0,0,0,219,222,5,20,0,0,220,222,3,32,16,0,221, - 212,1,0,0,0,221,220,1,0,0,0,222,31,1,0,0,0,223,228,3,40,20,0,224,225,3, - 34,17,0,225,226,5,2,0,0,226,228,1,0,0,0,227,223,1,0,0,0,227,224,1,0,0,0, - 228,33,1,0,0,0,229,238,3,42,21,0,230,238,3,44,22,0,231,238,3,46,23,0,232, - 238,3,48,24,0,233,238,3,50,25,0,234,238,3,36,18,0,235,238,3,52,26,0,236, - 238,3,38,19,0,237,229,1,0,0,0,237,230,1,0,0,0,237,231,1,0,0,0,237,232,1, - 0,0,0,237,233,1,0,0,0,237,234,1,0,0,0,237,235,1,0,0,0,237,236,1,0,0,0,238, - 35,1,0,0,0,239,240,3,72,36,0,240,37,1,0,0,0,241,242,5,21,0,0,242,247,3, - 76,38,0,243,244,5,15,0,0,244,246,3,76,38,0,245,243,1,0,0,0,246,249,1,0, - 0,0,247,245,1,0,0,0,247,248,1,0,0,0,248,39,1,0,0,0,249,247,1,0,0,0,250, - 253,3,54,27,0,251,253,3,56,28,0,252,250,1,0,0,0,252,251,1,0,0,0,253,41, - 1,0,0,0,254,258,3,84,42,0,255,257,3,78,39,0,256,255,1,0,0,0,257,260,1,0, - 0,0,258,256,1,0,0,0,258,259,1,0,0,0,259,261,1,0,0,0,260,258,1,0,0,0,261, - 262,5,82,0,0,262,263,5,10,0,0,263,264,3,76,38,0,264,43,1,0,0,0,265,266, - 3,84,42,0,266,271,5,82,0,0,267,268,5,15,0,0,268,269,3,84,42,0,269,270,5, - 82,0,0,270,272,1,0,0,0,271,267,1,0,0,0,272,273,1,0,0,0,273,271,1,0,0,0, - 273,274,1,0,0,0,274,275,1,0,0,0,275,276,5,10,0,0,276,277,3,76,38,0,277, - 45,1,0,0,0,278,279,5,82,0,0,279,280,7,1,0,0,280,284,3,76,38,0,281,282,5, - 82,0,0,282,284,7,2,0,0,283,278,1,0,0,0,283,281,1,0,0,0,284,47,1,0,0,0,285, - 286,5,26,0,0,286,287,5,14,0,0,287,288,5,79,0,0,288,289,5,6,0,0,289,292, - 3,76,38,0,290,291,5,15,0,0,291,293,3,66,33,0,292,290,1,0,0,0,292,293,1, - 0,0,0,293,294,1,0,0,0,294,295,5,16,0,0,295,49,1,0,0,0,296,297,5,26,0,0, - 297,298,5,14,0,0,298,301,3,76,38,0,299,300,5,15,0,0,300,302,3,66,33,0,301, - 299,1,0,0,0,301,302,1,0,0,0,302,303,1,0,0,0,303,304,5,16,0,0,304,51,1,0, - 0,0,305,306,5,27,0,0,306,307,3,70,35,0,307,53,1,0,0,0,308,309,5,28,0,0, - 309,310,5,14,0,0,310,311,3,76,38,0,311,312,5,16,0,0,312,315,3,30,15,0,313, - 314,5,29,0,0,314,316,3,30,15,0,315,313,1,0,0,0,315,316,1,0,0,0,316,55,1, - 0,0,0,317,321,3,58,29,0,318,321,3,60,30,0,319,321,3,62,31,0,320,317,1,0, - 0,0,320,318,1,0,0,0,320,319,1,0,0,0,321,57,1,0,0,0,322,323,5,30,0,0,323, - 324,3,30,15,0,324,325,5,31,0,0,325,326,5,14,0,0,326,327,3,76,38,0,327,328, - 5,16,0,0,328,329,5,2,0,0,329,59,1,0,0,0,330,331,5,31,0,0,331,332,5,14,0, - 0,332,333,3,76,38,0,333,334,5,16,0,0,334,335,3,30,15,0,335,61,1,0,0,0,336, - 337,5,32,0,0,337,338,5,14,0,0,338,339,3,64,32,0,339,340,5,2,0,0,340,341, - 3,76,38,0,341,342,5,2,0,0,342,343,3,46,23,0,343,344,5,16,0,0,344,345,3, - 30,15,0,345,63,1,0,0,0,346,349,3,42,21,0,347,349,3,46,23,0,348,346,1,0, - 0,0,348,347,1,0,0,0,349,65,1,0,0,0,350,351,5,76,0,0,351,67,1,0,0,0,352, - 355,5,82,0,0,353,355,3,80,40,0,354,352,1,0,0,0,354,353,1,0,0,0,355,69,1, - 0,0,0,356,368,5,14,0,0,357,362,3,68,34,0,358,359,5,15,0,0,359,361,3,68, - 34,0,360,358,1,0,0,0,361,364,1,0,0,0,362,360,1,0,0,0,362,363,1,0,0,0,363, - 366,1,0,0,0,364,362,1,0,0,0,365,367,5,15,0,0,366,365,1,0,0,0,366,367,1, - 0,0,0,367,369,1,0,0,0,368,357,1,0,0,0,368,369,1,0,0,0,369,370,1,0,0,0,370, - 371,5,16,0,0,371,71,1,0,0,0,372,373,5,82,0,0,373,374,3,74,37,0,374,73,1, - 0,0,0,375,387,5,14,0,0,376,381,3,76,38,0,377,378,5,15,0,0,378,380,3,76, - 38,0,379,377,1,0,0,0,380,383,1,0,0,0,381,379,1,0,0,0,381,382,1,0,0,0,382, - 385,1,0,0,0,383,381,1,0,0,0,384,386,5,15,0,0,385,384,1,0,0,0,385,386,1, - 0,0,0,386,388,1,0,0,0,387,376,1,0,0,0,387,388,1,0,0,0,388,389,1,0,0,0,389, - 390,5,16,0,0,390,75,1,0,0,0,391,392,6,38,-1,0,392,393,5,14,0,0,393,394, - 3,76,38,0,394,395,5,16,0,0,395,441,1,0,0,0,396,397,3,86,43,0,397,398,5, - 14,0,0,398,400,3,76,38,0,399,401,5,15,0,0,400,399,1,0,0,0,400,401,1,0,0, - 0,401,402,1,0,0,0,402,403,5,16,0,0,403,441,1,0,0,0,404,441,3,72,36,0,405, - 406,5,33,0,0,406,407,5,82,0,0,407,441,3,74,37,0,408,409,5,36,0,0,409,410, - 5,34,0,0,410,411,3,76,38,0,411,412,5,35,0,0,412,413,7,3,0,0,413,441,1,0, - 0,0,414,415,5,42,0,0,415,416,5,34,0,0,416,417,3,76,38,0,417,418,5,35,0, - 0,418,419,7,4,0,0,419,441,1,0,0,0,420,421,7,5,0,0,421,441,3,76,38,15,422, - 434,5,34,0,0,423,428,3,76,38,0,424,425,5,15,0,0,425,427,3,76,38,0,426,424, - 1,0,0,0,427,430,1,0,0,0,428,426,1,0,0,0,428,429,1,0,0,0,429,432,1,0,0,0, - 430,428,1,0,0,0,431,433,5,15,0,0,432,431,1,0,0,0,432,433,1,0,0,0,433,435, - 1,0,0,0,434,423,1,0,0,0,434,435,1,0,0,0,435,436,1,0,0,0,436,441,5,35,0, - 0,437,441,5,81,0,0,438,441,5,82,0,0,439,441,3,80,40,0,440,391,1,0,0,0,440, - 396,1,0,0,0,440,404,1,0,0,0,440,405,1,0,0,0,440,408,1,0,0,0,440,414,1,0, - 0,0,440,420,1,0,0,0,440,422,1,0,0,0,440,437,1,0,0,0,440,438,1,0,0,0,440, - 439,1,0,0,0,441,494,1,0,0,0,442,443,10,14,0,0,443,444,7,6,0,0,444,493,3, - 76,38,15,445,446,10,13,0,0,446,447,7,7,0,0,447,493,3,76,38,14,448,449,10, - 12,0,0,449,450,7,8,0,0,450,493,3,76,38,13,451,452,10,11,0,0,452,453,7,9, - 0,0,453,493,3,76,38,12,454,455,10,10,0,0,455,456,7,10,0,0,456,493,3,76, - 38,11,457,458,10,9,0,0,458,459,5,61,0,0,459,493,3,76,38,10,460,461,10,8, - 0,0,461,462,5,4,0,0,462,493,3,76,38,9,463,464,10,7,0,0,464,465,5,62,0,0, - 465,493,3,76,38,8,466,467,10,6,0,0,467,468,5,63,0,0,468,493,3,76,38,7,469, - 470,10,5,0,0,470,471,5,64,0,0,471,493,3,76,38,6,472,473,10,21,0,0,473,474, - 5,34,0,0,474,475,5,69,0,0,475,493,5,35,0,0,476,477,10,18,0,0,477,493,7, - 11,0,0,478,479,10,17,0,0,479,480,5,49,0,0,480,481,5,14,0,0,481,482,3,76, - 38,0,482,483,5,16,0,0,483,493,1,0,0,0,484,485,10,16,0,0,485,486,5,50,0, - 0,486,487,5,14,0,0,487,488,3,76,38,0,488,489,5,15,0,0,489,490,3,76,38,0, - 490,491,5,16,0,0,491,493,1,0,0,0,492,442,1,0,0,0,492,445,1,0,0,0,492,448, - 1,0,0,0,492,451,1,0,0,0,492,454,1,0,0,0,492,457,1,0,0,0,492,460,1,0,0,0, - 492,463,1,0,0,0,492,466,1,0,0,0,492,469,1,0,0,0,492,472,1,0,0,0,492,476, - 1,0,0,0,492,478,1,0,0,0,492,484,1,0,0,0,493,496,1,0,0,0,494,492,1,0,0,0, - 494,495,1,0,0,0,495,77,1,0,0,0,496,494,1,0,0,0,497,498,7,12,0,0,498,79, - 1,0,0,0,499,505,5,67,0,0,500,505,3,82,41,0,501,505,5,76,0,0,502,505,5,77, - 0,0,503,505,5,78,0,0,504,499,1,0,0,0,504,500,1,0,0,0,504,501,1,0,0,0,504, - 502,1,0,0,0,504,503,1,0,0,0,505,81,1,0,0,0,506,508,5,69,0,0,507,509,5,68, - 0,0,508,507,1,0,0,0,508,509,1,0,0,0,509,83,1,0,0,0,510,511,7,13,0,0,511, - 85,1,0,0,0,512,513,7,14,0,0,513,87,1,0,0,0,44,91,97,103,117,120,133,145, - 150,168,182,193,197,199,207,216,221,227,237,247,252,258,273,283,292,301, - 315,320,348,354,362,366,368,381,385,387,400,428,432,434,440,492,494,504, - 508]; + 39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,1,0,5,0,92,8, + 0,10,0,12,0,95,9,0,1,0,5,0,98,8,0,10,0,12,0,101,9,0,1,0,5,0,104,8,0,10, + 0,12,0,107,9,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,3,1,3,3,3,120,8,3, + 1,4,3,4,123,8,4,1,4,1,4,1,5,1,5,1,6,1,6,1,6,1,6,1,7,1,7,1,7,3,7,136,8,7, + 1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,5,8,146,8,8,10,8,12,8,149,9,8,1,8,1,8,3, + 8,153,8,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10, + 5,10,169,8,10,10,10,12,10,172,9,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1, + 12,1,12,5,12,183,8,12,10,12,12,12,186,9,12,1,12,1,12,1,13,1,13,1,13,1,13, + 5,13,194,8,13,10,13,12,13,197,9,13,1,13,3,13,200,8,13,3,13,202,8,13,1,13, + 1,13,1,14,1,14,5,14,208,8,14,10,14,12,14,211,9,14,1,14,1,14,1,15,1,15,5, + 15,217,8,15,10,15,12,15,220,9,15,1,15,1,15,3,15,224,8,15,1,16,1,16,1,16, + 1,16,3,16,230,8,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,240,8,17, + 1,18,1,18,1,19,1,19,1,19,1,19,5,19,248,8,19,10,19,12,19,251,9,19,1,20,1, + 20,3,20,255,8,20,1,21,1,21,5,21,259,8,21,10,21,12,21,262,9,21,1,21,1,21, + 1,21,1,21,1,22,1,22,1,22,4,22,271,8,22,11,22,12,22,272,1,22,1,22,1,22,1, + 22,1,22,1,22,1,22,4,22,282,8,22,11,22,12,22,283,1,22,1,22,1,22,1,22,3,22, + 290,8,22,1,23,1,23,1,23,1,23,3,23,296,8,23,1,24,1,24,1,24,1,24,1,24,3,24, + 303,8,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,3,25,312,8,25,1,25,1,25,1,26, + 1,26,1,26,1,26,1,26,3,26,321,8,26,1,26,1,26,1,27,1,27,1,27,1,28,1,28,1, + 28,1,28,1,28,1,28,1,28,3,28,335,8,28,1,29,1,29,1,29,3,29,340,8,29,1,30, + 1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1, + 32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,3,33,368,8,33,1,34, + 1,34,1,35,1,35,3,35,374,8,35,1,36,1,36,1,36,1,36,5,36,380,8,36,10,36,12, + 36,383,9,36,1,36,3,36,386,8,36,3,36,388,8,36,1,36,1,36,1,37,1,37,1,37,1, + 38,1,38,1,38,1,38,5,38,399,8,38,10,38,12,38,402,9,38,1,38,3,38,405,8,38, + 3,38,407,8,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,3, + 39,420,8,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, + 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,5,39,446,8, + 39,10,39,12,39,449,9,39,1,39,3,39,452,8,39,3,39,454,8,39,1,39,1,39,1,39, + 1,39,3,39,460,8,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1, + 39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, + 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1, + 39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,5,39,512,8,39,10,39, + 12,39,515,9,39,1,40,1,40,1,41,1,41,1,41,1,41,1,41,3,41,524,8,41,1,42,1, + 42,3,42,528,8,42,1,43,1,43,1,44,1,44,1,44,0,1,78,45,0,2,4,6,8,10,12,14, + 16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62, + 64,66,68,70,72,74,76,78,80,82,84,86,88,0,15,1,0,4,10,2,0,10,10,22,23,1, + 0,24,25,1,0,37,41,2,0,37,41,43,46,2,0,5,5,51,52,1,0,53,55,2,0,52,52,56, + 56,1,0,57,58,1,0,6,9,1,0,59,60,1,0,47,48,2,0,17,17,65,65,1,0,72,74,2,0, + 72,73,80,80,567,0,93,1,0,0,0,2,110,1,0,0,0,4,115,1,0,0,0,6,117,1,0,0,0, + 8,122,1,0,0,0,10,126,1,0,0,0,12,128,1,0,0,0,14,135,1,0,0,0,16,137,1,0,0, + 0,18,156,1,0,0,0,20,163,1,0,0,0,22,175,1,0,0,0,24,180,1,0,0,0,26,189,1, + 0,0,0,28,205,1,0,0,0,30,223,1,0,0,0,32,229,1,0,0,0,34,239,1,0,0,0,36,241, + 1,0,0,0,38,243,1,0,0,0,40,254,1,0,0,0,42,256,1,0,0,0,44,289,1,0,0,0,46, + 295,1,0,0,0,48,302,1,0,0,0,50,304,1,0,0,0,52,315,1,0,0,0,54,324,1,0,0,0, + 56,327,1,0,0,0,58,339,1,0,0,0,60,341,1,0,0,0,62,349,1,0,0,0,64,355,1,0, + 0,0,66,367,1,0,0,0,68,369,1,0,0,0,70,373,1,0,0,0,72,375,1,0,0,0,74,391, + 1,0,0,0,76,394,1,0,0,0,78,459,1,0,0,0,80,516,1,0,0,0,82,523,1,0,0,0,84, + 525,1,0,0,0,86,529,1,0,0,0,88,531,1,0,0,0,90,92,3,2,1,0,91,90,1,0,0,0,92, + 95,1,0,0,0,93,91,1,0,0,0,93,94,1,0,0,0,94,99,1,0,0,0,95,93,1,0,0,0,96,98, + 3,12,6,0,97,96,1,0,0,0,98,101,1,0,0,0,99,97,1,0,0,0,99,100,1,0,0,0,100, + 105,1,0,0,0,101,99,1,0,0,0,102,104,3,14,7,0,103,102,1,0,0,0,104,107,1,0, + 0,0,105,103,1,0,0,0,105,106,1,0,0,0,106,108,1,0,0,0,107,105,1,0,0,0,108, + 109,5,0,0,1,109,1,1,0,0,0,110,111,5,1,0,0,111,112,3,4,2,0,112,113,3,6,3, + 0,113,114,5,2,0,0,114,3,1,0,0,0,115,116,5,3,0,0,116,5,1,0,0,0,117,119,3, + 8,4,0,118,120,3,8,4,0,119,118,1,0,0,0,119,120,1,0,0,0,120,7,1,0,0,0,121, + 123,3,10,5,0,122,121,1,0,0,0,122,123,1,0,0,0,123,124,1,0,0,0,124,125,5, + 66,0,0,125,9,1,0,0,0,126,127,7,0,0,0,127,11,1,0,0,0,128,129,5,11,0,0,129, + 130,5,76,0,0,130,131,5,2,0,0,131,13,1,0,0,0,132,136,3,16,8,0,133,136,3, + 18,9,0,134,136,3,20,10,0,135,132,1,0,0,0,135,133,1,0,0,0,135,134,1,0,0, + 0,136,15,1,0,0,0,137,138,5,12,0,0,138,139,5,82,0,0,139,152,3,26,13,0,140, + 141,5,13,0,0,141,142,5,14,0,0,142,147,3,86,43,0,143,144,5,15,0,0,144,146, + 3,86,43,0,145,143,1,0,0,0,146,149,1,0,0,0,147,145,1,0,0,0,147,148,1,0,0, + 0,148,150,1,0,0,0,149,147,1,0,0,0,150,151,5,16,0,0,151,153,1,0,0,0,152, + 140,1,0,0,0,152,153,1,0,0,0,153,154,1,0,0,0,154,155,3,24,12,0,155,17,1, + 0,0,0,156,157,3,86,43,0,157,158,5,17,0,0,158,159,5,82,0,0,159,160,5,10, + 0,0,160,161,3,78,39,0,161,162,5,2,0,0,162,19,1,0,0,0,163,164,5,18,0,0,164, + 165,5,82,0,0,165,166,3,26,13,0,166,170,5,19,0,0,167,169,3,22,11,0,168,167, + 1,0,0,0,169,172,1,0,0,0,170,168,1,0,0,0,170,171,1,0,0,0,171,173,1,0,0,0, + 172,170,1,0,0,0,173,174,5,20,0,0,174,21,1,0,0,0,175,176,5,12,0,0,176,177, + 5,82,0,0,177,178,3,26,13,0,178,179,3,24,12,0,179,23,1,0,0,0,180,184,5,19, + 0,0,181,183,3,32,16,0,182,181,1,0,0,0,183,186,1,0,0,0,184,182,1,0,0,0,184, + 185,1,0,0,0,185,187,1,0,0,0,186,184,1,0,0,0,187,188,5,20,0,0,188,25,1,0, + 0,0,189,201,5,14,0,0,190,195,3,28,14,0,191,192,5,15,0,0,192,194,3,28,14, + 0,193,191,1,0,0,0,194,197,1,0,0,0,195,193,1,0,0,0,195,196,1,0,0,0,196,199, + 1,0,0,0,197,195,1,0,0,0,198,200,5,15,0,0,199,198,1,0,0,0,199,200,1,0,0, + 0,200,202,1,0,0,0,201,190,1,0,0,0,201,202,1,0,0,0,202,203,1,0,0,0,203,204, + 5,16,0,0,204,27,1,0,0,0,205,209,3,86,43,0,206,208,3,80,40,0,207,206,1,0, + 0,0,208,211,1,0,0,0,209,207,1,0,0,0,209,210,1,0,0,0,210,212,1,0,0,0,211, + 209,1,0,0,0,212,213,5,82,0,0,213,29,1,0,0,0,214,218,5,19,0,0,215,217,3, + 32,16,0,216,215,1,0,0,0,217,220,1,0,0,0,218,216,1,0,0,0,218,219,1,0,0,0, + 219,221,1,0,0,0,220,218,1,0,0,0,221,224,5,20,0,0,222,224,3,32,16,0,223, + 214,1,0,0,0,223,222,1,0,0,0,224,31,1,0,0,0,225,230,3,40,20,0,226,227,3, + 34,17,0,227,228,5,2,0,0,228,230,1,0,0,0,229,225,1,0,0,0,229,226,1,0,0,0, + 230,33,1,0,0,0,231,240,3,42,21,0,232,240,3,44,22,0,233,240,3,48,24,0,234, + 240,3,50,25,0,235,240,3,52,26,0,236,240,3,36,18,0,237,240,3,54,27,0,238, + 240,3,38,19,0,239,231,1,0,0,0,239,232,1,0,0,0,239,233,1,0,0,0,239,234,1, + 0,0,0,239,235,1,0,0,0,239,236,1,0,0,0,239,237,1,0,0,0,239,238,1,0,0,0,240, + 35,1,0,0,0,241,242,3,74,37,0,242,37,1,0,0,0,243,244,5,21,0,0,244,249,3, + 78,39,0,245,246,5,15,0,0,246,248,3,78,39,0,247,245,1,0,0,0,248,251,1,0, + 0,0,249,247,1,0,0,0,249,250,1,0,0,0,250,39,1,0,0,0,251,249,1,0,0,0,252, + 255,3,56,28,0,253,255,3,58,29,0,254,252,1,0,0,0,254,253,1,0,0,0,255,41, + 1,0,0,0,256,260,3,86,43,0,257,259,3,80,40,0,258,257,1,0,0,0,259,262,1,0, + 0,0,260,258,1,0,0,0,260,261,1,0,0,0,261,263,1,0,0,0,262,260,1,0,0,0,263, + 264,5,82,0,0,264,265,5,10,0,0,265,266,3,78,39,0,266,43,1,0,0,0,267,270, + 3,46,23,0,268,269,5,15,0,0,269,271,3,46,23,0,270,268,1,0,0,0,271,272,1, + 0,0,0,272,270,1,0,0,0,272,273,1,0,0,0,273,274,1,0,0,0,274,275,5,10,0,0, + 275,276,3,78,39,0,276,290,1,0,0,0,277,278,5,14,0,0,278,281,3,46,23,0,279, + 280,5,15,0,0,280,282,3,46,23,0,281,279,1,0,0,0,282,283,1,0,0,0,283,281, + 1,0,0,0,283,284,1,0,0,0,284,285,1,0,0,0,285,286,5,16,0,0,286,287,5,10,0, + 0,287,288,3,78,39,0,288,290,1,0,0,0,289,267,1,0,0,0,289,277,1,0,0,0,290, + 45,1,0,0,0,291,292,3,86,43,0,292,293,5,82,0,0,293,296,1,0,0,0,294,296,5, + 82,0,0,295,291,1,0,0,0,295,294,1,0,0,0,296,47,1,0,0,0,297,298,5,82,0,0, + 298,299,7,1,0,0,299,303,3,78,39,0,300,301,5,82,0,0,301,303,7,2,0,0,302, + 297,1,0,0,0,302,300,1,0,0,0,303,49,1,0,0,0,304,305,5,26,0,0,305,306,5,14, + 0,0,306,307,5,79,0,0,307,308,5,6,0,0,308,311,3,78,39,0,309,310,5,15,0,0, + 310,312,3,68,34,0,311,309,1,0,0,0,311,312,1,0,0,0,312,313,1,0,0,0,313,314, + 5,16,0,0,314,51,1,0,0,0,315,316,5,26,0,0,316,317,5,14,0,0,317,320,3,78, + 39,0,318,319,5,15,0,0,319,321,3,68,34,0,320,318,1,0,0,0,320,321,1,0,0,0, + 321,322,1,0,0,0,322,323,5,16,0,0,323,53,1,0,0,0,324,325,5,27,0,0,325,326, + 3,72,36,0,326,55,1,0,0,0,327,328,5,28,0,0,328,329,5,14,0,0,329,330,3,78, + 39,0,330,331,5,16,0,0,331,334,3,30,15,0,332,333,5,29,0,0,333,335,3,30,15, + 0,334,332,1,0,0,0,334,335,1,0,0,0,335,57,1,0,0,0,336,340,3,60,30,0,337, + 340,3,62,31,0,338,340,3,64,32,0,339,336,1,0,0,0,339,337,1,0,0,0,339,338, + 1,0,0,0,340,59,1,0,0,0,341,342,5,30,0,0,342,343,3,30,15,0,343,344,5,31, + 0,0,344,345,5,14,0,0,345,346,3,78,39,0,346,347,5,16,0,0,347,348,5,2,0,0, + 348,61,1,0,0,0,349,350,5,31,0,0,350,351,5,14,0,0,351,352,3,78,39,0,352, + 353,5,16,0,0,353,354,3,30,15,0,354,63,1,0,0,0,355,356,5,32,0,0,356,357, + 5,14,0,0,357,358,3,66,33,0,358,359,5,2,0,0,359,360,3,78,39,0,360,361,5, + 2,0,0,361,362,3,48,24,0,362,363,5,16,0,0,363,364,3,30,15,0,364,65,1,0,0, + 0,365,368,3,42,21,0,366,368,3,48,24,0,367,365,1,0,0,0,367,366,1,0,0,0,368, + 67,1,0,0,0,369,370,5,76,0,0,370,69,1,0,0,0,371,374,5,82,0,0,372,374,3,82, + 41,0,373,371,1,0,0,0,373,372,1,0,0,0,374,71,1,0,0,0,375,387,5,14,0,0,376, + 381,3,70,35,0,377,378,5,15,0,0,378,380,3,70,35,0,379,377,1,0,0,0,380,383, + 1,0,0,0,381,379,1,0,0,0,381,382,1,0,0,0,382,385,1,0,0,0,383,381,1,0,0,0, + 384,386,5,15,0,0,385,384,1,0,0,0,385,386,1,0,0,0,386,388,1,0,0,0,387,376, + 1,0,0,0,387,388,1,0,0,0,388,389,1,0,0,0,389,390,5,16,0,0,390,73,1,0,0,0, + 391,392,5,82,0,0,392,393,3,76,38,0,393,75,1,0,0,0,394,406,5,14,0,0,395, + 400,3,78,39,0,396,397,5,15,0,0,397,399,3,78,39,0,398,396,1,0,0,0,399,402, + 1,0,0,0,400,398,1,0,0,0,400,401,1,0,0,0,401,404,1,0,0,0,402,400,1,0,0,0, + 403,405,5,15,0,0,404,403,1,0,0,0,404,405,1,0,0,0,405,407,1,0,0,0,406,395, + 1,0,0,0,406,407,1,0,0,0,407,408,1,0,0,0,408,409,5,16,0,0,409,77,1,0,0,0, + 410,411,6,39,-1,0,411,412,5,14,0,0,412,413,3,78,39,0,413,414,5,16,0,0,414, + 460,1,0,0,0,415,416,3,88,44,0,416,417,5,14,0,0,417,419,3,78,39,0,418,420, + 5,15,0,0,419,418,1,0,0,0,419,420,1,0,0,0,420,421,1,0,0,0,421,422,5,16,0, + 0,422,460,1,0,0,0,423,460,3,74,37,0,424,425,5,33,0,0,425,426,5,82,0,0,426, + 460,3,76,38,0,427,428,5,36,0,0,428,429,5,34,0,0,429,430,3,78,39,0,430,431, + 5,35,0,0,431,432,7,3,0,0,432,460,1,0,0,0,433,434,5,42,0,0,434,435,5,34, + 0,0,435,436,3,78,39,0,436,437,5,35,0,0,437,438,7,4,0,0,438,460,1,0,0,0, + 439,440,7,5,0,0,440,460,3,78,39,15,441,453,5,34,0,0,442,447,3,78,39,0,443, + 444,5,15,0,0,444,446,3,78,39,0,445,443,1,0,0,0,446,449,1,0,0,0,447,445, + 1,0,0,0,447,448,1,0,0,0,448,451,1,0,0,0,449,447,1,0,0,0,450,452,5,15,0, + 0,451,450,1,0,0,0,451,452,1,0,0,0,452,454,1,0,0,0,453,442,1,0,0,0,453,454, + 1,0,0,0,454,455,1,0,0,0,455,460,5,35,0,0,456,460,5,81,0,0,457,460,5,82, + 0,0,458,460,3,82,41,0,459,410,1,0,0,0,459,415,1,0,0,0,459,423,1,0,0,0,459, + 424,1,0,0,0,459,427,1,0,0,0,459,433,1,0,0,0,459,439,1,0,0,0,459,441,1,0, + 0,0,459,456,1,0,0,0,459,457,1,0,0,0,459,458,1,0,0,0,460,513,1,0,0,0,461, + 462,10,14,0,0,462,463,7,6,0,0,463,512,3,78,39,15,464,465,10,13,0,0,465, + 466,7,7,0,0,466,512,3,78,39,14,467,468,10,12,0,0,468,469,7,8,0,0,469,512, + 3,78,39,13,470,471,10,11,0,0,471,472,7,9,0,0,472,512,3,78,39,12,473,474, + 10,10,0,0,474,475,7,10,0,0,475,512,3,78,39,11,476,477,10,9,0,0,477,478, + 5,61,0,0,478,512,3,78,39,10,479,480,10,8,0,0,480,481,5,4,0,0,481,512,3, + 78,39,9,482,483,10,7,0,0,483,484,5,62,0,0,484,512,3,78,39,8,485,486,10, + 6,0,0,486,487,5,63,0,0,487,512,3,78,39,7,488,489,10,5,0,0,489,490,5,64, + 0,0,490,512,3,78,39,6,491,492,10,21,0,0,492,493,5,34,0,0,493,494,5,69,0, + 0,494,512,5,35,0,0,495,496,10,18,0,0,496,512,7,11,0,0,497,498,10,17,0,0, + 498,499,5,49,0,0,499,500,5,14,0,0,500,501,3,78,39,0,501,502,5,16,0,0,502, + 512,1,0,0,0,503,504,10,16,0,0,504,505,5,50,0,0,505,506,5,14,0,0,506,507, + 3,78,39,0,507,508,5,15,0,0,508,509,3,78,39,0,509,510,5,16,0,0,510,512,1, + 0,0,0,511,461,1,0,0,0,511,464,1,0,0,0,511,467,1,0,0,0,511,470,1,0,0,0,511, + 473,1,0,0,0,511,476,1,0,0,0,511,479,1,0,0,0,511,482,1,0,0,0,511,485,1,0, + 0,0,511,488,1,0,0,0,511,491,1,0,0,0,511,495,1,0,0,0,511,497,1,0,0,0,511, + 503,1,0,0,0,512,515,1,0,0,0,513,511,1,0,0,0,513,514,1,0,0,0,514,79,1,0, + 0,0,515,513,1,0,0,0,516,517,7,12,0,0,517,81,1,0,0,0,518,524,5,67,0,0,519, + 524,3,84,42,0,520,524,5,76,0,0,521,524,5,77,0,0,522,524,5,78,0,0,523,518, + 1,0,0,0,523,519,1,0,0,0,523,520,1,0,0,0,523,521,1,0,0,0,523,522,1,0,0,0, + 524,83,1,0,0,0,525,527,5,69,0,0,526,528,5,68,0,0,527,526,1,0,0,0,527,528, + 1,0,0,0,528,85,1,0,0,0,529,530,7,13,0,0,530,87,1,0,0,0,531,532,7,14,0,0, + 532,89,1,0,0,0,47,93,99,105,119,122,135,147,152,170,184,195,199,201,209, + 218,223,229,239,249,254,260,272,283,289,295,302,311,320,334,339,367,373, + 381,385,387,400,404,406,419,447,451,453,459,511,513,523,527]; private static __ATN: ATN; public static get _ATN(): ATN { @@ -3537,17 +3628,11 @@ export class TupleAssignmentContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } - public typeName_list(): TypeNameContext[] { - return this.getTypedRuleContexts(TypeNameContext) as TypeNameContext[]; - } - public typeName(i: number): TypeNameContext { - return this.getTypedRuleContext(TypeNameContext, i) as TypeNameContext; + public tupleTarget_list(): TupleTargetContext[] { + return this.getTypedRuleContexts(TupleTargetContext) as TupleTargetContext[]; } - public Identifier_list(): TerminalNode[] { - return this.getTokens(CashScriptParser.Identifier); - } - public Identifier(i: number): TerminalNode { - return this.getToken(CashScriptParser.Identifier, i); + public tupleTarget(i: number): TupleTargetContext { + return this.getTypedRuleContext(TupleTargetContext, i) as TupleTargetContext; } public expression(): ExpressionContext { return this.getTypedRuleContext(ExpressionContext, 0) as ExpressionContext; @@ -3566,6 +3651,31 @@ export class TupleAssignmentContext extends ParserRuleContext { } +export class TupleTargetContext extends ParserRuleContext { + constructor(parser?: CashScriptParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public typeName(): TypeNameContext { + return this.getTypedRuleContext(TypeNameContext, 0) as TypeNameContext; + } + public Identifier(): TerminalNode { + return this.getToken(CashScriptParser.Identifier, 0); + } + public get ruleIndex(): number { + return CashScriptParser.RULE_tupleTarget; + } + // @Override + public accept(visitor: CashScriptVisitor): Result { + if (visitor.visitTupleTarget) { + return visitor.visitTupleTarget(this); + } else { + return visitor.visitChildren(this); + } + } +} + + export class AssignStatementContext extends ParserRuleContext { public _op!: Token; constructor(parser?: CashScriptParser, parent?: ParserRuleContext, invokingState?: number) { diff --git a/packages/cashc/src/grammar/CashScriptVisitor.ts b/packages/cashc/src/grammar/CashScriptVisitor.ts index 187bf8c79..3c4d117bc 100644 --- a/packages/cashc/src/grammar/CashScriptVisitor.ts +++ b/packages/cashc/src/grammar/CashScriptVisitor.ts @@ -26,6 +26,7 @@ import { ReturnStatementContext } from "./CashScriptParser.js"; import { ControlStatementContext } from "./CashScriptParser.js"; import { VariableDefinitionContext } from "./CashScriptParser.js"; import { TupleAssignmentContext } from "./CashScriptParser.js"; +import { TupleTargetContext } from "./CashScriptParser.js"; import { AssignStatementContext } from "./CashScriptParser.js"; import { TimeOpStatementContext } from "./CashScriptParser.js"; import { RequireStatementContext } from "./CashScriptParser.js"; @@ -207,6 +208,12 @@ export default class CashScriptVisitor extends ParseTreeVisitor * @return the visitor result */ visitTupleAssignment?: (ctx: TupleAssignmentContext) => Result; + /** + * Visit a parse tree produced by `CashScriptParser.tupleTarget`. + * @param ctx the parse tree + * @return the visitor result + */ + visitTupleTarget?: (ctx: TupleTargetContext) => Result; /** * Visit a parse tree produced by `CashScriptParser.assignStatement`. * @param ctx the parse tree diff --git a/packages/cashc/src/print/OutputSourceCodeTraversal.ts b/packages/cashc/src/print/OutputSourceCodeTraversal.ts index b6921d3ba..1bb52a77b 100644 --- a/packages/cashc/src/print/OutputSourceCodeTraversal.ts +++ b/packages/cashc/src/print/OutputSourceCodeTraversal.ts @@ -141,7 +141,10 @@ export default class OutputSourceCodeTraversal extends AstTraversal { } visitTupleAssignment(node: TupleAssignmentNode): Node { - const targets = node.targets.map((target) => `${target.type} ${target.name}`).join(', '); + // A reassignment target (no declared type) prints as a bare identifier; a declaration as `type name`. + const targets = node.targets + .map((target) => (target.isReassignment ? target.name : `${target.type} ${target.name}`)) + .join(', '); this.addOutput(`${targets} = `, true); this.visit(node.tuple); diff --git a/packages/cashc/src/semantic/SymbolTableTraversal.ts b/packages/cashc/src/semantic/SymbolTableTraversal.ts index 11c380a02..064781562 100644 --- a/packages/cashc/src/semantic/SymbolTableTraversal.ts +++ b/packages/cashc/src/semantic/SymbolTableTraversal.ts @@ -29,6 +29,8 @@ import { UnusedVariableError, InvalidSymbolTypeError, ConstantModificationError, + DuplicateTupleTargetError, + TupleTargetOrderError, InvalidModifierError, } from '../Errors.js'; @@ -177,7 +179,49 @@ export default class SymbolTableTraversal extends AstTraversal { } visitTupleAssignment(node: TupleAssignmentNode): Node { + // Declarations must form a contiguous block before all reassignment targets — the one layout + // scoped codegen can fold in place (see GenerateTargetTraversal.visitTupleAssignment). Enforced + // uniformly so a statement's legality does not depend on whether it sits inside a loop/branch. + const firstReassignment = node.targets.findIndex((target) => target.isReassignment); + if (firstReassignment !== -1 && node.targets.slice(firstReassignment).some((target) => !target.isReassignment)) { + throw new TupleTargetOrderError(node); + } + + const seenTargetNames = new Set(); node.targets.forEach((variable) => { + if (seenTargetNames.has(variable.name)) { + throw new DuplicateTupleTargetError(node, variable.name); + } + seenTargetNames.add(variable.name); + + if (variable.isReassignment) { + // Reassignment of an existing variable (`x`, no type): adopt its type for the type-check and + // register a reference (so it isn't flagged unused). Do NOT create a new symbol. + const reference = new IdentifierNode(variable.name); + reference.location = node.location; + const existing = this.symbolTables[0].get(variable.name); + if (!existing) { + throw new UndefinedReferenceError(reference); + } + // only variables can be reassigned (not functions or classes) + if (existing.symbolType !== SymbolType.VARIABLE) { + throw new InvalidSymbolTypeError(reference, SymbolType.VARIABLE); + } + if (existing.definition instanceof ConstantDefinitionNode) { + throw new ConstantModificationError(node, variable.name); + } + const definition = existing.definition as VariableDefinitionNode | undefined; + if (definition?.modifiers?.includes(Modifier.CONSTANT)) { + throw new ConstantModificationError(node, variable.name); + } + variable.type = existing.type; + existing.references.push(reference); + // The reassignment is now the variable's latest use, so codegen must not roll it off the + // stack at an earlier read (mirrors visitIdentifier's bookkeeping for plain assignments). + this.currentFunction.opRolls.set(variable.name, reference); + return; + } + const definition = createTupleVariableDefinition(node, variable); const { name } = variable; @@ -275,7 +319,9 @@ function createTupleVariableDefinition( node: TupleAssignmentNode, variable: TupleAssignmentTarget, ): VariableDefinitionNode { - const definition = new VariableDefinitionNode(variable.type, [], variable.name, node.tuple); + // Only declaration targets reach here (reassignment targets are handled before this is called), + // so `type` is always defined. + const definition = new VariableDefinitionNode(variable.type!, [], variable.name, node.tuple); definition.location = node.location; return definition; } diff --git a/packages/cashc/src/semantic/TypeCheckTraversal.ts b/packages/cashc/src/semantic/TypeCheckTraversal.ts index 8a5af6637..eaa7129d8 100644 --- a/packages/cashc/src/semantic/TypeCheckTraversal.ts +++ b/packages/cashc/src/semantic/TypeCheckTraversal.ts @@ -79,7 +79,9 @@ export default class TypeCheckTraversal extends AstTraversal { visitTupleAssignment(node: TupleAssignmentNode): Node { node.tuple = this.visit(node.tuple); - const targetsType = new TupleType(node.targets.map((target) => target.type)); + // target.type is resolved for every target by SymbolTableTraversal before this pass (declared + // targets carry their declared type; reassignment targets adopt the existing variable's type). + const targetsType = new TupleType(node.targets.map((target) => target.type!)); if (!implicitlyCastable(node.tuple.type, targetsType)) { const targetNames = node.targets.map((target) => target.name).join(', '); const syntheticAssignment = new VariableDefinitionNode(targetsType, [], targetNames, node.tuple); diff --git a/packages/cashc/src/stack-rescheduling.ts b/packages/cashc/src/stack-rescheduling.ts new file mode 100644 index 000000000..2fa4100f3 --- /dev/null +++ b/packages/cashc/src/stack-rescheduling.ts @@ -0,0 +1,1226 @@ +// Stack rescheduling: re-derive the evaluation schedule of straight-line code from its +// dataflow DAG so operands are on top of the stack when needed, instead of fetching every +// read from a variable slot with ` OP_PICK/OP_ROLL` pairs. +// +// The compiled script is split into basic blocks at control opcodes (IF/ELSE/ENDIF, +// BEGIN/UNTIL) and side-effecting checks (VERIFY-family, CLTV/CSV). Each block is lifted +// to a value DAG over its (opaque) entry stack slots, then re-emitted by a scheduler that +// walks the DAG in dependency order, choosing per operand between consuming moves +// (SWAP/ROT/ROLL), copies (DUP/OVER/PICK) and re-pushing constants. Because every block +// reproduces its exact entry->exit stack layout and the control opcodes are kept verbatim +// and in order, blocks compose and the transform is semantics-preserving by construction. +// Per block the compiler keeps min(original, rescheduled), so no block ever gets worse. +// +// The exit-layout guarantee alone does not cover DEAD COMPUTATION: a value computed but +// never reaching the block's exit. Script failure aborts the whole evaluation, so +// rejecting a spend is the only observable effect a dead node has — and the realistic +// instance is a VOID FUNCTION CALL, whose require() lives inside the OP_DEFINE'd body: +// the call site is an invoke node with zero outputs, dead by construction. The scheduler +// only emits nodes reachable from the exit, so a re-emitted block would silently delete +// the guard (and the well-formedness checks it performs on witness data), accepting +// spends the original rejects. Blocks containing dead computation are therefore never +// re-emitted: they keep their original ops verbatim (see hasDeadComputation), and any +// variant that cannot keep them (permuted entry layout or callee conventions) is +// discarded wholesale. +// +// Objectives (follows `optimizeFor`): +// 'size' — candidate schedules ranked by serialized bytes. +// 'opcost' — ranked by the BCH2026 op-cost meter (100 per evaluated instruction + the +// bytes it pushes; SWAP/ROT push 0, copies push the item, ROLL pushes +// item+depth, so e.g. re-pushing a 32-byte constant (~132) beats PICKing a +// stashed copy (~234)). Right for op-bound contracts whose unlocking scripts +// are zero-padded to buy op budget. +// +// OP_DEFINE'd function bodies are additionally validated by a differential test on a +// loosened VM (random input vectors; the rescheduled body must reproduce the original's +// output stack) and, under 'opcost', selected by MEASURED op-cost rather than the static +// estimate. The main routine cannot be executed standalone (it reads transaction context), +// so it relies on the per-block structural guarantee and static ranking. +// +// ENTRY-LAYOUT SEARCH: the pass also chooses, per OP_DEFINE'd function, the stack ORDER +// in which its arguments arrive — jointly with the schedule (the optimal order depends +// on the schedule and vice versa), including a small beam search over node orders. Call +// sites already stage arguments explicitly, so a permuted convention costs nothing extra +// there; every caller (and the main routine, when it invokes a permuted callee directly) +// is re-emitted to stage arguments in the callee's chosen order, and the plain-cashc +// variant of any block that invokes a permuted callee is invalid and excluded from the +// per-block min. If any validation fails, the pass falls back to identity layouts +// wholesale. +// +// This pass is opt-in (`rescheduleStacks`), runs after bytecode optimisation, and is +// restricted to single-function contracts (with a function selector, the entry stack +// depth differs per spend path, which this block model does not represent). Scripts and +// bodies containing console.log statements are left untouched (wholesale reordering +// cannot preserve per-instruction log data). Rescheduled regions keep coarse debug info: +// every emitted opcode of a rewritten block maps to the block's merged source span, and +// require() messages re-anchor to their (preserved) verify boundary. +import { + ConsensusBch2025, + binToHex, + createInstructionSetBch2026, + createTestAuthenticationProgramBch, + createVirtualMachine, + ripemd160, + secp256k1, + sha1, + sha256, + vmNumberToBigInt, +} from '@bitauth/libauth'; +import { + DebugFrame, + FullLocationData, + Op, + OpOrData, + OptimiseBytecodeResult, + PositionHint, + RequireStatement, + Script, + SingleLocationData, + SourceTagEntry, + bytecodeToScript, + calculateBytesize, + decodeInt, + encodeInt, + generateSourceMap, + scriptToBytecode, + sourceMapToLocationData, +} from '@cashscript/utils'; + +export interface FunctionArity { in: number; out: number } + +export interface RescheduleOptions { + arities: Map; // per OP_DEFINE'd functionId + mainInArity: number; // spend parameters + constructor parameters + objective: 'size' | 'opcost'; + constructorParamLength: number; +} + +export interface RescheduleOutcome { + result: OptimiseBytecodeResult; + frames: DebugFrame[]; +} + +// ---------- script-element helpers ---------- + +const elOpcode = (el: OpOrData): number => (typeof el === 'number' ? el : -1); + +// The constant a script element pushes, if it is a push. Small integers may be stored +// either as data (encodeInt) or as OP_0/OP_1..16/OP_1NEGATE opcode numbers (inserted by +// optimisation replacements) — normalise both to their byte content. +function constDataOf(el: OpOrData): Uint8Array | undefined { + if (el instanceof Uint8Array) return el; + if (el === Op.OP_0) return Uint8Array.of(); + if (el === Op.OP_1NEGATE) return Uint8Array.of(0x81); + if (el >= Op.OP_1 && el <= Op.OP_16) return Uint8Array.of(el - (Op.OP_1 - 1)); + return undefined; +} + +const constNumOf = (el: OpOrData): number | undefined => { + const data = constDataOf(el); + return data === undefined ? undefined : Number(decodeInt(data)); +}; + +// ---------- DAG model ---------- + +type Ref = + | { k: 'const'; data: Uint8Array } + | { k: 'in'; i: number } + | { k: 'ain'; i: number } + | { k: 'out'; node: DagNode; j: number }; + +interface DagNode { + id: number; + kind: 'prim' | 'invoke'; + code: number; // opcode for prim, functionId for invoke + ins: Ref[]; + nout: number; +} + +interface BasicBlock { + entryDepth: number; + entryAlt: number; + exit: Ref[]; + exitAlt: Ref[]; + nodes: DagNode[]; // every node created in this block, reachable from the exit or not + rawOps: Script; + rawStart: number; // element index range in the source script + rawEnd: number; // exclusive +} + +type Item = { block: BasicBlock } | { ctrl: number; index: number }; + +// value-producing ops that are pure within one transaction evaluation: opcode -> [in, out] +const VALOP = new Map([ + [Op.OP_1ADD, [1, 1]], [Op.OP_1SUB, [1, 1]], [Op.OP_NEGATE, [1, 1]], [Op.OP_ABS, [1, 1]], + [Op.OP_NOT, [1, 1]], [Op.OP_0NOTEQUAL, [1, 1]], + [Op.OP_ADD, [2, 1]], [Op.OP_SUB, [2, 1]], [Op.OP_MUL, [2, 1]], [Op.OP_DIV, [2, 1]], [Op.OP_MOD, [2, 1]], + [Op.OP_LSHIFTNUM, [2, 1]], [Op.OP_RSHIFTNUM, [2, 1]], + [Op.OP_BOOLAND, [2, 1]], [Op.OP_BOOLOR, [2, 1]], + [Op.OP_NUMEQUAL, [2, 1]], [Op.OP_NUMNOTEQUAL, [2, 1]], + [Op.OP_LESSTHAN, [2, 1]], [Op.OP_GREATERTHAN, [2, 1]], + [Op.OP_LESSTHANOREQUAL, [2, 1]], [Op.OP_GREATERTHANOREQUAL, [2, 1]], + [Op.OP_MIN, [2, 1]], [Op.OP_MAX, [2, 1]], [Op.OP_WITHIN, [3, 1]], + [Op.OP_CAT, [2, 1]], [Op.OP_SPLIT, [2, 2]], + [Op.OP_NUM2BIN, [2, 1]], [Op.OP_BIN2NUM, [1, 1]], [Op.OP_SIZE, [1, 2]], + [Op.OP_AND, [2, 1]], [Op.OP_OR, [2, 1]], [Op.OP_XOR, [2, 1]], [Op.OP_EQUAL, [2, 1]], + [Op.OP_REVERSEBYTES, [1, 1]], + [Op.OP_RIPEMD160, [1, 1]], [Op.OP_SHA1, [1, 1]], [Op.OP_SHA256, [1, 1]], + [Op.OP_HASH160, [1, 1]], [Op.OP_HASH256, [1, 1]], + [Op.OP_CHECKSIG, [2, 1]], [Op.OP_CHECKDATASIG, [3, 1]], + // introspection (constant within the evaluated transaction context) + [Op.OP_INPUTINDEX, [0, 1]], [Op.OP_ACTIVEBYTECODE, [0, 1]], + [Op.OP_TXVERSION, [0, 1]], [Op.OP_TXINPUTCOUNT, [0, 1]], [Op.OP_TXOUTPUTCOUNT, [0, 1]], [Op.OP_TXLOCKTIME, [0, 1]], + [Op.OP_UTXOVALUE, [1, 1]], [Op.OP_UTXOBYTECODE, [1, 1]], + [Op.OP_OUTPOINTTXHASH, [1, 1]], [Op.OP_OUTPOINTINDEX, [1, 1]], + [Op.OP_INPUTBYTECODE, [1, 1]], [Op.OP_INPUTSEQUENCENUMBER, [1, 1]], + [Op.OP_OUTPUTVALUE, [1, 1]], [Op.OP_OUTPUTBYTECODE, [1, 1]], + [Op.OP_UTXOTOKENCATEGORY, [1, 1]], [Op.OP_UTXOTOKENCOMMITMENT, [1, 1]], [Op.OP_UTXOTOKENAMOUNT, [1, 1]], + [Op.OP_OUTPUTTOKENCATEGORY, [1, 1]], [Op.OP_OUTPUTTOKENCOMMITMENT, [1, 1]], [Op.OP_OUTPUTTOKENAMOUNT, [1, 1]], +]); + +// control/side-effecting opcodes that bound basic blocks: opcode -> main-stack pops. +// (IF/NOTIF/UNTIL pop their condition; the VERIFY family consumes its operands; CLTV/CSV +// only peek. Emitted verbatim between blocks, so their relative order — and therefore +// which check fails first — is preserved.) +const CTRL = new Map([ + [Op.OP_IF, 1], [Op.OP_NOTIF, 1], [Op.OP_ELSE, 0], [Op.OP_ENDIF, 0], + [Op.OP_BEGIN, 0], [Op.OP_UNTIL, 1], + [Op.OP_VERIFY, 1], [Op.OP_EQUALVERIFY, 2], [Op.OP_NUMEQUALVERIFY, 2], + [Op.OP_CHECKSIGVERIFY, 2], [Op.OP_CHECKDATASIGVERIFY, 3], + [Op.OP_CHECKLOCKTIMEVERIFY, 0], [Op.OP_CHECKSEQUENCEVERIFY, 0], +]); + +let nodeCounter = 0; + +// Lift a script into [block | ctrl] items over a symbolic stack of `inArity` opaque +// entry slots. Throws on anything it cannot model (dynamic PICK depths, unknown opcodes, +// OP_DEPTH, ...) — callers treat a throw as "keep the original". +function decompile(script: Script, arities: Map, inArity: number): Item[] { + const items: Item[] = []; + let main: Ref[] = Array.from({ length: inArity }, (_, i) => ({ k: 'in', i } as Ref)); + let alt: Ref[] = []; + let entryDepth = inArity; + let entryAlt = 0; + let rawStart = 0; + let blockNodes: DagNode[] = []; + const ctrlDepth: { m: number; a: number }[] = []; + + const beginBlock = (): void => { + entryDepth = main.length; + entryAlt = alt.length; + main = main.map((_, i) => ({ k: 'in', i } as Ref)); + alt = alt.map((_, i) => ({ k: 'ain', i } as Ref)); + }; + const closeBlock = (end: number): void => { + items.push({ + block: { + entryDepth, + entryAlt, + exit: [...main], + exitAlt: [...alt], + nodes: blockNodes, + rawOps: script.slice(rawStart, end), + rawStart, + rawEnd: end, + }, + }); + blockNodes = []; + }; + const pop = (): Ref => { + const ref = main.pop(); + if (ref === undefined) throw new Error('stack underflow while decompiling'); + return ref; + }; + const popConstNum = (): number => { + const ref = pop(); + if (ref.k !== 'const') throw new Error('dynamic operand for PICK/ROLL/INVOKE'); + return Number(decodeInt(ref.data)); + }; + + for (let i = 0; i < script.length; i += 1) { + const el = script[i]; + const op = elOpcode(el); + + if (CTRL.has(op)) { + closeBlock(i); + if (op === Op.OP_IF || op === Op.OP_NOTIF) { + pop(); + ctrlDepth.push({ m: main.length, a: alt.length }); + } else if (op === Op.OP_ELSE) { + const s = ctrlDepth[ctrlDepth.length - 1]; + main.length = s.m; + alt.length = s.a; + } else if (op === Op.OP_ENDIF) { + ctrlDepth.pop(); + } else { + for (let k = 0; k < CTRL.get(op)!; k += 1) pop(); + } + items.push({ ctrl: op, index: i }); + rawStart = i + 1; + beginBlock(); + continue; + } + + const data = constDataOf(el); + if (data !== undefined) { + main.push({ k: 'const', data }); + continue; + } + + switch (op) { + case Op.OP_DUP: main.push(main[main.length - 1]); break; + case Op.OP_DROP: pop(); break; + case Op.OP_2DROP: pop(); pop(); break; + case Op.OP_2DUP: { const b = main[main.length - 1]; const a = main[main.length - 2]; main.push(a, b); break; } + case Op.OP_3DUP: { + const c = main[main.length - 1]; const b = main[main.length - 2]; const a = main[main.length - 3]; + main.push(a, b, c); break; + } + case Op.OP_OVER: main.push(main[main.length - 2]); break; + case Op.OP_2OVER: { const b = main[main.length - 3]; const a = main[main.length - 4]; main.push(a, b); break; } + case Op.OP_SWAP: { const n = main.length; [main[n - 1], main[n - 2]] = [main[n - 2], main[n - 1]]; break; } + case Op.OP_2SWAP: { + const n = main.length; const a = main[n - 4]; const b = main[n - 3]; + main[n - 4] = main[n - 2]; main[n - 3] = main[n - 1]; main[n - 2] = a; main[n - 1] = b; break; + } + case Op.OP_ROT: { + const n = main.length; const a = main[n - 3]; + main[n - 3] = main[n - 2]; main[n - 2] = main[n - 1]; main[n - 1] = a; break; + } + case Op.OP_2ROT: { + const n = main.length; const a = main[n - 6]; const b = main[n - 5]; + for (let k = n - 6; k < n - 2; k += 1) main[k] = main[k + 2]; + main[n - 2] = a; main[n - 1] = b; break; + } + case Op.OP_TUCK: { + const n = main.length; const b = main[n - 1]; const a = main[n - 2]; + main[n - 2] = b; main[n - 1] = a; main.push(b); break; + } + case Op.OP_NIP: main.splice(main.length - 2, 1); break; + case Op.OP_TOALTSTACK: alt.push(pop()); break; + case Op.OP_FROMALTSTACK: { const ref = alt.pop(); if (ref === undefined) throw new Error('alt underflow'); main.push(ref); break; } + case Op.OP_PICK: { + const n = popConstNum(); + const picked = main[main.length - 1 - n]; + if (picked === undefined) throw new Error('PICK past stack bottom'); + main.push(picked); break; + } + case Op.OP_ROLL: { + const n = popConstNum(); + const idx = main.length - 1 - n; + if (idx < 0) throw new Error('ROLL past stack bottom'); + const [v] = main.splice(idx, 1); + main.push(v); break; + } + case Op.OP_INVOKE: { + const id = popConstNum(); + const arity = arities.get(id); + if (arity === undefined) throw new Error(`unknown function id ${id}`); + const ins: Ref[] = []; + for (let k = 0; k < arity.in; k += 1) ins.unshift(pop()); + nodeCounter += 1; + const node: DagNode = { id: nodeCounter, kind: 'invoke', code: id, ins, nout: arity.out }; + blockNodes.push(node); + for (let j = 0; j < arity.out; j += 1) main.push({ k: 'out', node, j }); + break; + } + default: { + const valop = VALOP.get(op); + if (valop === undefined) throw new Error(`unsupported opcode 0x${op.toString(16)}`); + const [nin, nout] = valop; + const ins: Ref[] = []; + for (let k = 0; k < nin; k += 1) ins.unshift(pop()); + nodeCounter += 1; + const node: DagNode = { id: nodeCounter, kind: 'prim', code: op, ins, nout }; + blockNodes.push(node); + for (let j = 0; j < nout; j += 1) main.push({ k: 'out', node, j }); + break; + } + } + } + closeBlock(script.length); + return items; +} + +// ---------- scheduler ---------- + +type Strategy = 'topo' | 'greedy' | 'opcost' | 'beam'; + +const keyOf = (ref: Ref): string | undefined => { + if (ref.k === 'in') return `m${ref.i}`; + if (ref.k === 'ain') return `a${ref.i}`; + if (ref.k === 'out') return `n${ref.node.id}_${ref.j}`; + return undefined; +}; + +// nominal stack-item length for op-cost estimates (item sizes are unknown statically) +const NOMINAL_LEN = 33; + +// incremental per-element op-cost, mirroring opCostEstimate below +function costOfAppend(prev: OpOrData | undefined, el: OpOrData): number { + const data = constDataOf(el); + if (data !== undefined) return 100 + data.length; + switch (elOpcode(el)) { + case Op.OP_DUP: case Op.OP_OVER: case Op.OP_TUCK: case Op.OP_FROMALTSTACK: case Op.OP_PICK: + return 100 + NOMINAL_LEN; + case Op.OP_ROLL: return 100 + NOMINAL_LEN + (prev !== undefined ? (constNumOf(prev) ?? 0) : 0); + case Op.OP_2DUP: case Op.OP_2OVER: case Op.OP_2ROT: return 100 + 2 * NOMINAL_LEN; + case Op.OP_3DUP: return 100 + 3 * NOMINAL_LEN; + default: return 100; + } +} + +// mutable emission state; cloneable for the beam search +interface EmitState { + out: Script; + stk: string[]; + useCount: Map; + remaining: Set; + cost: number; // accumulated static op-cost of `out` +} + +const cloneState = (s: EmitState): EmitState => ({ + out: [...s.out], stk: [...s.stk], useCount: new Map(s.useCount), remaining: new Set(s.remaining), cost: s.cost, +}); + +const appendOp = (s: EmitState, el: OpOrData): void => { + s.cost += costOfAppend(s.out[s.out.length - 1], el); + s.out.push(el); +}; + +const topmostIndex = (stk: string[], key: string): number => stk.lastIndexOf(key); +const deepestIndex = (stk: string[], key: string): number => stk.indexOf(key); +const pushNum = (s: EmitState, value: number): void => { appendOp(s, encodeInt(BigInt(value))); }; + +// bring `key` to the top of the stack: a copy targets the shallowest occurrence +// (cheapest); a consuming move targets the deepest (original) so freshly staged copies +// above it are preserved +function bring(s: EmitState, key: string, move: boolean): void { + const idx = move ? deepestIndex(s.stk, key) : topmostIndex(s.stk, key); + if (idx < 0) throw new Error(`value not on stack: ${key}`); + const depth = s.stk.length - 1 - idx; + if (move) { + if (depth === 1) appendOp(s, Op.OP_SWAP); + else if (depth === 2) appendOp(s, Op.OP_ROT); + else if (depth > 2) { pushNum(s, depth); appendOp(s, Op.OP_ROLL); } + s.stk.splice(idx, 1); + s.stk.push(key); + } else { + if (depth === 0) appendOp(s, Op.OP_DUP); + else if (depth === 1) appendOp(s, Op.OP_OVER); + else { pushNum(s, depth); appendOp(s, Op.OP_PICK); } + s.stk.push(key); + } +} + +function bringRef(s: EmitState, ref: Ref, consumeContext: boolean, exitNeed: Map): void { + if (ref.k === 'const') { appendOp(s, ref.data); s.stk.push('#k'); return; } + const key = keyOf(ref)!; + if (consumeContext) { + const remNode = s.useCount.get(key) ?? 0; // node-input uses remaining (incl. this one) + const survives = remNode > 1 || (exitNeed.get(key) ?? 0) > 0; + bring(s, key, !survives); + s.useCount.set(key, remNode - 1); + } else { + const remExit = exitNeed.get(key) ?? 0; + bring(s, key, !(remExit > 1)); + exitNeed.set(key, remExit - 1); + } +} + +// compute one DAG node on top of the stack, staging arguments in the callee's chosen +// entry order for permuted OP_INVOKE targets +function computeNode(s: EmitState, node: DagNode, exitNeed: Map, calleePerms: Map): void { + s.remaining.delete(node.id); + const perm = node.kind === 'invoke' ? calleePerms.get(node.code) : undefined; + const staged = perm !== undefined ? perm.map((i) => node.ins[i]) : node.ins; + staged.forEach((r) => bringRef(s, r, true, exitNeed)); + if (node.kind === 'invoke') { pushNum(s, node.code); appendOp(s, Op.OP_INVOKE); } else appendOp(s, node.code); + s.stk.length -= node.ins.length; + for (let j = 0; j < node.nout; j += 1) s.stk.push(`n${node.id}_${j}`); +} + +interface EmitOptions { + entrySeed?: string[]; // stack labels at block entry, bottom -> top (defaults to m0..mN-1) + calleePerms?: Map; +} + +const NO_PERMS: Map = new Map(); +const BEAM_WIDTH = 4; +const BEAM_EXPAND = 3; + +// Re-emit one block: seed the entry slots, compute the DAG nodes in a strategy-chosen +// dependency order (fetching each operand with the cheapest available move/copy), then +// assemble the exact exit layout and clean leftover slots. +function emitBlock(block: BasicBlock, strategy: Strategy, options: EmitOptions = {}): Script { + const { entryDepth: n, entryAlt: p, exit, exitAlt } = block; + const calleePerms = options.calleePerms ?? NO_PERMS; + + // topo order of needed nodes (memoised — the DAGs share subtrees heavily) + const visited = new Set(); + const order: DagNode[] = []; + const visitNode = (node: DagNode): void => { + if (visited.has(node.id)) return; + visited.add(node.id); + node.ins.forEach((ref) => { if (ref.k === 'out') visitNode(ref.node); }); + order.push(node); + }; + [...exit, ...exitAlt].forEach((ref) => { if (ref.k === 'out') visitNode(ref.node); }); + + // altstack passthrough: if the entry altstack is returned untouched and never read, + // leave it there (emit zero alt ops) + const altReferenced = exit.some((r) => r.k === 'ain') || order.some((nd) => nd.ins.some((r) => r.k === 'ain')); + const exitAltPass = exitAlt.length === p && exitAlt.every((r, i) => r.k === 'ain' && r.i === i); + const altPass = exitAltPass && !altReferenced; + + const useCount = new Map(); + const exitNeed = new Map(); + const bump = (map: Map, key: string | undefined): void => { + if (key !== undefined) map.set(key, (map.get(key) ?? 0) + 1); + }; + order.forEach((nd) => nd.ins.forEach((r) => bump(useCount, keyOf(r)))); + [...exit, ...exitAlt].forEach((r) => bump(exitNeed, keyOf(r))); + + const initial: EmitState = { + out: [], stk: [], useCount, remaining: new Set(order.map((nd) => nd.id)), cost: 0, + }; + for (let i = 0; i < n; i += 1) initial.stk.push(options.entrySeed?.[i] ?? `m${i}`); + if (!altPass) for (let i = 0; i < p; i += 1) { appendOp(initial, Op.OP_FROMALTSTACK); initial.stk.push(`a${p - 1 - i}`); } + + // fetch-cost heuristics for choosing the next ready node + const pushBytesForDepth = (depth: number): number => (depth <= 16 ? 1 : 2); + const fetchCostBytes = (s: EmitState, nd: DagNode): number => nd.ins.reduce((sum, r) => { + if (r.k === 'const') return sum + Math.max(1, r.data.length); + const key = keyOf(r)!; + const idx = topmostIndex(s.stk, key); + if (idx < 0) return sum; + const depth = s.stk.length - 1 - idx; + const lastUse = (s.useCount.get(key) ?? 0) <= 1 && (exitNeed.get(key) ?? 0) === 0; + if (lastUse) return sum + (depth === 0 ? 0 : depth <= 2 ? 1 : pushBytesForDepth(depth) + 1); + return sum + (depth <= 1 ? 1 : pushBytesForDepth(depth) + 1); + }, 0); + const fetchCostOp = (s: EmitState, nd: DagNode): number => nd.ins.reduce((sum, r) => { + if (r.k === 'const') return sum + 100 + r.data.length; + const key = keyOf(r)!; + const idx = topmostIndex(s.stk, key); + if (idx < 0) return sum; + const depth = s.stk.length - 1 - idx; + const lastUse = (s.useCount.get(key) ?? 0) <= 1 && (exitNeed.get(key) ?? 0) === 0; + if (lastUse) return sum + (depth === 0 ? 0 : depth <= 2 ? 100 : 201 + NOMINAL_LEN + depth); + return sum + (depth <= 1 ? 100 + NOMINAL_LEN : 201 + NOMINAL_LEN); + }, 0); + + const inputNodeIds = (nd: DagNode): number[] => nd.ins.flatMap((r) => (r.k === 'out' ? [r.node.id] : [])); + const readyNodes = (s: EmitState): DagNode[] => order.filter( + (nd) => s.remaining.has(nd.id) && inputNodeIds(nd).every((id) => !s.remaining.has(id)), + ); + + // assemble the exit layout on top (exit-main ++ reverse(exit-alt) unless alt passes + // through), clean buried junk, and restore the altstack + const finish = (s: EmitState): void => { + const need = new Map(exitNeed); + const desired = altPass ? [...exit] : [...exit, ...[...exitAlt].reverse()]; + const desiredKeys = desired.map((r) => (r.k === 'const' ? undefined : keyOf(r))); + const inPlace = s.stk.length >= desired.length + && desiredKeys.every((dk, i) => dk !== undefined && s.stk[s.stk.length - desired.length + i] === dk); + if (!inPlace) desired.forEach((r) => bringRef(s, r, false, need)); + + const K = desired.length; + const junk = s.stk.length - K; + if (junk > 0) { + if (2 * K + junk <= 4 * junk) { + for (let i = 0; i < K; i += 1) appendOp(s, Op.OP_TOALTSTACK); + for (let i = 0; i < junk; i += 1) appendOp(s, Op.OP_DROP); + for (let i = 0; i < K; i += 1) appendOp(s, Op.OP_FROMALTSTACK); + s.stk = s.stk.slice(s.stk.length - K); + } else { + for (let i = 0; i < junk; i += 1) { + pushNum(s, s.stk.length - 1); appendOp(s, Op.OP_ROLL); appendOp(s, Op.OP_DROP); s.stk.splice(0, 1); + } + } + } + if (!altPass) for (let i = 0; i < exitAlt.length; i += 1) appendOp(s, Op.OP_TOALTSTACK); + }; + + if (strategy !== 'beam') { + const state = initial; + while (state.remaining.size > 0) { + const candidates = readyNodes(state); + let node = candidates[0]; + if (strategy !== 'topo') { + const cost = strategy === 'opcost' ? fetchCostOp : fetchCostBytes; + let best = cost(state, candidates[0]); + candidates.forEach((candidate) => { + const c = cost(state, candidate); + if (c < best) { best = c; node = candidate; } + }); + } + computeNode(state, node, exitNeed, calleePerms); + } + finish(state); + return state.out; + } + + // beam search: expand each surviving state by its BEAM_EXPAND cheapest ready nodes, + // keep the BEAM_WIDTH cheapest accumulated schedules (all states have computed the + // same number of nodes, so accumulated cost is comparable) + let beam: EmitState[] = [initial]; + for (let step = 0; step < order.length; step += 1) { + const next: EmitState[] = []; + beam.forEach((state) => { + const candidates = readyNodes(state) + .map((nd) => ({ nd, c: fetchCostOp(state, nd) })) + .sort((a, b) => a.c - b.c) + .slice(0, BEAM_EXPAND); + candidates.forEach(({ nd }) => { + const branch = cloneState(state); + computeNode(branch, nd, exitNeed, calleePerms); + next.push(branch); + }); + }); + next.sort((a, b) => a.cost - b.cost); + beam = next.slice(0, BEAM_WIDTH); + } + let best: Script | undefined; + let bestCost = Infinity; + beam.forEach((state) => { + finish(state); + if (state.cost < bestCost) { bestCost = state.cost; best = state.out; } + }); + // A dead-ended beam means no complete schedule was found; initial.out would be a script that + // computed nothing, so fail closed — every caller catches and keeps the original body. + if (best === undefined) throw new Error('beam search dead-ended without a complete schedule'); + return best; +} + +// collapse adjacent single-item stack ops into their multi-item forms (all provably +// stack-equivalent; the main optimiser applies the same families of rewrites) +function peephole(script: Script): Script { + const isN = (el: OpOrData | undefined, value: number): boolean => ( + el !== undefined && constNumOf(el) === value && constDataOf(el)!.length === 1 + ); + const isOp = (el: OpOrData | undefined, op: number): boolean => el !== undefined && typeof el === 'number' && el === op; + let ops = script; + let changed = true; + while (changed) { + changed = false; + const out: Script = []; + for (let i = 0; i < ops.length; i += 1) { + const [a, b, c, d, e, f] = [ops[i], ops[i + 1], ops[i + 2], ops[i + 3], ops[i + 4], ops[i + 5]]; + if (isN(a, 2) && isOp(b, Op.OP_PICK) && isN(c, 2) && isOp(d, Op.OP_PICK) && isN(e, 2) && isOp(f, Op.OP_PICK)) { + out.push(Op.OP_3DUP); i += 5; changed = true; continue; + } + if (isOp(a, Op.OP_OVER) && isOp(b, Op.OP_OVER)) { out.push(Op.OP_2DUP); i += 1; changed = true; continue; } + if (isOp(a, Op.OP_SWAP) && isOp(b, Op.OP_OVER)) { out.push(Op.OP_TUCK); i += 1; changed = true; continue; } + if (isOp(a, Op.OP_SWAP) && isOp(b, Op.OP_DROP)) { out.push(Op.OP_NIP); i += 1; changed = true; continue; } + if (isN(a, 3) && isOp(b, Op.OP_PICK) && isN(c, 3) && isOp(d, Op.OP_PICK)) { + out.push(Op.OP_2OVER); i += 3; changed = true; continue; + } + if (isN(a, 3) && isOp(b, Op.OP_ROLL) && isN(c, 3) && isOp(d, Op.OP_ROLL)) { + out.push(Op.OP_2SWAP); i += 3; changed = true; continue; + } + if (isN(a, 5) && isOp(b, Op.OP_ROLL) && isN(c, 5) && isOp(d, Op.OP_ROLL)) { + out.push(Op.OP_2ROT); i += 3; changed = true; continue; + } + out.push(a); + } + ops = out; + } + return ops; +} + +// ---------- cost models ---------- + +// Static estimate of the schedule-dependent part of the BCH2026 op-cost meter. Value ops' +// own result pushes are identical across schedules of the same DAG and are left out; this +// RANKS schedules, it does not predict absolute cost. +export function opCostEstimate(script: Script): number { + let cost = 0; + for (let i = 0; i < script.length; i += 1) { + cost += costOfAppend(i > 0 ? script[i - 1] : undefined, script[i]); + } + return cost; +} + +const scriptCost = (script: Script, objective: 'size' | 'opcost'): number => ( + // redeem bytes still cost 1 op each in the scriptSig push, which doubles as a tiebreak + objective === 'opcost' ? opCostEstimate(script) + calculateBytesize(script) : calculateBytesize(script) +); + +// ---------- per-script rescheduling ---------- + +interface ItemMapping { + oldStart: number; + oldEnd: number; // exclusive + newStart: number; + newEnd: number; // exclusive + rewritten: boolean; +} + +interface RecompiledScript { + script: Script; + mapping: ItemMapping[]; + changed: boolean; +} + +interface RecompileOptions { + entryPerm?: number[]; // permuted entry layout of the FIRST block (bottom -> top holds slot perm[j]) + calleePerms?: Map; +} + +const isIdentityPerm = (perm: number[] | undefined): boolean => perm === undefined || perm.every((v, i) => v === i); + +// over ALL block nodes (not just exit-reachable ones): a dead invoke of a permuted callee +// still stages its arguments in the raw ops, which would be the wrong order +const blockInvokesPermuted = (block: BasicBlock, calleePerms: Map): boolean => ( + calleePerms.size > 0 && block.nodes.some((nd) => nd.kind === 'invoke' && calleePerms.has(nd.code)) +); + +// A node unreachable from the block's exit stacks is dead computation: its result never +// leaves the block. Since script failure aborts evaluation, rejecting the spend is the +// only observable effect such a node has — deleting it widens the set of accepted +// witnesses. The realistic case is a void function call (its require lives in the callee, +// so the call site is a zero-output invoke node, dead by construction); an `unused` +// variable's failure-capable initializer (e.g. a division) is the same hole. Blocks +// containing dead computation keep their original ops verbatim. +const hasDeadComputation = (block: BasicBlock): boolean => { + if (block.nodes.length === 0) return false; + const reachable = new Set(); + const visitNode = (node: DagNode): void => { + if (reachable.has(node.id)) return; + reachable.add(node.id); + node.ins.forEach((ref) => { if (ref.k === 'out') visitNode(ref.node); }); + }; + [...block.exit, ...block.exitAlt].forEach((ref) => { if (ref.k === 'out') visitNode(ref.node); }); + return block.nodes.some((node) => !reachable.has(node.id)); +}; + +function recompileScript( + script: Script, + arities: Map, + inArity: number, + strategy: Strategy, + objective: 'size' | 'opcost', + options: RecompileOptions = {}, +): RecompiledScript { + const calleePerms = options.calleePerms ?? NO_PERMS; + const items = decompile(script, arities, inArity); + const out: Script = []; + const mapping: ItemMapping[] = []; + let changed = false; + let firstBlock = true; + items.forEach((item) => { + if ('ctrl' in item) { + mapping.push({ + oldStart: item.index, oldEnd: item.index + 1, newStart: out.length, newEnd: out.length + 1, rewritten: false, + }); + out.push(item.ctrl); + return; + } + const { block } = item; + const entryPerm = firstBlock ? options.entryPerm : undefined; + const entrySeed = entryPerm !== undefined ? entryPerm.map((slot) => `m${slot}`) : undefined; + firstBlock = false; + // the plain-cashc block is only a valid candidate under the standard entry layout + // and standard callee conventions + const rawValid = isIdentityPerm(entryPerm) && !blockInvokesPermuted(block, calleePerms); + // a block with dead computation has no valid re-emission (see hasDeadComputation); + // if the original ops are not valid either, this whole variant is unusable + const dead = hasDeadComputation(block); + if (dead && !rawValid) throw new Error('dead computation in a block that cannot keep its original ops'); + // per-block min(original, rescheduled): both reproduce the block's entry->exit + // transform AND its boundary layout, so they compose freely + const mine = dead ? undefined : peephole(emitBlock(block, strategy, { entrySeed, calleePerms })); + const useMine = mine !== undefined + && (!rawValid || scriptCost(mine, objective) < scriptCost(block.rawOps, objective)); + const chosen = useMine ? mine! : block.rawOps; + mapping.push({ + oldStart: block.rawStart, + oldEnd: block.rawEnd, + newStart: out.length, + newEnd: out.length + chosen.length, + rewritten: useMine, + }); + chosen.forEach((el) => out.push(el)); + if (useMine) changed = true; + }); + return { script: out, mapping, changed }; +} + +// ---------- OP_DEFINE table dissection ---------- + +interface Dissected { + order: number[]; + bodies: Map; + tableLength: number; // element count of the define table prefix + main: Script; +} + +function dissect(script: Script): Dissected { + const order: number[] = []; + const bodies = new Map(); + let i = 0; + while (i + 2 < script.length && script[i] instanceof Uint8Array && elOpcode(script[i + 2]) === Op.OP_DEFINE) { + const id = constNumOf(script[i + 1]); + if (id === undefined) break; + bodies.set(id, bytecodeToScript(script[i] as Uint8Array)); + order.push(id); + i += 3; + } + return { order, bodies, tableLength: i, main: script.slice(i) }; +} + +// function ids a body invokes (directly) +function bodyInvokes(body: Script): number[] { + const ids: number[] = []; + for (let i = 1; i < body.length; i += 1) { + if (elOpcode(body[i]) === Op.OP_INVOKE) { + const id = constNumOf(body[i - 1]); + if (id !== undefined) ids.push(id); + } + } + return ids; +} + +// An OP_INVOKE whose callee id is not the immediately preceding constant is invisible to +// bodyInvokes, so a permuted callee would silently receive its arguments in the old order at +// such a call site — layout experiments must not run when one exists anywhere in the program. +const hasAmbiguousInvoke = (script: Script): boolean => script.some((el, i) => ( + elOpcode(el) === Op.OP_INVOKE && (i === 0 || constNumOf(script[i - 1]) === undefined) +)); + +// callee-first processing order over the call graph (declaration order on cycles) +function calleeFirstOrder(d: Dissected): number[] { + const visited = new Set(); + const out: number[] = []; + const visit = (id: number): void => { + if (visited.has(id) || !d.bodies.has(id)) return; + visited.add(id); + bodyInvokes(d.bodies.get(id)!).forEach(visit); + out.push(id); + }; + d.order.forEach(visit); + return out; +} + +// ---------- differential testing + measurement on a loosened VM ---------- + +// Loosened BCH2026 VM: lifts size/op-cost/stack caps so any single function body can run +// to completion on random inputs regardless of contract-level limits. +/* eslint-disable @typescript-eslint/no-explicit-any */ +const loosenedConsensus: any = { + ...ConsensusBch2025, + baseInstructionCost: 100, + maximumFunctionIdentifierLength: 7, + maximumMemorySlots: Number.MAX_SAFE_INTEGER, + maximumStandardLockingBytecodeLength: -1, + maximumStandardUnlockingBytecodeLength: Number.MAX_SAFE_INTEGER, + maximumTokenCommitmentLength: 128, + operationCostBudgetPerByte: Number.MAX_SAFE_INTEGER, + maximumStackItemLength: Number.MAX_SAFE_INTEGER, + maximumVmNumberByteLength: Number.MAX_SAFE_INTEGER, + maximumStackDepth: Number.MAX_SAFE_INTEGER, + maximumControlStackDepth: Number.MAX_SAFE_INTEGER, + maximumBytecodeLength: Number.MAX_SAFE_INTEGER, + maximumOperationCount: Number.MAX_SAFE_INTEGER, +}; + +let cachedVm: any; +const looseVm = (): any => { + cachedVm ??= createVirtualMachine(createInstructionSetBch2026(false, { + consensus: loosenedConsensus, ripemd160, secp256k1, sha1, sha256, + } as any)); + return cachedVm; +}; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +// deterministic pseudo-random test inputs (~254-bit values, matching wide int math) +const RND_MOD = 21888242871839275222246405745257275088696311157297823662689037894645226208583n; +const rnd = (seed: number): bigint => { + let x = BigInt(seed + 7); + for (let i = 0; i < 6; i += 1) x = (x * 6364136223846793005n + 1442695040888963407n) % RND_MOD; + return x; +}; + +interface BodyRun { stack?: string[]; opCost: number; error?: string } + +// run one function on the loosened VM. `inputPerm` stages the inputs in the target's +// (possibly permuted) entry order: stack position j receives inputs[perm[j]]. +function runBody( + d: Dissected, overrides: Map, targetId: number, inputs: bigint[], inputPerm?: number[], +): BodyRun { + const program: Script = []; + d.order.forEach((id) => { + program.push(scriptToBytecode(overrides.get(id) ?? d.bodies.get(id)!), encodeInt(BigInt(id)), Op.OP_DEFINE); + }); + const staged = inputPerm !== undefined ? inputPerm.map((i) => inputs[i]) : inputs; + staged.forEach((input) => program.push(encodeInt(input))); + program.push(encodeInt(BigInt(targetId)), Op.OP_INVOKE); + const state = looseVm().evaluate(createTestAuthenticationProgramBch({ + lockingBytecode: scriptToBytecode(program), unlockingBytecode: Uint8Array.of(), valueSatoshis: 1000n, + })); + // the only expected "error" is the benign post-evaluation clean-stack check + const benign = state.error === undefined || /Non-P2SH|clean stack|exactly one|single/i.test(String(state.error)); + return { + stack: benign + ? state.stack.map((item: Uint8Array) => ( + vmNumberToBigInt(item, { maximumVmNumberByteLength: Number.MAX_SAFE_INTEGER as never }).toString() + )) + : undefined, + opCost: Number(state.metrics.operationCost), + error: benign ? undefined : String(state.error), + }; +} + +// The candidate (with all chosen overrides and its entry permutation) must reproduce the +// ORIGINAL body's output stack — original bodies all the way down, standard input order — +// on K random vectors. +function bodyEquiv( + d: Dissected, overrides: Map, id: number, + candidate: Script, arity: FunctionArity, perm: number[] | undefined, K = 3, +): boolean { + for (let t = 0; t < K; t += 1) { + const inputs = Array.from({ length: arity.in }, (_, i) => rnd(i * 17 + t * 1009 + id)); + const reference = runBody(d, new Map(), id, inputs); + const candidateRun = runBody(d, new Map([...overrides, [id, candidate]]), id, inputs, perm); + if (reference.stack === undefined || candidateRun.stack === undefined) return false; + if (reference.stack.join() !== candidateRun.stack.join()) return false; + } + return true; +} + +// summed measured op-cost of a body over K fixed random vectors (Infinity on any error) +function measureBody( + d: Dissected, overrides: Map, id: number, + arity: FunctionArity, perm: number[] | undefined, K = 3, +): number { + let total = 0; + for (let t = 0; t < K; t += 1) { + const inputs = Array.from({ length: arity.in }, (_, i) => rnd(i * 23 + t * 811 + id)); + const run = runBody(d, overrides, id, inputs, perm); + if (run.error !== undefined || run.stack === undefined) return Infinity; + total += run.opCost; + } + return total; +} + +// ---------- entry-layout candidates ---------- + +// Candidate argument orders for a body, derived from its first block's DAG: identity, +// reverse, earliest-first-use nearest the top, and most-used nearest the top. A `perm` +// maps stack position j (bottom -> top) to the original entry slot held there. +function layoutCandidates(body: Script, arities: Map, inArity: number): number[][] { + const identity = Array.from({ length: inArity }, (_, i) => i); + if (inArity < 2) return [identity]; + let items: Item[]; + try { items = decompile(body, arities, inArity); } catch { return [identity]; } + const first = items.find((item): item is { block: BasicBlock } => 'block' in item); + if (first === undefined) return [identity]; + + const visited = new Set(); + const order: DagNode[] = []; + const visitNode = (node: DagNode): void => { + if (visited.has(node.id)) return; + visited.add(node.id); + node.ins.forEach((ref) => { if (ref.k === 'out') visitNode(ref.node); }); + order.push(node); + }; + [...first.block.exit, ...first.block.exitAlt].forEach((ref) => { if (ref.k === 'out') visitNode(ref.node); }); + + const firstUse = new Array(inArity).fill(Number.MAX_SAFE_INTEGER); + const useCount = new Array(inArity).fill(0); + order.forEach((nd, t) => nd.ins.forEach((r) => { + if (r.k !== 'in' || r.i >= inArity) return; + firstUse[r.i] = Math.min(firstUse[r.i], t); + useCount[r.i] += 1; + })); + + // stable sorts; the deepest position gets the FIRST element of the sorted list + const byFirstUseDesc = [...identity].sort((a, b) => (firstUse[b] - firstUse[a]) || (a - b)); + const byUseCountAsc = [...identity].sort((a, b) => (useCount[a] - useCount[b]) || (a - b)); + const reverse = [...identity].reverse(); + + const seen = new Set(); + return [identity, byFirstUseDesc, byUseCountAsc, reverse].filter((perm) => { + const key = perm.join(','); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +// ---------- body selection ---------- + +const BODY_STRATEGIES: Strategy[] = ['topo', 'greedy', 'opcost']; +const MAIN_STRATEGIES: Strategy[] = ['topo', 'greedy', 'opcost']; +// adopt a non-identity entry layout only if it beats the best identity variant by >1% +// (its call-site staging costs are not visible in the body-level measurement) +const LAYOUT_HYSTERESIS = 1.01; + +interface BodyChoices { + overrides: Map; + perms: Map; +} + +// Reschedule every OP_DEFINE'd body; keep, per body, the best diff-test-equivalent +// candidate ('size': smallest bytes; 'opcost': smallest MEASURED op-cost). With +// `layouts`, entry-order permutations and a beam schedule join the candidate set, and +// bodies are processed callee-first so callers re-stage for their callees' chosen +// orders. Throws if a body whose callee was permuted has no valid replacement (the +// caller falls back to identity layouts wholesale). +function rescheduleBodies( + d: Dissected, arities: Map, objective: 'size' | 'opcost', layouts: boolean, +): BodyChoices { + const overrides = new Map(); + const perms = new Map(); + const strategies: Strategy[] = layouts ? [...BODY_STRATEGIES, 'beam'] : BODY_STRATEGIES; + const processing = layouts ? calleeFirstOrder(d) : d.order; + + processing.forEach((id) => { + const arity = arities.get(id); + const original = d.bodies.get(id)!; + const calleesPermuted = bodyInvokes(original).some((callee) => perms.has(callee)); + if (arity === undefined) { + if (calleesPermuted) throw new Error(`body ${id} invokes a permuted callee but has no arity`); + return; + } + const bodyCost = (candidate: Script, perm: number[] | undefined): number => ( + objective === 'opcost' + ? measureBody(d, new Map([...overrides, [id, candidate]]), id, arity, perm) + : calculateBytesize(candidate) + ); + + let best: Script | undefined; + let bestPerm: number[] | undefined; + let bestCost = Infinity; + // baseline: the untouched cashc body (only valid while its callees are unpermuted) + if (!calleesPermuted) { + bestCost = objective === 'opcost' ? measureBody(d, overrides, id, arity, undefined) : calculateBytesize(original); + } + + const candidatesPerms = layouts ? layoutCandidates(original, arities, arity.in) : [undefined]; + candidatesPerms.forEach((perm) => { + const identity = isIdentityPerm(perm as number[] | undefined); + strategies.forEach((strategy) => { + let candidate: Script; + try { + candidate = recompileScript(original, arities, arity.in, strategy, objective, { + entryPerm: perm as number[] | undefined, calleePerms: perms, + }).script; + } catch { return; } + const cost = bodyCost(candidate, identity ? undefined : (perm as number[])); + const effective = identity ? cost : cost * LAYOUT_HYSTERESIS; + const effectivePerm = identity ? undefined : (perm as number[]); + if (effective < bestCost && bodyEquiv(d, overrides, id, candidate, arity, effectivePerm)) { + best = candidate; + bestPerm = identity ? undefined : (perm as number[]); + bestCost = effective; + } + }); + }); + + if (best !== undefined) { + overrides.set(id, best); + if (bestPerm !== undefined) perms.set(id, bestPerm); + } else if (calleesPermuted) { + // the original body is invalid under the new callee conventions and no candidate + // validated — abort the layout experiment for this contract + throw new Error(`no valid re-emission for body ${id} under permuted callees`); + } + }); + + return { overrides, perms }; +} + +// ---------- debug-information remapping ---------- + +const mergeLocations = (locations: SingleLocationData[]): SingleLocationData => { + const lowest = locations.reduce((low, cur) => { + const { start: a } = low.location; const { start: b } = cur.location; + return (b.line < a.line || (b.line === a.line && b.column < a.column)) ? cur : low; + }); + const highest = locations.reduce((high, cur) => { + const { end: a } = high.location; const { end: b } = cur.location; + return (b.line > a.line || (b.line === a.line && b.column > a.column)) ? cur : high; + }); + return { + location: { start: lowest.location.start, end: highest.location.end }, + positionHint: locations[locations.length - 1]?.positionHint ?? PositionHint.START, + }; +}; + +// old element index -> new element index. Exact for untouched regions and preserved +// boundary opcodes; indices inside a rewritten block clamp to the block's last opcode +// (the closest instruction to the failing check for error attribution). +const indexRemapper = (mapping: ItemMapping[], offset: number) => (oldIndex: number): number => { + if (oldIndex < offset) return oldIndex; + const local = oldIndex - offset; + for (const m of mapping) { + if (local >= m.oldStart && local < m.oldEnd) { + return offset + (m.rewritten ? Math.max(m.newStart, m.newEnd - 1) : m.newStart + (local - m.oldStart)); + } + } + const last = mapping[mapping.length - 1]; + return offset + (last?.newEnd ?? 0) + (local - (last?.oldEnd ?? 0)); +}; + +function remapLocationData(locationData: FullLocationData, mapping: ItemMapping[], offset: number): FullLocationData { + const out: FullLocationData = locationData.slice(0, offset); + mapping.forEach((m) => { + const slice = locationData.slice(offset + m.oldStart, offset + m.oldEnd); + if (!m.rewritten) { slice.forEach((entry) => out.push(entry)); return; } + const merged = slice.length > 0 ? mergeLocations(slice) : locationData[offset + m.oldStart - 1]; + for (let i = m.newStart; i < m.newEnd; i += 1) out.push(merged); + }); + return out; +} + +// ---------- the pass ---------- + +export function applyStackRescheduling( + optimised: OptimiseBytecodeResult, + frames: DebugFrame[], + options: RescheduleOptions, +): RescheduleOutcome { + const { arities, objective, constructorParamLength } = options; + const d = dissect(optimised.script); + + // 1. bodies (skipped for a body with console.log entries — wholesale reordering cannot + // preserve per-instruction log data) + const framesByBytecode = new Map(frames.map((frame) => [frame.bytecode, frame])); + const loggedBodies = new Set( + d.order.filter((id) => { + const frame = framesByBytecode.get(binToHex(scriptToBytecode(d.bodies.get(id)!))); + return (frame?.logs.length ?? 0) > 0; + }), + ); + const loggableArities = new Map([...arities].filter(([id]) => !loggedBodies.has(id))); + + let choices: BodyChoices; + // The layout search needs main to be re-writable (it may have to re-stage arguments), and + // every call site in the whole program must be ` OP_INVOKE` — see hasAmbiguousInvoke. + const allScripts = [d.main, ...d.order.map((id) => d.bodies.get(id)!)]; + const layouts = optimised.logs.length === 0 && !allScripts.some(hasAmbiguousInvoke); + const debug = process.env.CASHC_DEBUG_RESCHEDULE !== undefined; + try { + choices = rescheduleBodies(d, loggableArities, objective, layouts); + if (debug && layouts) { + // eslint-disable-next-line no-console + console.error(`[reschedule] layouts: ${choices.perms.size} permuted of ${d.order.length} bodies (${[...choices.perms.keys()].join(',')})`); + } + } catch (e) { + if (debug) console.error(`[reschedule] layout experiment fell back to identity: ${(e as Error).message}`); // eslint-disable-line no-console + // the layout experiment failed validation somewhere — identity layouts wholesale + choices = rescheduleBodies(d, loggableArities, objective, false); + } + let { overrides } = choices; + let { perms } = choices; + + // 2. main routine (kept as-is when the contract logs to console). When any callee's + // entry layout changed, main MUST be re-emitted (its raw call sites stage the old + // order); if that fails, redo everything with identity layouts. + let main = d.main; + let mainMapping: ItemMapping[] | undefined; + const rescheduleMain = (): boolean => { + // main MUST be re-emitted only when it itself stages arguments for a permuted callee; + // permuted inner helpers (reached through other bodies) don't affect its call sites + const needsReemit = perms.size > 0 && bodyInvokes(d.main).some((id) => perms.has(id)); + let best: RecompiledScript | undefined; + let bestCost = needsReemit ? Infinity : scriptCost(d.main, objective); + MAIN_STRATEGIES.forEach((strategy) => { + let candidate: RecompiledScript; + try { + candidate = recompileScript(d.main, arities, options.mainInArity, strategy, objective, { calleePerms: perms }); + } catch (e) { + if (debug) console.error(`[reschedule] main (${strategy}) failed: ${(e as Error).message}`); // eslint-disable-line no-console + return; + } + const cost = scriptCost(candidate.script, objective); + if (cost < bestCost && candidate.changed) { best = candidate; bestCost = cost; } + }); + if (best !== undefined) { main = best.script; mainMapping = best.mapping; } + return best !== undefined || !needsReemit; + }; + if (optimised.logs.length === 0) { + if (!rescheduleMain() && perms.size > 0) { + ({ overrides, perms } = rescheduleBodies(d, loggableArities, objective, false)); + main = d.main; + mainMapping = undefined; + rescheduleMain(); + } + } else if (perms.size > 0) { + // main cannot be re-emitted (logs) so callee conventions must stay standard + ({ overrides, perms } = rescheduleBodies(d, loggableArities, objective, false)); + } + + if (debug) { + // eslint-disable-next-line no-console + console.error(`[reschedule] final: overrides=${overrides.size} perms=${perms.size} mainRewritten=${mainMapping !== undefined}`); + } + if (overrides.size === 0 && mainMapping === undefined) return { result: optimised, frames }; + + // 3. rebuild the script with the rescheduled bodies + main + const script: Script = []; + d.order.forEach((id) => { + script.push(scriptToBytecode(overrides.get(id) ?? d.bodies.get(id)!), encodeInt(BigInt(id)), Op.OP_DEFINE); + }); + main.forEach((el) => script.push(el)); + + // 4. remap top-level debug structures across the main rewrite (the define-table prefix + // is index-stable: body pushes change bytes, not element positions) + let { locationData, requires, sourceTags } = optimised; + if (mainMapping !== undefined) { + const remap = indexRemapper(mainMapping, d.tableLength); + locationData = remapLocationData(locationData, mainMapping, d.tableLength); + requires = requires.map((req: RequireStatement) => ({ + ...req, + ip: remap(req.ip - constructorParamLength) + constructorParamLength, + })); + sourceTags = sourceTags.map((tag: SourceTagEntry) => ({ + ...tag, + startIndex: remap(tag.startIndex), + endIndex: remap(tag.endIndex), + })); + } + + // 5. refresh the debug frames of rescheduled bodies: coarse (whole-function) source + // map, requires re-anchored to their preserved boundaries, tags dropped + const newFrames = frames.map((frame) => { + const id = d.order.find((candidate) => binToHex(scriptToBytecode(d.bodies.get(candidate)!)) === frame.bytecode); + if (id === undefined || !overrides.has(id)) return frame; + const body = overrides.get(id)!; + // The frame carries no whole-definition location, so span its own source map instead + // (first entry's start to last entry's end). + const frameLocations = sourceMapToLocationData(frame.sourceMap); + const wholeLocation: SingleLocationData = { + location: { + start: frameLocations[0].location.start, + end: frameLocations[frameLocations.length - 1].location.end, + }, + positionHint: PositionHint.START, + }; + const bodyLocationData: FullLocationData = new Array(body.length).fill(wholeLocation); + // block-accurate require remapping is not tracked per body (candidates are selected + // per strategy); clamp every ip into the new body's range instead + const maxIp = Math.max(0, body.length - 1); + return { + ...frame, + bytecode: binToHex(scriptToBytecode(body)), + sourceMap: generateSourceMap(bodyLocationData), + requires: frame.requires.map((req) => ({ ...req, ip: Math.min(req.ip, maxIp) })), + ...(frame.sourceTags !== undefined ? { sourceTags: undefined } : {}), + }; + }); + + // Inline ranges are not remapped across a main rewrite (TODO: remap them alongside requires); + // dropping them degrades inlined-callable attribution gracefully instead of pointing debuggers + // at the wrong instructions. + const inlineRanges = mainMapping === undefined ? optimised.inlineRanges : []; + + return { + result: { + ...optimised, script, locationData, requires, sourceTags, inlineRanges, + }, + frames: newFrames, + }; +} diff --git a/packages/cashc/test/compiler/ConstantModificationError/tuple_reassign_constant.cash b/packages/cashc/test/compiler/ConstantModificationError/tuple_reassign_constant.cash new file mode 100644 index 000000000..f439d5641 --- /dev/null +++ b/packages/cashc/test/compiler/ConstantModificationError/tuple_reassign_constant.cash @@ -0,0 +1,11 @@ +function pair(int n) returns (int, int) { + return n + 1, n + 2; +} + +contract Test() { + function spend(int n) { + int constant f = 5; + int q, f = pair(n); + require(q > f); + } +} diff --git a/packages/cashc/test/compiler/DuplicateTupleTargetError/declaration_then_reassignment.cash b/packages/cashc/test/compiler/DuplicateTupleTargetError/declaration_then_reassignment.cash new file mode 100644 index 000000000..b1785eb5f --- /dev/null +++ b/packages/cashc/test/compiler/DuplicateTupleTargetError/declaration_then_reassignment.cash @@ -0,0 +1,10 @@ +function pair(int n) returns (int, int) { + return n + 1, n + 2; +} + +contract Test() { + function spend(int n) { + int a, a = pair(n); + require(a > 0); + } +} diff --git a/packages/cashc/test/compiler/DuplicateTupleTargetError/duplicate_reassignment_targets.cash b/packages/cashc/test/compiler/DuplicateTupleTargetError/duplicate_reassignment_targets.cash new file mode 100644 index 000000000..d4a721253 --- /dev/null +++ b/packages/cashc/test/compiler/DuplicateTupleTargetError/duplicate_reassignment_targets.cash @@ -0,0 +1,11 @@ +function pair(int n) returns (int, int) { + return n + 1, n + 2; +} + +contract Test() { + function spend(int n) { + int a = n; + (a, a) = pair(n); + require(a > 0); + } +} diff --git a/packages/cashc/test/compiler/InvalidSymbolTypeError/tuple_reassign_function_name.cash b/packages/cashc/test/compiler/InvalidSymbolTypeError/tuple_reassign_function_name.cash new file mode 100644 index 000000000..572e098eb --- /dev/null +++ b/packages/cashc/test/compiler/InvalidSymbolTypeError/tuple_reassign_function_name.cash @@ -0,0 +1,10 @@ +function pair(int n) returns (int, int) { + return n + 1, n + 2; +} + +contract Test() { + function spend(int n) { + int q, pair = pair(n); + require(q > 0); + } +} diff --git a/packages/cashc/test/compiler/ParseError/single_type_destructuring.cash b/packages/cashc/test/compiler/ParseError/single_type_destructuring.cash deleted file mode 100644 index 89afb1bfe..000000000 --- a/packages/cashc/test/compiler/ParseError/single_type_destructuring.cash +++ /dev/null @@ -1,5 +0,0 @@ -contract Test() { - function test1(string s) { - string x, y = s.split(5); - } -} diff --git a/packages/cashc/test/compiler/TupleTargetOrderError/reassignment_before_declaration.cash b/packages/cashc/test/compiler/TupleTargetOrderError/reassignment_before_declaration.cash new file mode 100644 index 000000000..72ae284f3 --- /dev/null +++ b/packages/cashc/test/compiler/TupleTargetOrderError/reassignment_before_declaration.cash @@ -0,0 +1,11 @@ +function pair(int n) returns (int, int) { + return n + 1, n + 2; +} + +contract Test() { + function spend(int n) { + int a = n; + a, int b = pair(n); + require(a > b); + } +} diff --git a/packages/cashc/test/compiler/UndefinedReferenceError/tuple_reassign_undefined.cash b/packages/cashc/test/compiler/UndefinedReferenceError/tuple_reassign_undefined.cash new file mode 100644 index 000000000..396dcf8ac --- /dev/null +++ b/packages/cashc/test/compiler/UndefinedReferenceError/tuple_reassign_undefined.cash @@ -0,0 +1,12 @@ +function swap(int x, int y) returns (int, int) { + return y, x; +} + +contract Test() { + function spend(int seed) { + int a = seed; + // `b` is never declared, so reassigning it must fail + (a, b) = swap(a, seed); + require(a + b >= 0); + } +} diff --git a/packages/cashc/test/compiler/compiler.test.ts b/packages/cashc/test/compiler/compiler.test.ts index f121476ca..d62c7ff16 100644 --- a/packages/cashc/test/compiler/compiler.test.ts +++ b/packages/cashc/test/compiler/compiler.test.ts @@ -86,6 +86,7 @@ describe('Compiler', () => { expect(artifact.compiler.options).toEqual({ enforceFunctionParameterTypes: true, enforceLocktimeGuard: false, + optimizeFor: 'opcost', }); }); }); diff --git a/packages/cashc/test/constant-hoisting.test.ts b/packages/cashc/test/constant-hoisting.test.ts new file mode 100644 index 000000000..6142f110c --- /dev/null +++ b/packages/cashc/test/constant-hoisting.test.ts @@ -0,0 +1,186 @@ +import { compileString } from '../src/index.js'; +import { IndexOutOfBoundsError } from '../src/Errors.js'; + +const P = '21888242871839275222246405745257275088696311157297823662689037894645226208583'; + +const asm = (code: string, hoist: boolean): string => ( + compileString(code, { optimizeFor: hoist ? 'size' : 'opcost' }).bytecode +); +const count = (haystack: string, needle: string): number => haystack.split(needle).length - 1; + +// The prime's minimal VM-number encoding as it appears in ASM (64 hex chars), derived +// from a single-use compile so the tests don't hardcode the encoding. +const PRIME_ASM = asm(`contract C() { function spend(int x) { require(x == ${P}); } }`, false) + .match(/[0-9a-f]{64}/)![0]; + +describe("Repeated-constant hoisting (optimizeFor: 'size')", () => { + const doubleUse = ` + contract C() { + function spend(int x, int y) { + require((x - y + ${P}) % ${P} == 1); + } + }`; + + it('is off by default: the duplicate literal is pushed twice', () => { + expect(count(asm(doubleUse, false), PRIME_ASM)).toBe(2); + }); + + it("defaults to the 'opcost' objective when the option is unset", () => { + expect(compileString(doubleUse).bytecode).toBe(asm(doubleUse, false)); + }); + + it('binds a duplicated big literal to a local under the flag', () => { + expect(count(asm(doubleUse, true), PRIME_ASM)).toBe(1); + }); + + it('shrinks the bytecode under the flag', () => { + expect(asm(doubleUse, true).length).toBeLessThan(asm(doubleUse, false).length); + }); + + it('hoists inside global function bodies too', () => { + const code = ` + function subFp(int x, int y) returns (int) { return (x - y + ${P}) % ${P}; } + contract C() { + function spend(int x, int y) { require(subFp(x, y) == 1); } + }`; + expect(count(asm(code, false), PRIME_ASM)).toBe(2); + expect(count(asm(code, true), PRIME_ASM)).toBe(1); + }); + + it('hoists a duplicated big hex literal', () => { + const blob = 'aa'.repeat(32); + const code = ` + contract C() { + function spend(bytes b) { + require(b.split(32)[0] == 0x${blob} || b.split(32)[1] == 0x${blob}); + } + }`; + expect(count(asm(code, false), blob)).toBe(2); + expect(count(asm(code, true), blob)).toBe(1); + }); + + it('leaves small repeated literals alone (hoisting would cost bytes)', () => { + const code = ` + contract C() { + function spend(int x) { require((x + 3) % 3 == 1); } + }`; + expect(asm(code, true)).toBe(asm(code, false)); + }); + + it('avoids colliding with existing names', () => { + const code = ` + contract C() { + function spend(int hc0, int y) { + require((hc0 - y + ${P}) % ${P} == 1); + } + }`; + expect(count(asm(code, true), PRIME_ASM)).toBe(1); // compiles (no shadowing), still hoisted + }); + + it('composes with named top-level constants', () => { + // A multi-use named constant is already deduplicated by the shared-definition mechanism + // (it compiles like a zero-argument function), so hoisting has nothing left to do — the + // prime appears once under either objective, and 'size' must not duplicate or break it. + const code = ` + int constant PRIME = ${P}; + contract C() { + function spend(int x, int y) { + require((x - y + PRIME) % PRIME == 1); + } + }`; + expect(count(asm(code, false), PRIME_ASM)).toBe(1); + expect(count(asm(code, true), PRIME_ASM)).toBe(1); + }); + + it('produces the same ABI either way', () => { + const a = compileString(doubleUse, { optimizeFor: 'size' }); + const b = compileString(doubleUse, { optimizeFor: 'opcost' }); + expect(a.abi).toEqual(b.abi); + }); +}); + +describe('Literal-sensitive positions are never hoisted', () => { + it('keeps split bounds literal so bounded-type inference survives', () => { + // Pre-exclusion, the repeated 500 was hoisted, split() lost its literal bound, the tuple + // degraded to (bytes, bytes) and the bytes500 assignment threw AssignTypeError. + const code = ` + contract C() { + function spend(bytes b, int i) { + require(i == 500); + require(i + 500 == 1000); + bytes500 x = b.split(500)[0]; + require(x.length == 500); + } + }`; + expect(() => asm(code, true)).not.toThrow(); + }); + + it("raises IndexOutOfBoundsError under 'size' exactly as under 'opcost'", () => { + // Pre-exclusion, hoisting the repeated 500 suppressed the static bounds check and the + // contract compiled into a script whose OP_SPLIT always fails at runtime. + const code = ` + contract C() { + function spend(bytes32 b, int i) { + require(i == 500); + require(i + 500 == 1000); + require(b.split(500)[0] != 0x00); + } + }`; + expect(() => asm(code, false)).toThrow(IndexOutOfBoundsError); + expect(() => asm(code, true)).toThrow(IndexOutOfBoundsError); + }); + + it('does not hoist literals that only appear in console.log statements', () => { + // Console logs are stripped from the real bytecode, so hoisting their literals would + // materialise debug-only data as live stack values and inflate the script. + const withConsole = ` + contract C() { + function spend(int x) { + console.log(${P}, ${P}); + require(x == 1); + } + }`; + const withoutConsole = ` + contract C() { + function spend(int x) { + require(x == 1); + } + }`; + expect(asm(withConsole, true)).toBe(asm(withoutConsole, true)); + }); + + it('keeps time-op operands literal so the locktime-guard heuristic still recognises them', () => { + // A require(this.age >= ) covers the tx.locktime read; hoisting the literal made + // the check unrecognisable and a synthetic guard (OP_CHECKLOCKTIMEVERIFY) was injected. + const code = ` + contract C() { + function spend(int x) { + require(this.age >= 100000); + require(x == 100000); + require(tx.locktime <= 100000 + x); + } + }`; + expect(count(asm(code, true), 'OP_CHECKLOCKTIMEVERIFY')).toBe(0); + }); +}); + +describe('Hoisting guardrail (compile-both-keep-smaller)', () => { + // LockingBytecodeNullData elements get cheaper codegen when they stay literals (the VarInt + // size prefix is computed at compile time), so hoisting the repeated element inflates the + // script — engineered inflation that the guardrail must reject. + const nulldata = ` + contract C() { + function spend() { + require(new LockingBytecodeNullData([0xaabbcc, 0xaabbcc]) != 0x00); + } + }`; + + it("never produces a 'size' artifact larger than the unhoisted compile", () => { + const unhoisted = compileString(nulldata, { optimizeFor: 'size', disableConstantHoisting: true }).bytecode; + expect(asm(nulldata, true).length).toBeLessThanOrEqual(unhoisted.length); + }); + + it('keeps the literal-shaped codegen when hoisting would inflate the script', () => { + expect(count(asm(nulldata, true), 'aabbcc')).toBe(2); // both stay literal element pushes + }); +}); diff --git a/packages/cashc/test/def-sinking.test.ts b/packages/cashc/test/def-sinking.test.ts new file mode 100644 index 000000000..c3521ce20 --- /dev/null +++ b/packages/cashc/test/def-sinking.test.ts @@ -0,0 +1,251 @@ +import { compileString } from '../src/index.js'; + +// Def-sinking moves each definition down to just before its first use. It is a byte-size +// optimisation that only runs under the 'size' objective. The primary assertion idiom: +// compiling a source with sinking must produce the same bytecode as compiling the +// hand-reordered source with sinking disabled. +const sunk = (code: string): string => compileString(code, { optimizeFor: 'size' }).bytecode; +const unsunk = (code: string): string => ( + compileString(code, { optimizeFor: 'size', disableDefSinking: true }).bytecode +); +const count = (haystack: string, needle: string): number => haystack.split(needle).length - 1; + +const wrap = (body: string): string => ` + contract C() { + function spend(int a, int b) { + ${body} + } + }`; + +describe('Definition sinking', () => { + it('sinks a single-use definition to adjacency', () => { + const original = wrap(` + int x = a + 1; + require(b == 2); + require(x == 5);`); + const reordered = wrap(` + require(b == 2); + int x = a + 1; + require(x == 5);`); + expect(sunk(original)).toBe(unsunk(reordered)); + }); + + it('sinks a chain of dependent definitions together in one pass', () => { + const original = wrap(` + int x = a + 1; + int y = x + 2; + require(b == 3); + require(y == 9);`); + const reordered = wrap(` + require(b == 3); + int x = a + 1; + int y = x + 2; + require(y == 9);`); + expect(sunk(original)).toBe(unsunk(reordered)); + }); + + it('stops above a reassignment of an initializer input, and never sinks reassigned variables', () => { + const original = wrap(` + int y = a; + int x = y + 1; + y = b; + require(b == 2); + require(x + y == 3);`); + // `y` is reassigned, so its definition stays; `x` reads `y`, so it cannot cross `y = b`. + expect(sunk(original)).toBe(unsunk(original)); + }); + + it('treats a branch as a hard barrier: a partial sink lands just above it', () => { + const original = wrap(` + int x = a + 1; + require(b == 2); + if (b == 2) { + require(a == 1); + } + require(x == 5);`); + const reordered = wrap(` + require(b == 2); + int x = a + 1; + if (b == 2) { + require(a == 1); + } + require(x == 5);`); + expect(sunk(original)).toBe(unsunk(reordered)); + }); + + it('sinks to a first use inside a branch, landing just above it', () => { + const original = wrap(` + int x = a + 1; + require(b == 2); + if (x == 2) { + require(a == 1); + } else { + require(a == 0); + }`); + const reordered = wrap(` + require(b == 2); + int x = a + 1; + if (x == 2) { + require(a == 1); + } else { + require(a == 0); + }`); + expect(sunk(original)).toBe(unsunk(reordered)); + }); + + it('sinks within nested blocks independently', () => { + const original = wrap(` + if (b == 2) { + int x = a + 1; + require(b == 2); + require(x == 5); + } else { + require(a == 0); + }`); + const reordered = wrap(` + if (b == 2) { + require(b == 2); + int x = a + 1; + require(x == 5); + } else { + require(a == 0); + }`); + expect(sunk(original)).toBe(unsunk(reordered)); + }); + + it('sinks a tuple destructure that declares only new variables', () => { + const withTuple = (order: 'original' | 'reordered'): string => ` + function divmod(int x, int y) returns (int, int) { + return x / y, x % y; + } + contract C() { + function spend(int a, int b) { + ${order === 'original' + ? 'int q, int r = divmod(a, 3); require(b == 2);' + : 'require(b == 2); int q, int r = divmod(a, 3);'} + require(q + r == 5); + } + }`; + expect(sunk(withTuple('original'))).toBe(unsunk(withTuple('reordered'))); + }); + + it('treats a tuple reassignment as a hard barrier and never moves it', () => { + const original = ` + function divmod(int x, int y) returns (int, int) { + return x / y, x % y; + } + contract C() { + function spend(int a, int b) { + int q = a + 1; + int x = b + 1; + int r, q = divmod(a, 3); + require(b == 2); + require(x + q + r == 5); + } + }`; + // `q` is reassigned by the destructure, so its definition stays put; `x` cannot cross the + // mixed destructure (hard barrier) even though it does not touch `x`'s inputs. + expect(sunk(original)).toBe(unsunk(original)); + }); + + it('leaves unused-modified definitions in place', () => { + const original = wrap(` + int unused pad = a + 1; + require(b == 2); + require(a == 1);`); + expect(sunk(original)).toBe(unsunk(original)); + }); + + it('lands each of several definitions adjacent to its own require', () => { + const original = wrap(` + int x = a + 1; + int y = a + 2; + int z = a + 3; + require(x == 1); + require(y == 2); + require(z == b);`); + const reordered = wrap(` + int x = a + 1; + require(x == 1); + int y = a + 2; + require(y == 2); + int z = a + 3; + require(z == b);`); + expect(sunk(original)).toBe(unsunk(reordered)); + }); + + it('counts console.log parameters as uses', () => { + const original = wrap(` + int x = a + 1; + console.log(x); + require(b == 2); + require(x == 5);`); + // The log pins `x` in place (codegen needs the variable on the stack at the log's position). + expect(sunk(original)).toBe(unsunk(original)); + }); + + it('never sinks past the final require, even toward a trailing log', () => { + const original = wrap(` + int x = a + 1; + require(b == 2); + console.log(x);`); + // Without the cap, `x`'s first use (the trailing log) would pull its definition past the + // final require and trip EnsureFinalRequireTraversal. + expect(() => compileString(original, { optimizeFor: 'size' })).not.toThrow(); + expect(sunk(original)).toBe(unsunk(original)); + }); + + it('sinks inside global function bodies', () => { + const withGlobal = (order: 'original' | 'reordered'): string => ` + function f(int v, int w) returns (int) { + ${order === 'original' + ? 'int t = v + 1; require(w > 0);' + : 'require(w > 0); int t = v + 1;'} + return t + w; + } + contract C() { + function spend(int a, int b) { + require(f(a, b) == 3); + } + }`; + expect(sunk(withGlobal('original'))).toBe(unsunk(withGlobal('reordered'))); + }); + + it('erases the access pair of a definition sunk to adjacency and shrinks the bytecode', () => { + // Unsunk, `x` sits at depth 3 under p/q/r when it is read (`OP_3 OP_ROLL`). Sinking moves + // p/q/r below `x`'s use, so `x` resolves at depth 0 and the peephole erases the access pair. + const original = wrap(` + int x = a + 1; + int p = a + 2; + int q = a + 3; + int r = a + 4; + require(x == 5); + require(p + q + r == b);`); + expect(count(unsunk(original), 'OP_ROLL')).toBeGreaterThan(0); + expect(count(sunk(original), 'OP_ROLL')).toBeLessThan(count(unsunk(original), 'OP_ROLL')); + expect(sunk(original).length).toBeLessThan(unsunk(original).length); + }); + + it("only runs under the 'size' objective, and produces the same ABI either way", () => { + const original = wrap(` + int x = a + 1; + require(b == 2); + require(x == 5);`); + expect(sunk(original)).not.toBe(unsunk(original)); + // The default 'opcost' objective never sinks. + expect(compileString(original).bytecode).toBe(compileString(original, { disableDefSinking: true }).bytecode); + expect(compileString(original, { optimizeFor: 'size' }).abi).toEqual(compileString(original).abi); + }); + + it('still rejects use-before-definition under the size objective (no laundering by reordering)', () => { + // Sinking `int x = later + 1;` below `int later = 2;` would make this invalid program + // compile; definitions counting as assigns pins the sink above the redefinition. + const original = wrap(` + int x = later + 1; + int later = 2; + require(x == 3); + require(later == 2);`); + expect(() => compileString(original)).toThrow('Undefined reference to symbol later'); + expect(() => compileString(original, { optimizeFor: 'size' })).toThrow('Undefined reference to symbol later'); + }); +}); diff --git a/packages/cashc/test/generation/fixtures.ts b/packages/cashc/test/generation/fixtures.ts index 83301fcbf..0ed7ba595 100644 --- a/packages/cashc/test/generation/fixtures.ts +++ b/packages/cashc/test/generation/fixtures.ts @@ -38,6 +38,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -84,6 +85,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -136,6 +138,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -182,6 +185,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -260,6 +264,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -288,6 +293,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -323,6 +329,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -356,6 +363,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -421,6 +429,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -458,6 +467,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -489,6 +499,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -527,6 +538,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -622,6 +634,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -705,6 +718,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -760,6 +774,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -788,6 +803,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -816,6 +832,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -847,6 +864,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -875,6 +893,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -905,6 +924,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -950,6 +970,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -981,6 +1002,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1013,6 +1035,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1045,6 +1068,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1074,6 +1098,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1103,6 +1128,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1134,6 +1160,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1176,6 +1203,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1207,6 +1235,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1239,6 +1268,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1269,6 +1299,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1298,6 +1329,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1351,6 +1383,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1403,6 +1436,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: false, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1448,6 +1482,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1456,7 +1491,8 @@ export const fixtures: Fixture[] = [ }, { // A multi-parameter global function — locks in the parameter stack-seeding and argument order - // (the contract OP_SWAPs x and y into place; the body computes a - b directly). + // (the body OP_SWAPs into place against the first-parameter-on-top entry layout; the + // declaration-order arguments at the call site then need no staging at all). fn: 'global_function_multi_param.cash', compilerOptions: { disableInlining: true }, artifact: { @@ -1464,24 +1500,24 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'spend', inputs: [{ name: 'x', type: 'int' }, { name: 'y', type: 'int' }] }], bytecode: - // OP_DEFINE sub (id 0): return a - b - '94 OP_0 OP_DEFINE ' - // require(sub(x, y) == 7) - + 'OP_SWAP OP_0 OP_INVOKE OP_7 OP_NUMEQUAL', + // OP_DEFINE sub (id 0): return a - b (entry layout: a on top, so OP_SWAP before OP_SUB) + '7c94 OP_0 OP_DEFINE ' + // require(sub(x, y) == 7) — x and y are in declaration order: zero staging instructions + + 'OP_0 OP_INVOKE OP_7 OP_NUMEQUAL', debug: { - bytecode: '019400897c008a579c', + bytecode: '027c940089008a579c', logs: [], requires: [ - { ip: 8, line: 7 }, + { ip: 7, line: 7 }, ], - sourceMap: '1::3:1;;::::1;7:23:7:24:0;:16::25:1;;:29::30:0;:8::32:1', + sourceMap: '1::3:1;;::::1;7:16:7:25;;: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', + bytecode: '7c94', + sourceMap: '2:15:2:16;:11:::1', logs: [], requires: [], }, @@ -1494,10 +1530,11 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', - fingerprint: '8fc72a3f89ee3238266d6dd9ad3919f7238c8d6a31296cc8925968a31c78c7dc', + fingerprint: 'ef6dd7819e66a430286fe16f3d6dad7e026cf1970eda6bc620be7e7a3bdd2a4d', }, }, { @@ -1539,6 +1576,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1615,6 +1653,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1662,6 +1701,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1709,6 +1749,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1764,6 +1805,7 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', @@ -1780,25 +1822,27 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'spend', inputs: [{ name: 'x', type: 'int' }] }], bytecode: - // OP_DEFINE divmod (id 0): return a / b, a % b — leaves [quotient, remainder], remainder on top - '6e967b7b97 OP_0 OP_DEFINE ' + // OP_DEFINE divmod (id 0): compiled against first-parameter-on-top entry — + // return a / b, a % b — leaves [quotient, remainder], remainder on top + '6e7c967c7b97 OP_0 OP_DEFINE ' // int q, int r = divmod(x, 3); require(q == 4); require(r == 1) - + 'OP_3 OP_0 OP_INVOKE OP_SWAP OP_4 OP_NUMEQUALVERIFY OP_1 OP_NUMEQUAL', + // (arguments staged right-to-left: 3 first, then x swapped on top) + + 'OP_3 OP_SWAP OP_0 OP_INVOKE OP_SWAP OP_4 OP_NUMEQUALVERIFY OP_1 OP_NUMEQUAL', debug: { - bytecode: '056e967b7b97008953008a7c549d519c', + bytecode: '066e7c967c7b970089537c008a7c549d519c', logs: [], requires: [ - { ip: 8, line: 8 }, - { ip: 11, line: 9 }, + { ip: 9, line: 8 }, + { ip: 12, line: 9 }, ], - sourceMap: '1::3:1;;::::1;7:33:7:34:0;:23::35:1;;8:16:8:17:0;:21::22;:8::24:1;9:21:9:22:0;:8::24:1', + sourceMap: '1::3:1;;::::1;7:33:7:34:0;:30::31;:23::35:1;;8:16:8:17:0;:21::22;:8::24:1;9:21:9:22:0;:8::24:1', functions: [ { id: 0, name: 'divmod', inputs: [{ name: 'a', type: 'int' }, { name: 'b', type: 'int' }], - bytecode: '6e967b7b97', - sourceMap: '2:11:2:16;::::1;:18::19:0;:22::23;:18:::1', + bytecode: '6e7c967c7b97', + sourceMap: '2:11:2:16;;::::1;:18::19:0;:22::23;:18:::1', logs: [], requires: [], }, @@ -1811,10 +1855,11 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', - fingerprint: 'f747468c9408ec52949a22dc2f271a944ee5793eabaa913c9c2b1b4c3fbd0a56', + fingerprint: '13fdcb97e1a0f3546c30fb4f0c29338c1678c729db4e148151a026f93b27652d', }, }, { @@ -1832,8 +1877,9 @@ export const fixtures: Fixture[] = [ inputs: [{ name: 'a', type: 'int' }, { name: 'b', type: 'int' }, { name: 'zeroPadding', type: 'bytes' }], }], bytecode: - // OP_DEFINE pad (id 0): drop unused param `padding`, leaving `value` as the return value - '75 OP_0 OP_DEFINE ' + // OP_DEFINE pad (id 0): first param on top, so the trailing unused param `padding` + // sits below `value` and is dropped with OP_NIP, leaving `value` as the return value + '77 OP_0 OP_DEFINE ' // drop unused constructor param `salt` (top of stack) + 'OP_DROP ' // roll up and drop unused function param `zeroPadding` @@ -1842,21 +1888,21 @@ export const fixtures: Fixture[] = [ + 'OP_2DUP OP_ADD OP_DROP ' // int constant unused magic = 42 — dropped as well + '2a OP_DROP ' - // require(pad(a, 100) + b == 5) - + '64 OP_0 OP_INVOKE OP_ADD OP_5 OP_NUMEQUAL', + // require(pad(a, 100) + b == 5) — arguments staged right-to-left, so `a` is rolled on top + + '64 OP_SWAP OP_0 OP_INVOKE OP_ADD OP_5 OP_NUMEQUAL', debug: { - bytecode: '01750089757b756e9375012a750164008a93559c', + bytecode: '01770089757b756e9375012a7501647c008a93559c', logs: [], requires: [ - { ip: 18, line: 9 }, + { ip: 19, line: 9 }, ], - sourceMap: '1::3:1;;::::1;5:24:5:39:0;6:33:6:57;;7:29:7:34;::::1;:8::35;8:36:8:38:0;:8::39:1;9:23:9:26:0;:16::27:1;;:::31;:35::36:0;:8::38:1', + sourceMap: '1::3:1;;::::1;5:24:5:39:0;6:33:6:57;;7:29:7:34;::::1;:8::35;8:36:8:38:0;:8::39:1;9:23:9:26:0;:20::21;:16::27:1;;:::31;:35::36:0;:8::38:1', functions: [ { id: 0, name: 'pad', inputs: [{ name: 'value', type: 'int' }, { name: 'padding', type: 'int' }], - bytecode: '75', + bytecode: '77', sourceMap: '1:24:1:42', logs: [], requires: [], @@ -1870,10 +1916,11 @@ export const fixtures: Fixture[] = [ options: { enforceFunctionParameterTypes: true, enforceLocktimeGuard: true, + optimizeFor: 'opcost', }, }, updatedAt: '', - fingerprint: '4fcac7e0c885a2d3d6a344866c39c4febdffcaf9bb658ac08a87ed7dea9808b6', + fingerprint: '776d6c2a2477b7dd5820a5439110bae3a6bf399c3494e06ba4657d90fcc34d8e', }, }, ]; diff --git a/packages/cashc/test/global-definitions.test.ts b/packages/cashc/test/global-definitions.test.ts index f2a61d872..0710a7e17 100644 --- a/packages/cashc/test/global-definitions.test.ts +++ b/packages/cashc/test/global-definitions.test.ts @@ -177,6 +177,140 @@ describe('Inlining and shared definitions', () => { expect(bytecode).toContain('OP_INVOKE'); }); + it("inlines a small multi-use body under 'opcost' (default) even when OP_DEFINE would be smaller", () => { + // 5-byte body used 4x: byte-exact accounting prefers OP_DEFINE, but every invocation costs + // ~2 executed instructions that splicing avoids — the 'opcost' objective (the default) takes + // the ops, the 'size' objective takes the bytes. + const code = ` + function addF(int x, int y) returns (int) { return (x + y) % 7919; } + contract C() { + function spend(int a, int b) { + int t1 = addF(a, b); + int t2 = addF(t1, a); + int t3 = addF(t2, b); + require(addF(t3, t1) >= 0); + } + }`; + expect(compileString(code).bytecode).not.toContain('OP_DEFINE'); + expect(compileString(code, { optimizeFor: 'opcost' }).bytecode).not.toContain('OP_DEFINE'); + expect(compileString(code, { optimizeFor: 'size' }).bytecode).toContain('OP_DEFINE'); + }); + + it('keeps a function called inside a loop shared via OP_DEFINE (skipped-branch stepping cost)', () => { + const code = ` + function big(int x) returns (int) { return (x * 7 + 3) * (x + 11) - 5; } + contract C() { + function spend(int n) { + int acc = 0; + for (int i = 0; i < 10; i = i + 1) { + if (n > i) { acc = big(acc + i); } + } + require(acc > 0); + } + }`; + + const { bytecode } = compileString(code); + expect(bytecode).toContain('OP_DEFINE'); + expect(bytecode).toContain('OP_INVOKE'); + }); + + it('keeps the callees of a loop-called function shared too (they would be stepped per iteration)', () => { + const code = ` + function inner(int x) returns (int) { return (x * 7 + 3) * (x + 11) - 5; } + function outer(int x) returns (int) { if (x > 5) { x = inner(x); } return x; } + contract C() { + function spend(int n) { + int acc = 0; + for (int i = 0; i < 10; i = i + 1) { acc = outer(acc + n); } + require(acc > 0); + } + }`; + + // both outer and inner stay defined -> two OP_DEFINEs + const { bytecode } = compileString(code); + expect(countOp(bytecode, 'OP_DEFINE')).toEqual(2); + expect(bytecode).toContain('OP_INVOKE'); + }); + + it('keeps a tiny helper shared when its call sites are spread across contract functions', () => { + // 6-byte, 6-element body called once per entry: spending any entry steps the other two + // inlined copies at 100/opcode (2 x 400 extra vs their invoke sites), which exceeds the + // saved define (313) + own invoke (201) — so the density guard keeps OP_DEFINE. + const code = ` + function calc(int x) returns (int) { return x * 3 + 2 - 5; } + contract C() { + function a(int n) { require(calc(n) == 0); } + function b(int n) { require(calc(n) == 1); } + function c(int n) { require(calc(n) == 2); } + }`; + + const { bytecode } = compileString(code); + expect(bytecode).toContain('OP_DEFINE'); + expect(countOp(bytecode, 'OP_INVOKE')).toEqual(3); + }); + + it('still force-inlines a spread helper when its body is small enough that no spend path loses', () => { + // 4-byte, 4-element body, same spread: 2 x 200 extra stepping stays under the saved + // define (309) + invoke (201), so inlining cannot lose op-cost on any path. + const code = ` + function calc(int x) returns (int) { return x * 3 + 2; } + contract C() { + function a(int n) { require(calc(n) == 0); } + function b(int n) { require(calc(n) == 1); } + function c(int n) { require(calc(n) == 2); } + }`; + + const { bytecode } = compileString(code); + expect(bytecode).not.toContain('OP_DEFINE'); + expect(bytecode).not.toContain('OP_INVOKE'); + }); + + it('inlines a 7-byte body used twice (byte-exact tie: the define prelude carries a push prefix)', () => { + // inlined: 2 x 7 = 14 bytes; defined: (1 + 7) body push + 1 id + 1 OP_DEFINE + 2 x 2 call + // sites = 14 bytes — a tie in bytes, and inlining skips the define and invoke op-cost. + const code = ` + function calc(int x) returns (int) { return x * 17 + 2 - 5; } + contract C() { function spend(int n) { require(calc(n) + calc(n + 1) > 0); } }`; + + const { bytecode } = compileString(code); + expect(bytecode).not.toContain('OP_DEFINE'); + expect(bytecode).not.toContain('OP_INVOKE'); + }); + + it('inlines a function whose only call site is a for-loop initializer (runs before OP_BEGIN)', () => { + const code = ` + function calc(int x) returns (int) { return (x * 7 + 3) * (x + 11) - 5; } + contract C() { + function spend(int n) { + int acc = 0; + for (int i = calc(n); i < 10; i = i + 1) { acc = acc + i; } + require(acc > 0); + } + }`; + + const { bytecode } = compileString(code); + expect(bytecode).not.toContain('OP_DEFINE'); + expect(bytecode).not.toContain('OP_INVOKE'); + }); + + it('still inlines a tiny body inside a loop (steps no more than the invoke site would)', () => { + const code = ` + function inc(int x) returns (int) { return x + 1; } + contract C() { + function spend(int n) { + int acc = 0; + for (int i = 0; i < 10; i = i + 1) { + if (n > i) { acc = inc(acc); } + } + require(acc > 0); + } + }`; + + const { bytecode } = compileString(code); + expect(bytecode).not.toContain('OP_DEFINE'); + expect(bytecode).not.toContain('OP_INVOKE'); + }); + it('ignores call sites inside eliminated functions when deciding to inline', () => { const code = ` function big(int x) returns (int) { return (x * 7 + 3) * (x + 11) - 5; } diff --git a/packages/cashc/test/stack-rescheduling.test.ts b/packages/cashc/test/stack-rescheduling.test.ts new file mode 100644 index 000000000..7deff170b --- /dev/null +++ b/packages/cashc/test/stack-rescheduling.test.ts @@ -0,0 +1,166 @@ +import { + createTestAuthenticationProgramBch, + createVirtualMachineBch2026, + hexToBin, +} from '@bitauth/libauth'; +import { asmToBytecode, bytecodeToScript, encodeInt, scriptToBytecode } from '@cashscript/utils'; +import { compileString } from '../src/index.js'; + +const vm = createVirtualMachineBch2026(false); + +// evaluate a compiled contract (no constructor args) directly against pushed spend args +function evaluate(bytecodeAsm: string, args: bigint[]): boolean { + // spend args are pushed in reverse declaration order (first parameter on top) + const unlocking = scriptToBytecode([...args].reverse().map((arg) => encodeInt(arg))); + const state = vm.evaluate(createTestAuthenticationProgramBch({ + lockingBytecode: asmToBytecode(bytecodeAsm), + unlockingBytecode: unlocking, + valueSatoshis: 1000n, + })); + const top = state.stack[state.stack.length - 1]; + return state.error === undefined && state.stack.length === 1 && top !== undefined && top.length === 1 && top[0] === 1; +} + +// slot-heavy single-function contract: many locals with interleaved, repeated reads +// (the access pattern the rescheduler exists to improve), plus a user function that +// stays OP_DEFINE'd, a loop, and a branch +const CONTRACT = ` + function mulmod(int a, int b, int m) returns (int) { + int p = (a * b) % m; + return p; + } + contract Sched() { + function spend(int x, int y, int z, int w) { + int a = x + y; + int b = y + z; + int c = z + w; + int d = a * b + b * c + c * a; + int e = mulmod(a, b, 1000000007) + mulmod(b, c, 1000000007); + int acc = 0; + for (int i = 0; i < 3; i = i + 1) { + acc = acc + d + e; + } + if (acc > 0) { + require(acc >= d); + } + require(a + b + c + d + e + acc > 0); + } + }`; + +describe('stack rescheduling (rescheduleStacks)', () => { + it('is off by default and leaves the bytecode unchanged', () => { + expect(compileString(CONTRACT).bytecode).toEqual(compileString(CONTRACT, {}).bytecode); + }); + + it.each([['opcost'], ['size']] as const)( + 'preserves accept/reject behaviour under the %s objective', + (optimizeFor) => { + const plain = compileString(CONTRACT, { optimizeFor }); + const rescheduled = compileString(CONTRACT, { optimizeFor, rescheduleStacks: true }); + + const accepts: bigint[][] = [ + [3n, 5n, 7n, 11n], + [1000n, 2000n, 3000n, 4000n], + [123456789n, 987654321n, 555555n, 42n], + ]; + accepts.forEach((args) => { + expect(evaluate(plain.bytecode, args)).toBe(true); + expect(evaluate(rescheduled.bytecode, args)).toBe(true); + }); + + // all-zero inputs fail the final require in both compiles + expect(evaluate(plain.bytecode, [0n, 0n, 0n, 0n])).toBe(false); + expect(evaluate(rescheduled.bytecode, [0n, 0n, 0n, 0n])).toBe(false); + }, + ); + + it('keeps the debug source map aligned with the script', () => { + const artifact = compileString(CONTRACT, { rescheduleStacks: true }); + const scriptLength = hexToBin(artifact.debug!.bytecode).length; + expect(scriptLength).toBeGreaterThan(0); + // one source-map entry per script element: entries are separated by ';' + const elementCount = artifact.debug!.sourceMap.split(';').length; + expect(elementCount).toEqual(bytecodeToScript(hexToBin(artifact.debug!.bytecode)).length); + }); + + it('skips multi-function contracts', () => { + const multi = ` + contract Multi() { + function a(int x) { require(x > 0); } + function b(int y) { require(y > 1); } + }`; + expect(compileString(multi, { rescheduleStacks: true }).bytecode) + .toEqual(compileString(multi).bytecode); + }); +}); + +// Dead computation: a value that never reaches its block's exit still executes for its +// failure behaviour — rejecting the spend is its only observable effect, so eliminating +// it would ACCEPT witnesses the plain compile rejects (see hasDeadComputation). +describe('dead computation is never eliminated', () => { + // a void guard function is a zero-output invoke node at the call site — dead by + // construction; disableInlining pins the OP_DEFINE/OP_INVOKE shape that exercises it + const VOID_GUARD = ` + function check(int x) { require(x > 500); } + contract Guard() { + function spend(int a, int b) { + check(a); + check(b); + require(a + b > 0); + } + }`; + + it.each([['opcost'], ['size']] as const)( + 'keeps void guard calls under the %s objective', + (optimizeFor) => { + const plain = compileString(VOID_GUARD, { optimizeFor, disableInlining: true }); + const rescheduled = compileString(VOID_GUARD, { optimizeFor, disableInlining: true, rescheduleStacks: true }); + + // both call sites survive rescheduling + const invokeCount = (asm: string): number => [...asm.matchAll(/OP_INVOKE/g)].length; + expect(invokeCount(rescheduled.bytecode)).toEqual(invokeCount(plain.bytecode)); + + expect(evaluate(plain.bytecode, [501n, 1000n])).toBe(true); + expect(evaluate(rescheduled.bytecode, [501n, 1000n])).toBe(true); + + // each guard must still reject independently + expect(evaluate(plain.bytecode, [1n, 1000n])).toBe(false); + expect(evaluate(rescheduled.bytecode, [1n, 1000n])).toBe(false); + expect(evaluate(plain.bytecode, [1000n, 1n])).toBe(false); + expect(evaluate(rescheduled.bytecode, [1000n, 1n])).toBe(false); + }, + ); + + it('keeps void guard calls with inlining enabled (default)', () => { + const plain = compileString(VOID_GUARD); + const rescheduled = compileString(VOID_GUARD, { rescheduleStacks: true }); + expect(evaluate(plain.bytecode, [501n, 1000n])).toBe(true); + expect(evaluate(rescheduled.bytecode, [501n, 1000n])).toBe(true); + expect(evaluate(rescheduled.bytecode, [1n, 1000n])).toBe(false); + expect(evaluate(rescheduled.bytecode, [1000n, 1n])).toBe(false); + }); + + it.each([['opcost'], ['size']] as const)( + "keeps an 'unused' variable's failure-capable initializer under the %s objective", + (optimizeFor) => { + const UNUSED_DIV = ` + contract DivGuard() { + function spend(int a) { + int unused x = 500 / a; + require(a < 100); + } + }`; + const plain = compileString(UNUSED_DIV, { optimizeFor }); + const rescheduled = compileString(UNUSED_DIV, { optimizeFor, rescheduleStacks: true }); + + expect(rescheduled.bytecode).toContain('OP_DIV'); + + expect(evaluate(plain.bytecode, [5n])).toBe(true); + expect(evaluate(rescheduled.bytecode, [5n])).toBe(true); + + // division by zero aborts the script in both compiles + expect(evaluate(plain.bytecode, [0n])).toBe(false); + expect(evaluate(rescheduled.bytecode, [0n])).toBe(false); + }, + ); +}); diff --git a/packages/cashc/test/tuple-destructuring.test.ts b/packages/cashc/test/tuple-destructuring.test.ts new file mode 100644 index 000000000..15c82cf83 --- /dev/null +++ b/packages/cashc/test/tuple-destructuring.test.ts @@ -0,0 +1,131 @@ +import { + createTestAuthenticationProgramBch, + createVirtualMachineBch2026, +} from '@bitauth/libauth'; +import { asmToBytecode, encodeInt, scriptToBytecode } from '@cashscript/utils'; +import { compileString } from '../src/index.js'; + +const vm = createVirtualMachineBch2026(false); + +// evaluate a compiled contract (no constructor args) directly against pushed spend args +function evaluate(bytecodeAsm: string, args: bigint[]): boolean { + // spend args are pushed in reverse declaration order (first parameter on top) + const unlocking = scriptToBytecode([...args].reverse().map((arg) => encodeInt(arg))); + const state = vm.evaluate(createTestAuthenticationProgramBch({ + lockingBytecode: asmToBytecode(bytecodeAsm), + unlockingBytecode: unlocking, + valueSatoshis: 1000n, + })); + const top = state.stack[state.stack.length - 1]; + return state.error === undefined && state.stack.length === 1 && top !== undefined && top.length === 1 && top[0] === 1; +} + +// Execution coverage for the SCOPED destructuring fold (GenerateTargetTraversal +// visitTupleAssignment): inside a loop/branch the stack layout must be preserved, so +// reassignments are folded into the existing slots via emitReplace instead of the +// depth-0 pure-rename strategy. +describe('tuple destructuring into existing variables (scoped fold)', () => { + it.each([[true], [false]] as const)( + 'folds pure reassignment in a loop (disableInlining: %s)', + (disableInlining) => { + const code = ` + function swap(int x, int y) returns (int, int) { + return y, x; + } + contract LoopSwap() { + function spend(int a, int b) { + // odd iteration count: the net effect is one swap + for (int i = 0; i < 3; i = i + 1) { + (a, b) = swap(a, b); + } + require(a > b); + } + }`; + const artifact = compileString(code, { disableInlining }); + + // after the net swap, a holds the original b + expect(evaluate(artifact.bytecode, [2n, 5n])).toBe(true); + expect(evaluate(artifact.bytecode, [5n, 2n])).toBe(false); + }, + ); + + it('folds mixed declaration + reassignment inside a branch', () => { + const code = ` + function pair(int n) returns (int, int) { + return n * 2, n + 1; + } + contract BranchFold() { + function spend(int a) { + int total = 0; + if (a > 10) { + int d, total = pair(a); + require(d == a * 2); + } + require(total > 0); + } + }`; + const artifact = compileString(code); + + // branch taken: total is reassigned to a + 1 > 0 + expect(evaluate(artifact.bytecode, [11n])).toBe(true); + // branch not taken: total stays 0 and the final require fails + expect(evaluate(artifact.bytecode, [5n])).toBe(false); + }); + + it('folds mixed declaration + reassignment of loop-carried state', () => { + const code = ` + function step(int x, int y) returns (int, int, int) { + return x + y, x + y, y; + } + contract Fib() { + // pad buys op-cost budget for the loop (the budget scales with unlocking bytes) + function spend(int expected, int unused pad) { + int a = 0; + int b = 1; + int last = 0; + for (int i = 0; i < 5; i = i + 1) { + // fresh declaration first, then the two updated accumulators + (int next, b, a) = step(a, b); + last = next; + } + require(last == expected); + } + }`; + const artifact = compileString(code); + const pad = 1n << 4000n; // ~500-byte push + + // fib recurrence (a, b = b, a + b): iterations produce 1, 2, 3, 5, 8 + expect(evaluate(artifact.bytecode, [8n, pad])).toBe(true); + expect(evaluate(artifact.bytecode, [5n, pad])).toBe(false); + }); + + // Regression: a scoped reassignment is itself the variable's latest use, so the symbol pass must + // move the opRolls entry to it. Before the fix, the last plain read kept the roll, codegen rolled + // the variable off the stack model, and the reassignment crashed with a raw + // "Expected variable ... does not exist on the stack". + it.each([['if'], ['while']] as const)( + 'compiles a scoped reassignment after the variable\'s final plain read (%s)', + (construct) => { + const body = construct === 'if' + ? 'if (x == 1) { (p, q) = pair(x); }' + : 'while (x == 1) { (p, q) = pair(x); x = x + 1; }'; + const code = ` + function pair(int n) returns (int, int) { + return n * 2, n + 1; + } + contract ReassignAfterFinalRead() { + function spend(int x) { + int p = 10; + int q = 0; + require(p == 10); + ${body} + require(q == 0 || q == 2); + } + }`; + + const artifact = compileString(code); + expect(evaluate(artifact.bytecode, [1n])).toBe(true); + expect(evaluate(artifact.bytecode, [2n])).toBe(true); + }, + ); +}); diff --git a/packages/cashc/test/valid-contract-files/tuple_reassignment.cash b/packages/cashc/test/valid-contract-files/tuple_reassignment.cash new file mode 100644 index 000000000..f0aa91ec3 --- /dev/null +++ b/packages/cashc/test/valid-contract-files/tuple_reassignment.cash @@ -0,0 +1,40 @@ +// Destructuring into existing variables: a target without a type reassigns an already-declared +// variable instead of declaring a fresh one. Covers straight-line reassignment, mixed +// declaration+reassignment, and in-loop reassignment of loop-carried state (the case that previously +// needed a fresh-temp + per-element rebind workaround). + +function swap(int x, int y) returns (int, int) { + return y, x; +} + +// returns a fresh scalar followed by the two updated accumulators +function step(int x, int y) returns (int, int, int) { + return x * x + 1, y, x; +} + +contract TupleReassignment() { + function spend(int seed) { + int a = seed; + int b = seed + 1; + + // straight-line reassignment of both existing variables + (a, b) = swap(a, b); + + // mixed: declare c fresh, reassign a (allowed at the top level) + (int c, a) = swap(a, b); + + // loop-carried state reassigned in place each iteration + for (int i = 0; i < 4; i = i + 1) { + (a, b) = swap(a, b); + } + + // mixed declaration + reassignment IN A LOOP: declarations form a contiguous block at the + // front, the loop-carried reassignments trail at the end. + for (int j = 0; j < 4; j = j + 1) { + (int lo, a, b) = step(a, b); + require(lo >= 0); + } + + require(a + b + c >= 0); + } +} diff --git a/packages/utils/src/artifact.ts b/packages/utils/src/artifact.ts index ba2c34d41..c6acb4b88 100644 --- a/packages/utils/src/artifact.ts +++ b/packages/utils/src/artifact.ts @@ -1,7 +1,25 @@ export interface CompilerOptions { enforceFunctionParameterTypes?: boolean; enforceLocktimeGuard?: boolean; -} + // Reschedule straight-line stack code from its dataflow DAG after bytecode optimisation, + // so operands are computed onto the top of the stack instead of fetched from variable + // slots with ` OP_PICK/OP_ROLL`, and choose each user function's argument-arrival + // order jointly with its schedule (see stack-rescheduling.ts). Candidates are ranked by + // the `optimizeFor` objective; per block the compiler keeps min(original, rescheduled), + // function bodies are differentially tested on a VM against the plain compile. Off by + // default: rescheduled regions keep only statement-level debug info (each emitted + // opcode maps to its block's merged source span). Single-function contracts only. + rescheduleStacks?: boolean; + // The optimisation objective for decisions that trade bytecode size against op-cost. + // 'size' minimises bytecode bytes (e.g. binds a literal repeated within one function body to a + // local, so later uses are ~2-byte stack picks instead of repeated pushes: ~-30 bytes per + // duplicate of a 32-byte constant, ~+2 ops per execution of the binding). 'opcost' (the + // default) minimises executed op-cost instead — right for op-bound contracts, whose unlocking + // scripts are zero-padded to buy op budget, making byte savings free and extra ops pure cost. + optimizeFor?: OptimizationTarget; +} + +export type OptimizationTarget = 'size' | 'opcost'; export interface AbiInput { name: string; diff --git a/packages/utils/src/script.ts b/packages/utils/src/script.ts index 4d114a5ee..365462c90 100644 --- a/packages/utils/src/script.ts +++ b/packages/utils/src/script.ts @@ -163,6 +163,77 @@ export interface OptimiseBytecodeResult { inlineRanges: InlineRange[]; } +// Pre-parse the ASM-string optimisation patterns into opcode-number sequences once, so the +// optimiser can match directly against the Script array instead of stringifying it to ASM and +// regex-scanning a growing string on every match. The old approach recovered each match's script +// index with [...processedAsm.matchAll(/\s+/g)].length over the growing prefix, making replaceOps +// O(asm-length) per match — quadratic in script size and pathological for large, constant-heavy +// contracts (e.g. the BN254 pairing chunks that bake dozens of 32-40 byte field constants). +interface ParsedOptimisation { + pattern: Op[]; + replacement: Op[]; + // The original pattern split into tokens, kept only for the console.log transformation bookkeeping. + patternTokens: string[]; +} + +function parseOpcodeTokens(asm: string): Op[] { + const trimmed = asm.trim(); + if (trimmed === '') return []; + return trimmed.split(/\s+/).map((token) => { + const op = Op[token as keyof typeof Op]; + // A typo'd token would otherwise parse to `undefined` and its pattern would silently never match + if (typeof op !== 'number') throw new Error(`Unknown opcode token '${token}' in optimisation pattern`); + return op; + }); +} + +const parsedOptimisations: ParsedOptimisation[] = optimisationReplacements.map(([pattern, replacement]) => ({ + pattern: parseOpcodeTokens(pattern), + replacement: parseOpcodeTokens(replacement), + patternTokens: pattern.trim() === '' ? [] : pattern.trim().split(/\s+/), +})); + +// Structural equality on Script arrays: opcodes by value, data pushes by byte content. Replaces the +// previous fixed-point check that compared scriptToAsm(old) === scriptToAsm(new) (a full stringify +// of both scripts every pass). +function scriptsEqual(a: Script, b: Script): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) { + const x = a[i]; + const y = b[i]; + if (typeof x === 'number' || typeof y === 'number') { + if (x !== y) return false; + } else { + if (x.length !== y.length) return false; + for (let k = 0; k < x.length; k += 1) { + if (x[k] !== y[k]) return false; + } + } + } + return true; +} + +// The opcode a script element represents for matching purposes. Opcodes are stored as numbers, but +// small integer pushes are stored as data (encodeInt(0n) -> empty, 1n -> [1], -1n -> [0x81]); under +// minimal-push encoding these disassemble to OP_0 / OP_1..OP_16 / OP_1NEGATE, which the optimisation +// patterns reference. We derive that opcode exactly as scriptToBytecode/disassembly would: a data +// element whose minimal push is a single byte IS that opcode. Anything else (a genuine multi-byte +// data push) returns -1, which never equals a real opcode. +function elementOpcode(element: OpOrData): number { + if (typeof element === 'number') return element; + if (element.length >= 2) return -1; + const push = encodeDataPush(element); + return push.length === 1 ? push[0] : -1; +} + +// Does the optimisation pattern (a pure opcode sequence) match the script starting at `index`? +function patternMatchesAt(script: Script, index: number, pattern: Op[]): boolean { + for (let j = 0; j < pattern.length; j += 1) { + if (elementOpcode(script[index + j]) !== pattern[j]) return false; + } + return true; +} + export function optimiseBytecode( script: Script, locationData: FullLocationData, @@ -183,11 +254,11 @@ export function optimiseBytecode( sourceTags: newSourceTags, inlineRanges: newInlineRanges, } = replaceOps( - script, locationData, logs, requires, sourceTags, inlineRanges, constructorParamLength, optimisationReplacements, + script, locationData, logs, requires, sourceTags, inlineRanges, constructorParamLength, parsedOptimisations, ); // Break on fixed point - if (scriptToAsm(oldScript) === scriptToAsm(newScript)) break; + if (scriptsEqual(oldScript, newScript)) break; script = newScript; locationData = newLocationData; @@ -290,33 +361,54 @@ function replaceOps( sourceTags: SourceTagEntry[], inlineRanges: InlineRange[], constructorParamLength: number, - optimisations: string[][], + optimisations: ParsedOptimisation[], ): ReplaceOpsResult { - let asm = scriptToAsm(script); + const newScript: Script = [...script]; let newLocationData = [...locationData]; let newLogs = [...logs]; let newRequires = [...requires]; let newSourceTags = [...sourceTags]; let newInlineRanges = [...inlineRanges]; - optimisations.forEach(([pattern, replacement]) => { - let processedAsm = ''; - let asmToSearch = asm; + // Removal sites act as match barriers for the remainder of the pass, mirroring the string-based + // engine exactly: an empty replacement left a double space in the ASM, and no single-space + // pattern could match across it until the pass-end whitespace cleanup. A barrier value `b` + // blocks any match spanning the boundary between elements b-1 and b. + const barriers = new Set(); + const crossesBarrier = (index: number, length: number): boolean => { + for (const barrier of barriers) { + if (index < barrier && barrier < index + length) return true; + } + return false; + }; - // We add a space or end of string to the end of the pattern to ensure that we match the whole pattern - // (no partial matches) - const regex = new RegExp(`${pattern}(\\s|$)`, 'g'); + optimisations.forEach(({ pattern, replacement, patternTokens }) => { + const patternLength = pattern.length; + if (patternLength === 0) return; + const replacementLength = replacement.length; + const lengthDiff = patternLength - replacementLength; + + // Non-overlapping matches are collected up front against the sweep-start script (regex /g + // semantics): a replacement never participates in another match of the same rule, and neither + // does adjacency created by one of this rule's own removals. + const matchIndices: number[] = []; + let scanIndex = 0; + while (scanIndex <= newScript.length - patternLength) { + if (patternMatchesAt(newScript, scanIndex, pattern) && !crossesBarrier(scanIndex, patternLength)) { + matchIndices.push(scanIndex); + scanIndex += patternLength; + } else { + scanIndex += 1; + } + } - let matchIndex = asmToSearch.search(regex); - while (matchIndex !== -1) { - // We add the part before the match to the processed asm - processedAsm = mergeAsm(processedAsm, asmToSearch.slice(0, matchIndex)); + // Apply the splices left-to-right; earlier splices in this sweep shift later match positions. + let sweepShift = 0; + matchIndices.forEach((matchIndex) => { + const scriptIndex = matchIndex - sweepShift; - // We count the number of spaces in the processed asm + 1, which is equal to the script index - // We do the same thing to calculate the number of opcodes in the pattern and replacement - const scriptIndex = processedAsm === '' ? 0 : [...processedAsm.matchAll(/\s+/g)].length + 1; - const patternLength = [...pattern.matchAll(/\s+/g)].length + 1; - const replacementLength = replacement === '' ? 0 : [...replacement.matchAll(/\s+/g)].length + 1; + // Splice the matched pattern out of the script array, inserting the replacement opcodes. + newScript.splice(scriptIndex, patternLength, ...replacement); // We get the locationData entries for every opcode in the pattern const patternLocations = newLocationData.slice(scriptIndex, scriptIndex + patternLength); @@ -347,8 +439,6 @@ function replaceOps( const replacementLocations = new Array(replacementLength).fill(mergedLocation); newLocationData.splice(scriptIndex, patternLength, ...replacementLocations); - const lengthDiff = patternLength - replacementLength; // 2 or 1 - // The IP of an opcode in the script is its index within the script + the constructor parameters, because // the constructor parameters still have to get added to the front of the script when a new Contract is created. const scriptIp = scriptIndex + constructorParamLength; @@ -382,7 +472,7 @@ function replaceOps( } const addedTransformationsCount = data.ip - scriptIp; - const addedTransformations = [...pattern.split(/\s+/g)].slice(0, addedTransformationsCount).join(' '); + const addedTransformations = patternTokens.slice(0, addedTransformationsCount).join(' '); const newTransformations = data.transformations ? `${addedTransformations} ${data.transformations}` : addedTransformations; return { @@ -394,11 +484,13 @@ function replaceOps( }; }); - // Source tags use raw script indices (no constructor offset), so they adjust against scriptIndex + // Source tags use raw script indices (no constructor offset), so they adjust against the + // script index (snapshotted to a const since scriptIndex advances as the scan continues) + const tagIndex = scriptIndex; newSourceTags = newSourceTags.map((tag) => ({ ...tag, - startIndex: adjustPosition(tag.startIndex, scriptIndex), - endIndex: adjustPosition(tag.endIndex, scriptIndex), + startIndex: adjustPosition(tag.startIndex, tagIndex), + endIndex: adjustPosition(tag.endIndex, tagIndex), })); // Inline ranges use ip coordinates (like requires), so both bounds adjust against scriptIp @@ -408,27 +500,22 @@ function replaceOps( endIp: adjustPosition(inlineRange.endIp, scriptIp), })); - // We add the replacement to the processed asm - processedAsm = mergeAsm(processedAsm, replacement); - - // We do not add the matched pattern anywhere since it gets replaced - - // We set the asmToSearch to the part after the match - asmToSearch = asmToSearch.slice(matchIndex + pattern.length).trim(); - - // Find the next match - matchIndex = asmToSearch.search(regex); - } - - // We add the remaining asm to the processed asm - processedAsm = mergeAsm(processedAsm, asmToSearch); + // Keep barrier positions aligned with the spliced script (a barrier strictly inside the + // matched range is impossible — it would have blocked the match), then record the removal + // site of an empty replacement as a new barrier. + if (barriers.size > 0) { + const shifted = [...barriers].map((barrier) => (barrier > scriptIndex ? barrier - lengthDiff : barrier)); + barriers.clear(); + shifted.forEach((barrier) => barriers.add(barrier)); + } + if (replacementLength === 0) barriers.add(scriptIndex); - // We replace the original asm with the processed asm so that the next optimisation can use the updated asm - asm = processedAsm; + sweepShift += lengthDiff; + }); }); return { - script: asmToScript(asm), + script: newScript, locationData: newLocationData, logs: newLogs, requires: newRequires, @@ -469,8 +556,3 @@ const getLowestStartLocation = (locations: SingleLocationData[]): SingleLocation }, locations[0]); }; -const mergeAsm = (asm1: string, asm2: string): string => { - // We merge two ASM strings by adding a space between them, and removing any duplicate spaces - // or trailing/leading spaces, which might have been introduced due to regex matching / replacements / empty asm strings - return `${asm1} ${asm2}`.replace(/\s+/g, ' ').trim(); -}; diff --git a/packages/utils/test/fixtures/bitauth-script.fixture.ts b/packages/utils/test/fixtures/bitauth-script.fixture.ts index e3bea837d..be2eab583 100644 --- a/packages/utils/test/fixtures/bitauth-script.fixture.ts +++ b/packages/utils/test/fixtures/bitauth-script.fixture.ts @@ -487,7 +487,7 @@ OP_7 OP_0 OP_INVOKE OP_13 OP_NUMEQUAL /* */ /* >>> imported from helpers.cash */ < /* function addChecked(int a, int b) returns (int) { */ - OP_OVER OP_ADD /* int sum = a + b; */ + OP_DUP OP_ROT OP_ADD /* int sum = a + b; */ OP_DUP OP_ROT OP_GREATERTHANOREQUAL OP_VERIFY /* require(sum >= a, "overflow"); */ /* return sum; */ > OP_1 OP_DEFINE /* } */ @@ -497,7 +497,7 @@ OP_7 OP_0 OP_INVOKE OP_13 OP_NUMEQUAL /* contract ImportedFunctions() { */ /* function spend(int x) { */ OP_DUP OP_0 OP_INVOKE /* int doubled = double(x); */ -OP_SWAP OP_1 OP_INVOKE OP_15 OP_NUMEQUAL /* require(addChecked(doubled, x) == 15, "sum mismatch"); */ +OP_1 OP_INVOKE OP_15 OP_NUMEQUAL /* require(addChecked(doubled, x) == 15, "sum mismatch"); */ /* } */ /* } */ /* */ diff --git a/packages/utils/test/optimiser-differential.test.ts b/packages/utils/test/optimiser-differential.test.ts new file mode 100644 index 000000000..0c68d1de7 --- /dev/null +++ b/packages/utils/test/optimiser-differential.test.ts @@ -0,0 +1,208 @@ +import { + asmToScript, + FullLocationData, + Op, + optimiseBytecode, + PositionHint, + Script, + scriptToAsm, +} from '../src/index.js'; +import { optimisationReplacements } from '../src/optimisations.js'; + +// Differential oracle for the opcode-matching peephole optimiser: on any input, its bytecode must +// byte-for-byte match the string-based engine it replaced, running the SAME optimisation table. +// (The in-compiler cross-check against optimiseBytecodeOld is a different comparison: that +// optimiser applies the cashproof-derived table, whose rule order composes differently on +// adversarial inputs, so the two are only expected to agree on compiler-shaped output.) +// +// The reference below reproduces the replaced engine's string semantics: regex /g computes +// matches against the sweep-start string (a replacement never cascades within its own rule's +// sweep), and an empty replacement leaves a double space that no single-space pattern can match +// across until the pass-end whitespace cleanup. Patterns are anchored with \b, mirroring the +// replaced engine's `${pattern}(\s|$)` end anchor ("no partial matches") — equivalent for +// space-separated ASM — and the opcode matcher is immune to partial-token matches by +// construction. (The cashproof cross-check optimiser, optimiseBytecodeOld, has NO such anchor +// and genuinely can corrupt adversarial scripts: after `OP_NOT OP_IF` -> `OP_NOTIF`, its +// unanchored rule `OP_GREATERTHAN OP_NOT` matches inside `OP_GREATERTHAN OP_NOTIF`, yielding +// the invalid token `OP_LESSTHANOREQUALIF`. That, plus its differently-ordered rule table, is +// why this suite does not use it as the oracle.) +const referenceOptimise = (script: Script, runs: number = 1000): Script => { + for (let i = 0; i < runs; i += 1) { + const oldScript = script; + let asm = scriptToAsm(script); + optimisationReplacements.forEach(([pattern, replacement]) => { + asm = asm.replace(new RegExp(`\\b${pattern}\\b`, 'g'), replacement); + }); + asm = asm.replace(/\s+/g, ' ').trim(); + script = asmToScript(asm); // eslint-disable-line no-param-reassign + if (scriptToAsm(oldScript) === scriptToAsm(script)) break; + } + return script; +}; + +// Deterministic PRNG (mulberry32) so any failure is reproducible from the reported script index. +const mulberry32 = (initialSeed: number): (() => number) => { + let seed = initialSeed; + return (): number => { + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +}; + +const OPCODE_POOL: Op[] = [ + Op.OP_0, Op.OP_1, Op.OP_2, Op.OP_3, Op.OP_16, Op.OP_1NEGATE, + Op.OP_DUP, Op.OP_2DUP, Op.OP_3DUP, Op.OP_DROP, Op.OP_2DROP, Op.OP_NIP, Op.OP_TUCK, + Op.OP_SWAP, Op.OP_2SWAP, Op.OP_ROT, Op.OP_2ROT, Op.OP_OVER, Op.OP_2OVER, + Op.OP_PICK, Op.OP_ROLL, Op.OP_TOALTSTACK, Op.OP_FROMALTSTACK, Op.OP_DEPTH, Op.OP_SIZE, + Op.OP_NOT, Op.OP_0NOTEQUAL, Op.OP_ADD, Op.OP_SUB, Op.OP_1ADD, Op.OP_1SUB, Op.OP_NEGATE, Op.OP_ABS, + Op.OP_MIN, Op.OP_MAX, Op.OP_WITHIN, Op.OP_BOOLAND, Op.OP_BOOLOR, + Op.OP_NUMEQUAL, Op.OP_NUMEQUALVERIFY, Op.OP_NUMNOTEQUAL, Op.OP_EQUAL, Op.OP_EQUALVERIFY, + Op.OP_LESSTHAN, Op.OP_GREATERTHAN, Op.OP_LESSTHANOREQUAL, Op.OP_GREATERTHANOREQUAL, + Op.OP_VERIFY, Op.OP_IF, Op.OP_ELSE, Op.OP_ENDIF, + Op.OP_CAT, Op.OP_SPLIT, Op.OP_REVERSEBYTES, Op.OP_BIN2NUM, Op.OP_NUM2BIN, + Op.OP_SHA256, Op.OP_HASH160, Op.OP_HASH256, Op.OP_RIPEMD160, Op.OP_SHA1, + Op.OP_CHECKSIG, Op.OP_CHECKSIGVERIFY, Op.OP_CHECKDATASIG, Op.OP_CHECKDATASIGVERIFY, +]; + +// Pushes chosen to stress the disassembly-exact matching of small-integer representations: +// empty push (renders OP_0), non-minimal single bytes (render OP_1..OP_16 / OP_1NEGATE), and +// plain multi-byte data. +const PUSH_POOL: Uint8Array[] = [ + new Uint8Array([]), + new Uint8Array([0x00]), + new Uint8Array([0x01]), + new Uint8Array([0x07]), + new Uint8Array([0x10]), + new Uint8Array([0x81]), + new Uint8Array([0x11]), + new Uint8Array([0xff]), + new Uint8Array([0x01, 0x02]), + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + new Uint8Array(20).fill(0xab), + new Uint8Array(32).fill(0xcd), +]; + +// Injected multi-element fragments that overlap known rewrite rules, so matches (including +// cascading ones) stay dense enough to exercise the splice/fixed-point machinery. +const FRAGMENT_POOL: Script[] = [ + [Op.OP_SWAP, Op.OP_SWAP], + [Op.OP_DUP, Op.OP_DROP], + [Op.OP_0, Op.OP_ROLL], + [Op.OP_1, Op.OP_ROLL], + [Op.OP_1, Op.OP_PICK], + [Op.OP_TOALTSTACK, Op.OP_FROMALTSTACK], + [Op.OP_OVER, Op.OP_OVER], + [Op.OP_3, Op.OP_PICK, Op.OP_3, Op.OP_PICK], + [Op.OP_3, Op.OP_ROLL, Op.OP_3, Op.OP_ROLL], + [Op.OP_5, Op.OP_ROLL, Op.OP_5, Op.OP_ROLL], + [Op.OP_EQUAL, Op.OP_VERIFY], + [Op.OP_NUMEQUAL, Op.OP_VERIFY], + [Op.OP_CHECKSIG, Op.OP_VERIFY], + [Op.OP_SWAP, Op.OP_ADD], + [Op.OP_SWAP, Op.OP_MUL], + [Op.OP_DUP, Op.OP_SWAP], + [Op.OP_NOT, Op.OP_IF], +]; + +const randomScript = (rand: () => number, length: number): Script => { + const script: Script = []; + while (script.length < length) { + const roll = rand(); + if (roll < 0.5) { + script.push(OPCODE_POOL[Math.floor(rand() * OPCODE_POOL.length)]); + } else if (roll < 0.7) { + script.push(PUSH_POOL[Math.floor(rand() * PUSH_POOL.length)]); + } else { + script.push(...FRAGMENT_POOL[Math.floor(rand() * FRAGMENT_POOL.length)]); + } + } + return script; +}; + +const dummyLocationData = (length: number): FullLocationData => Array.from({ length }, () => ({ + location: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + positionHint: PositionHint.START, +})); + +const optimiseNew = (script: Script, requires = [], logs = []): ReturnType => ( + optimiseBytecode(script, dummyLocationData(script.length), logs, requires, [], [], 0) +); + +describe('optimiser differential (opcode matcher vs same-table string reference)', () => { + // ~2s uninstrumented, but coverage instrumentation pushes it past vitest's 5s default timeout + it('produces byte-for-byte identical output on seeded random scripts', { timeout: 60_000 }, () => { + const rand = mulberry32(0xca5c); + for (let i = 0; i < 2000; i += 1) { + const length = 20 + Math.floor(rand() * 130); + const script = randomScript(rand, length); + + const expected = scriptToAsm(referenceOptimise(script)); + const result = optimiseNew(script); + const actual = scriptToAsm(result.script); + + if (actual !== expected) { + throw new Error([ + `optimiser divergence at seeded script #${i}`, + `input: ${scriptToAsm(script)}`, + `reference: ${expected}`, + `new: ${actual}`, + ].join('\n')); + } + // metadata must track the script 1:1 through every splice + expect(result.locationData.length).toBe(result.script.length); + } + }); + + const directedCases: Array<[string, Script]> = [ + ['pattern at the very start', [Op.OP_SWAP, Op.OP_SWAP, Op.OP_ADD]], + ['pattern at the very end', [Op.OP_ADD, Op.OP_SWAP, Op.OP_SWAP]], + ['entire script optimises away', [Op.OP_SWAP, Op.OP_SWAP]], + ['empty push renders as OP_0', [new Uint8Array([]), Op.OP_ROLL]], + ['non-minimal single-byte push renders as OP_1', [new Uint8Array([0x01]), Op.OP_ROLL]], + ['non-minimal single-byte push renders as OP_1NEGATE', [new Uint8Array([0x81]), Op.OP_DROP]], + ['OP_1NEGATE opcode form', [Op.OP_1NEGATE, Op.OP_SWAP, Op.OP_SWAP, Op.OP_ADD]], + ['cascading matches across a splice', [Op.OP_DUP, Op.OP_SWAP, Op.OP_SWAP, Op.OP_DROP, Op.OP_1]], + ['overlapping candidates', [Op.OP_SWAP, Op.OP_SWAP, Op.OP_SWAP, Op.OP_ADD]], + ['push data is opaque to patterns', [new Uint8Array([0xde, 0xad]), Op.OP_SWAP, Op.OP_SWAP]], + ]; + + it.each(directedCases)('matches the string reference on edge case: %s', (_, script) => { + const expected = scriptToAsm(referenceOptimise(script)); + expect(scriptToAsm(optimiseNew(script).script)).toBe(expected); + }); +}); + +describe('optimiser metadata adjustment goldens', () => { + it('shifts require ips and location data across a removed pair', () => { + const script = [Op.OP_2, Op.OP_SWAP, Op.OP_SWAP, Op.OP_3, Op.OP_ADD]; + const requires = [{ ip: 4, line: 1 }]; + + const result = optimiseBytecode(script, dummyLocationData(script.length), [], requires, [], [], 0); + + expect(scriptToAsm(result.script)).toBe('OP_2 OP_3 OP_ADD'); + expect(result.requires).toEqual([{ ip: 2, line: 1 }]); + expect(result.locationData.length).toBe(3); + }); + + it('keeps require ips before a removed pair untouched', () => { + const script = [Op.OP_2, Op.OP_3, Op.OP_ADD, Op.OP_SWAP, Op.OP_SWAP]; + const requires = [{ ip: 2, line: 1 }]; + + const result = optimiseBytecode(script, dummyLocationData(script.length), [], requires, [], [], 0); + + expect(scriptToAsm(result.script)).toBe('OP_2 OP_3 OP_ADD'); + expect(result.requires).toEqual([{ ip: 2, line: 1 }]); + }); + + it('shifts log ips across a removed pair', () => { + const script = [Op.OP_DUP, Op.OP_DROP, Op.OP_2, Op.OP_3, Op.OP_ADD]; + const logs = [{ ip: 4, line: 1, data: [] }]; + + const result = optimiseBytecode(script, dummyLocationData(script.length), logs, [], [], [], 0); + + expect(scriptToAsm(result.script)).toBe('OP_2 OP_3 OP_ADD'); + expect(result.logs).toEqual([{ ip: 2, line: 1, data: [] }]); + }); +}); diff --git a/packages/utils/test/script.test.ts b/packages/utils/test/script.test.ts index fa7ae8f4a..f7989ce30 100644 --- a/packages/utils/test/script.test.ts +++ b/packages/utils/test/script.test.ts @@ -101,6 +101,6 @@ describe('script utils', () => { describe.skip('TODO: generateContractBytecodeScript()', () => { }); - describe.skip('TODO: optimiseBytecode()', () => { - }); + // optimiseBytecode() is covered by optimiser-differential.test.ts (seeded fuzz vs the legacy + // optimiser, directed edge cases, and metadata adjustment goldens). });