import { flattenBinArray } from '../../format/hex' ; import { AuthenticationInstruction , ParsedAuthenticationInstruction , ParsedAuthenticationInstructionMalformed , } from '../../vm/instruction-sets/instruction-sets-types' ; import { authenticationInstructionIsMalformed , parseBytecode , serializeParsedAuthenticationInstructionMalformed , } from '../../vm/instruction-sets/instruction-sets-utils' ; import { AuthenticationProgramStateExecutionStack } from '../../vm/vm' ; import { createCompilerCommonSynchronous } from '../compiler' ; import { CompilationError , CompilationErrorRecoverable , EvaluationSample , Range , ResolvedScript , ResolvedSegmentLiteralType , ScriptReductionTraceChildNode , ScriptReductionTraceScriptNode , } from './language-types' ; const pluckStartPosition = ( range ) => ( { startColumn : range . startColumn , startLineNumber : range . startLineNumber , } ) ; const pluckEndPosition = ( range ) => ( { endColumn : range . endColumn , endLineNumber : range . endLineNumber , } ) ; param ranges param parentRange export const mergeRanges = ( ranges , parentRange = { endColumn : 0 , endLineNumber : 0 , startColumn : 0 , startLineNumber : 0 , } as Range ) => { const minimumRangesToMerge = 2 ; const unsortedMerged = ranges . length < minimumRangesToMerge ? ranges . length === 1 ? ranges [ 0 ] : parentRange : ranges . reduce < Range > ( ( merged , range ) => ( { ... ( range . endLineNumber > merged . endLineNumber ? pluckEndPosition ( range ) : range . endLineNumber === merged . endLineNumber && range . endColumn > merged . endColumn ? pluckEndPosition ( range ) : pluckEndPosition ( merged ) ) , ... ( range . startLineNumber < merged . startLineNumber ? pluckStartPosition ( range ) : range . startLineNumber === merged . startLineNumber && range . startColumn < merged . startColumn ? pluckStartPosition ( range ) : pluckStartPosition ( merged ) ) , } ) , ranges [ 0 ] ) ; return { ... pluckEndPosition ( unsortedMerged ) , ... pluckStartPosition ( unsortedMerged ) , } ; } ; param outerRange param innerRange param exclusive export const containsRange = ( outerRange , innerRange , exclusive = true ) => { const startsAfter = outerRange . startLineNumber < innerRange . startLineNumber ? true : outerRange . startLineNumber === innerRange . startLineNumber ? exclusive ? outerRange . startColumn < innerRange . startColumn : outerRange . startColumn <= innerRange . startColumn : false ; const endsBefore = outerRange . endLineNumber > innerRange . endLineNumber ? true : outerRange . endLineNumber === innerRange . endLineNumber ? exclusive ? outerRange . endColumn > innerRange . endColumn : outerRange . endColumn >= innerRange . endColumn : false ; return startsAfter && endsBefore ; } ; param script export const compileBtl = ( script ) => { const result = createCompilerCommonSynchronous ( { scripts : { script } , } ) . generateBytecode ( 'script' , { } ) ; if ( result . success ) { return result . bytecode ; } return `BTL compilation error:${ result . errors . reduce ( ( all , { error , range } ) => `${ all } [${ range . startLineNumber }, ${ range . startColumn }]: ${ error }` , '' ) }` ; } ; param resolvedScript export const getResolutionErrors = ( resolvedScript ) : CompilationError [ ] => resolvedScript . reduce < CompilationError [ ] > ( ( errors , segment ) => { switch ( segment . type ) { case 'error' : return [ ... errors , { error : segment . value , ... ( segment . missingIdentifier === undefined ? { } : { missingIdentifier : segment . missingIdentifier , owningEntity : segment . owningEntity , } ) , range : segment . range , } , ] ; case 'push' : case 'evaluation' : return [ ... errors , ... getResolutionErrors ( segment . value ) ] ; default : return errors ; } } , [ ] ) ; param errors export const allErrorsAreRecoverable = ( errors ) : errors is CompilationErrorRecoverable [ ] => errors . every ( ( error ) => 'missingIdentifier' in error && 'owningEntity' in error ) ; /**
 * A single resolution for a `ResolvedSegment`. The `variable`, `script`, or
 * `opcode` property contains the full identifier which resolved to `bytecode`.
 */ export interface BtlResolution { bytecode : Uint8Array ; type : 'variable' | 'script' | 'opcode' | ResolvedSegmentLiteralType ; text : string ; } param resolvedScript export const extractBytecodeResolutions = ( resolvedScript ) : BtlResolution [ ] => resolvedScript . reduce < BtlResolution [ ] > ( ( all , segment ) => { switch ( segment . type ) { case 'push' : case 'evaluation' : return [ ... all , ... extractBytecodeResolutions ( segment . value ) ] ; case 'bytecode' : if ( 'variable' in segment ) { return [ ... all , { bytecode : segment . value , text : segment . variable , type : 'variable' , } , ] ; } if ( 'script' in segment ) { return [ ... all , ... extractBytecodeResolutions ( segment . source ) , { bytecode : segment . value , text : segment . script , type : 'script' , } , ] ; } if ( 'opcode' in segment ) { return [ ... all , { bytecode : segment . value , text : segment . opcode , type : 'opcode' , } , ] ; } return [ ... all , { bytecode : segment . value , text : segment . literal , type : segment . literalType , } , ] ; default : return all ; } } , [ ] ) ; param resolvedScript export const extractResolvedVariableBytecodeMap = ( resolvedScript ) => extractBytecodeResolutions ( resolvedScript ) . reduce < { [ fullIdentifier ] : Uint8Array ; } > ( ( all , resolution ) => resolution . type === 'variable' ? { ... all , [ resolution . text ] : resolution . bytecode } : all , { } ) ; param errors param separator export const stringifyErrors = ( errors , separator = '; ' ) => { return `${ errors . map ( ( error ) => `[${ error . range . startLineNumber }, ${ error . range . startColumn }] ${ error . error }` ) . join ( separator ) }` ; } ; export interface SampleExtractionResult < ProgramState , Opcodes = number > { /**
     * The samples successfully extracted from the provided `nodes` and `trace`.
     *
     * In a successful evaluation, one sample will be produced for each state in
     * `trace` with the exception of the last state (the evaluation result) which
     * will be returned in `unmatchedStates`.
     *
     * In an unsuccessful evaluation, the `trace` states will be exhausted before
     * all `nodes` have been matched. In this case, all matched samples are
     * returned, and the final state (the evaluation result) is dropped. This can
     * be detected by checking if the length of `unmatchedStates` is `0`.
     */ samples : EvaluationSample < ProgramState , Opcodes > [ ] ; /**
     * If the provided `nodes` are exhausted before all states from `trace` have
     * been matched, the remaining "unmatched" states are returned. This is useful
     * for extracting samples for an evaluation involving two or more
     * compilations.
     *
     * In a successful evaluation, after samples have been extracted from each set
     * of `nodes`, the final `trace` state (the evaluation result) will be
     * returned in `unmatchedStates`.
     */ unmatchedStates : ProgramState [ ] ; } remarks param evaluationRange param nodes param trace export const extractEvaluationSamples = < ProgramState , Opcodes = number > ( { evaluationRange , nodes , trace , } ) : SampleExtractionResult < ProgramState , Opcodes > => { const traceWithoutFinalState = trace . length > 1 ? trace . slice ( 0 , - 1 ) : trace . slice ( ) ; if ( traceWithoutFinalState . length === 0 ) { return { samples : [ ] , unmatchedStates : [ ] , } ; } const samples = [ { evaluationRange , internalStates : [ ] , range : { endColumn : evaluationRange . startColumn , endLineNumber : evaluationRange . startLineNumber , startColumn : evaluationRange . startColumn , startLineNumber : evaluationRange . startLineNumber , } , state : traceWithoutFinalState [ 0 ] , } , ] ; let nextState = 1 ; let nextNode = 0 ; let incomplete ; while ( nextState < traceWithoutFinalState . length && nextNode < nodes . length ) { const currentNode = nodes [ nextNode ] ; const { mergedBytecode , mergedRange } = incomplete === undefined ? { mergedBytecode : currentNode . bytecode , mergedRange : currentNode . range , } : { mergedBytecode : flattenBinArray ( [ incomplete . bytecode , currentNode . bytecode , ] ) , mergedRange : mergeRanges ( [ incomplete . range , currentNode . range ] ) , } ; const parsed = parseBytecode < Opcodes > ( mergedBytecode ) ; const hasNonMalformedInstructions = parsed . length !== 0 && ! ( 'malformed' in parsed [ 0 ] ) ; if ( hasNonMalformedInstructions ) { const lastInstruction = parsed [ parsed . length - 1 ] ; const validInstructions = ( authenticationInstructionIsMalformed ( lastInstruction ) ? parsed . slice ( 0 , parsed . length - 1 ) : parsed ) as AuthenticationInstruction < Opcodes > [ ] ; const firstUnmatchedStateIndex = nextState + validInstructions . length ; const matchingStates = traceWithoutFinalState . slice ( nextState , firstUnmatchedStateIndex ) ; const pairedStates = validInstructions . map ( ( instruction , index ) => ( { instruction , state : matchingStates [ index ] as ProgramState | undefined , } ) ) ; /**
             * Guaranteed to have a defined `state` (or the loop would have exited).
             */ const firstPairedState = pairedStates [ 0 ] as { instruction : ParsedAuthenticationInstruction < Opcodes > ; state : ProgramState ; } ; const closesCurrentlyOpenSample = incomplete !== undefined ; if ( closesCurrentlyOpenSample ) { samples . push ( { evaluationRange , instruction : firstPairedState . instruction , internalStates : [ ] , range : mergedRange , state : firstPairedState . state , } ) ; } const firstUndefinedStateIndex = pairedStates . findIndex ( ( { state } ) => state === undefined ) ; const sampleHasError = firstUndefinedStateIndex !== - 1 ; const sampleClosingIndex = sampleHasError ? firstUndefinedStateIndex - 1 : pairedStates . length - 1 ; const closesASecondSample = ! closesCurrentlyOpenSample || sampleClosingIndex > 0 ; if ( closesASecondSample ) { const finalState = pairedStates [ sampleClosingIndex ] as { instruction : ParsedAuthenticationInstruction < Opcodes > ; state : ProgramState ; } ; const secondSamplePairsBegin = closesCurrentlyOpenSample ? 1 : 0 ; const internalStates = pairedStates . slice ( secondSamplePairsBegin , sampleClosingIndex ) as { instruction : ParsedAuthenticationInstruction < Opcodes > ; state : ProgramState ; } [ ] ; samples . push ( { evaluationRange , instruction : finalState . instruction , internalStates , range : currentNode . range , state : finalState . state , } ) ; } nextState = firstUnmatchedStateIndex ; if ( authenticationInstructionIsMalformed ( lastInstruction ) ) { incomplete = { bytecode : serializeParsedAuthenticationInstructionMalformed ( lastInstruction ) , range : currentNode . range , } ; } else { incomplete = undefined ; } } else { const lastInstruction = parsed [ parsed . length - 1 ] as ParsedAuthenticationInstructionMalformed < Opcodes > | undefined ; incomplete = lastInstruction === undefined ? undefined : { bytecode : serializeParsedAuthenticationInstructionMalformed ( lastInstruction ) , range : mergedRange , } ; } nextNode += 1 ; } /**
     * Because we ran out of `trace` states before all `nodes` were matched, we
     * know an error occurred which halted evaluation. This error is indicated in
     * the result by returning an empty array of `unmatchedStates`. Successful
     * evaluations will always return at least one unmatched state: the final
     * "evaluation result" state produced by `vm.debug`.
     */ const errorOccurred = nextNode < nodes . length ; const unmatchedStates = errorOccurred ? [ ] : trace . slice ( nextState ) ; return { samples , unmatchedStates , } ; } ; param evaluationRange param nodes param trace export const extractEvaluationSamplesRecursive = < ProgramState , Opcodes = number > ( { evaluationRange , nodes , trace , } ) : SampleExtractionResult < ProgramState , Opcodes > => { const extractEvaluations = ( node , depth = 1 ) : EvaluationSample < ProgramState , Opcodes > [ ] => { if ( 'push' in node ) { return node . push . script . reduce < EvaluationSample < ProgramState , Opcodes > [ ] > ( ( all , childNode ) => [ ... all , ... extractEvaluations ( childNode , depth ) ] , [ ] ) ; } if ( 'source' in node ) { const childSamples = node . source . script . reduce < EvaluationSample < ProgramState , Opcodes > [ ] > ( ( all , childNode ) => [ ... all , ... extractEvaluations ( childNode , depth + 1 ) , ] , [ ] ) ; const traceWithoutUnlockingPhase = node . trace . slice ( 1 ) ; const evaluationBeginToken = '$(' ; const evaluationEndToken = ')' ; const extracted = extractEvaluationSamples < ProgramState , Opcodes > ( { evaluationRange : { endColumn : node . range . endColumn - evaluationEndToken . length , endLineNumber : node . range . endLineNumber , startColumn : node . range . startColumn + evaluationBeginToken . length , startLineNumber : node . range . startLineNumber , } , nodes : node . source . script , trace : traceWithoutUnlockingPhase , } ) ; return [ ... extracted . samples , ... childSamples ] ; } return [ ] ; } ; const { samples , unmatchedStates } = extractEvaluationSamples < ProgramState , Opcodes > ( { evaluationRange , nodes , trace , } ) ; const childSamples = nodes . reduce < EvaluationSample < ProgramState , Opcodes > [ ] > ( ( all , node ) => [ ... all , ... extractEvaluations ( node ) ] , [ ] ) ; const endingOrderedSamples = [ ... samples , ... childSamples ] . sort ( ( a , b ) => { const linesOrdered = a . range . endLineNumber - b . range . endLineNumber ; return linesOrdered === 0 ? a . range . endColumn - b . range . endColumn : linesOrdered ; } ) ; return { samples : endingOrderedSamples , unmatchedStates , } ; } ; const stateIsExecuting = ( state ) => state . executionStack . every ( ( item ) => item ) ; remarks param samples param evaluationBegins export const extractUnexecutedRanges = < ProgramState extends AuthenticationProgramStateExecutionStack , Opcodes = number > ( samples , evaluationBegins = '1,1' ) => { const reduced = samples . reduce < { precedingStateSkipsByEvaluation : { [ evaluationStartLineAndColumn ] : boolean ; } ; unexecutedRanges : Range [ ] ; } > ( ( all , sample ) => { const { precedingStateSkipsByEvaluation , unexecutedRanges } = all ; const currentEvaluationStartLineAndColumn = `${ sample . evaluationRange . startLineNumber },${ sample . evaluationRange . startColumn }` ; const precedingStateSkips = precedingStateSkipsByEvaluation [ currentEvaluationStartLineAndColumn ] ; const endsWithSkip = ! stateIsExecuting ( sample . state ) ; const sampleHasNoExecutedInstructions = endsWithSkip && sample . internalStates . every ( ( group ) => ! stateIsExecuting ( group . state ) ) ; if ( precedingStateSkips && sampleHasNoExecutedInstructions ) { return { precedingStateSkipsByEvaluation : { ... precedingStateSkipsByEvaluation , [ currentEvaluationStartLineAndColumn ] : true , } , unexecutedRanges : [ ... unexecutedRanges , sample . range ] , } ; } return { precedingStateSkipsByEvaluation : { ... precedingStateSkipsByEvaluation , [ currentEvaluationStartLineAndColumn ] : endsWithSkip , } , unexecutedRanges , } ; } , { precedingStateSkipsByEvaluation : { [ evaluationBegins ] : false , } , unexecutedRanges : [ ] , } ) ; const canHaveContainedRanges = 2 ; const containedRangesExcluded = reduced . unexecutedRanges . length < canHaveContainedRanges ? reduced . unexecutedRanges : reduced . unexecutedRanges . slice ( 0 , - 1 ) . reduceRight < Range [ ] > ( ( all , range ) => { if ( containsRange ( all [ 0 ] , range ) ) { return all ; } return [ range , ... all ] ; } , [ reduced . unexecutedRanges [ reduced . unexecutedRanges . length - 1 ] ] ) ; return containedRangesExcluded ; } ;