'use strict' ; import { window , OutputChannel , ExtensionContext , TextDocument , Diagnostic , DiagnosticSeverity } from 'vscode' ; import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper" ; import * as process from 'child_process' ; import { ProblemInfo , DomainInfo , HappeningsInfo , Happening , PlanStep } from 'pddl-workspace' ; import { HappeningsToValStep , Util } from 'ai-planning-val' ; import { PddlConfiguration } from '../configuration/configuration' ; import { dirname } from 'path' ; import { DomainAndProblem , isHappenings , getDomainAndProblemForHappenings } from '../workspace/workspaceUtils' ; import { createRangeFromLine , createDiagnostic , createDiagnosticFromParsingProblem } from './validatorUtils' ; import { CodePddlWorkspace } from '../workspace/CodePddlWorkspace' ; export const PDDL_HAPPENINGS_VALIDATE = 'pddl.happenings.validate' ; /**
 * Delegate for parsing and validating Plan Happenings files.
 */ export class HappeningsValidator { constructor ( private output , public codePddlWorkspace , public plannerConfiguration , context ) { context . subscriptions . push ( instrumentOperationAsVsCodeCommand ( PDDL_HAPPENINGS_VALIDATE , async ( ) => { if ( window . activeTextEditor && isHappenings ( window . activeTextEditor . document ) ) { try { const outcome = await this . validateTextDocument ( window . activeTextEditor . document ) ; if ( outcome . getError ( ) ) { window . showErrorMessage ( outcome . getError ( ) ! ) ; } } catch ( ex ) { window . showErrorMessage ( "Happenings validation failed: " + ex ) ; return ; } } else { window . showErrorMessage ( "There is no happenings file open." ) ; return ; } } ) ) ; } async validateTextDocument ( happeningsDocument ) { const planFileInfo = await this . codePddlWorkspace . upsertAndParseFile ( happeningsDocument ) as HappeningsInfo ; if ( ! planFileInfo ) { throw new Error ( "Cannot open or parse plan file: " + happeningsDocument . uri . fsPath ) ; } return this . validateAndReportDiagnostics ( planFileInfo , true , ( ) => { } , ( ) => { } ) ; } async validateAndReportDiagnostics ( happeningsInfo , showOutput , onSuccess , onError ) { if ( happeningsInfo . getParsingProblems ( ) . length > 0 ) { const diagnostics = happeningsInfo . getParsingProblems ( ) . map ( problem => createDiagnosticFromParsingProblem ( problem ) ) ; const outcome = HappeningsValidationOutcome . failedWithDiagnostics ( happeningsInfo , diagnostics ) ; onSuccess ( outcome . getDiagnostics ( ) ) ; return outcome ; } let context = null ; try { context = getDomainAndProblemForHappenings ( happeningsInfo , this . codePddlWorkspace . pddlWorkspace ) ; } catch ( err ) { const outcome = HappeningsValidationOutcome . info ( happeningsInfo , err as Error ) ; onSuccess ( outcome . getDiagnostics ( ) ) ; return outcome ; } const actionNameDiagnostics = this . validateActionNames ( context . domain , context . problem , happeningsInfo ) ; if ( actionNameDiagnostics . length ) { const errorOutcome = HappeningsValidationOutcome . failedWithDiagnostics ( happeningsInfo , actionNameDiagnostics ) ; onSuccess ( errorOutcome . getDiagnostics ( ) ) ; return errorOutcome ; } const actionTimeDiagnostics = this . validateActionTimes ( happeningsInfo ) ; if ( actionTimeDiagnostics . length ) { const errorOutcome = HappeningsValidationOutcome . failedWithDiagnostics ( happeningsInfo , actionTimeDiagnostics ) ; onSuccess ( errorOutcome . getDiagnostics ( ) ) ; return errorOutcome ; } const valStepPath = await this . plannerConfiguration . getValStepPath ( ) ; if ( ! valStepPath ) { onSuccess ( HappeningsValidationOutcome . unknown ( happeningsInfo , "ValStep not configured." ) . getDiagnostics ( ) ) ; } const domainFilePath = await Util . toPddlFile ( 'domain' , context . domain . getText ( ) ) ; const problemFilePath = await Util . toPddlFile ( 'problem' , context . problem . getText ( ) ) ; const happeningsConverter = new HappeningsToValStep ( ) ; happeningsConverter . convertAllHappenings ( happeningsInfo ) ; const valSteps = happeningsConverter . getExportedText ( true ) ; const args = [ domainFilePath , problemFilePath ] ; const child = process . spawnSync ( valStepPath ! , args , { cwd : dirname ( happeningsInfo . fileUri . fsPath ) , input : valSteps } ) ; if ( showOutput ) { this . output . appendLine ( valStepPath + ' ' + args . join ( ' ' ) ) ; } const output = child . stdout . toString ( ) ; if ( showOutput ) { this . output . appendLine ( output ) ; } if ( showOutput && child . stderr && child . stderr . length ) { this . output . append ( 'Error:' ) ; this . output . appendLine ( child . stderr . toString ( ) ) ; } const outcome = this . analyzeOutput ( happeningsInfo , child . error , output ) ; if ( child . error ) { if ( showOutput ) { this . output . appendLine ( `Error: name=${ child . error . name }, message=${ child . error . message }` ) ; } onError ( child . error . name ) ; } else { onSuccess ( outcome . getDiagnostics ( ) ) ; } if ( showOutput ) { this . output . appendLine ( `Exit code: ${ child . status }` ) ; this . output . show ( ) ; } return outcome ; } analyzeOutput ( happeningsInfo , error , _output ) { if ( error ) { return HappeningsValidationOutcome . failed ( happeningsInfo , error ) ; } return HappeningsValidationOutcome . valid ( happeningsInfo ) ; } param domain param problem param happeningsInfo validateActionNames ( domain , problem , happeningsInfo ) { return happeningsInfo . getHappenings ( ) . filter ( happening => ! this . isDomainAction ( domain , problem , happening ) ) . map ( happening => new Diagnostic ( createRangeFromLine ( happening . lineIndex ?? 0 ) , `Action '${ happening . getAction ( ) }' not known by the domain '${ domain . name }'` , DiagnosticSeverity . Error ) ) ; } param domain param problem param happeningsInfo validateActionTimes ( happeningsInfo ) { return happeningsInfo . getHappenings ( ) . slice ( 1 ) . filter ( ( happening , index ) => ! this . isTimeMonotonociallyIncreasing ( happeningsInfo . getHappenings ( ) [ index ] , happening ) ) . map ( happening => new Diagnostic ( createRangeFromLine ( happening . lineIndex ?? 0 ) , `Action '${ happening . getAction ( ) }' time ${ happening . getTime ( ) } is before the preceding action time` , DiagnosticSeverity . Error ) ) ; } private isDomainAction ( domain , problem , happening ) { const allActionNames = domain . getActions ( ) . map ( a => a . name ?. toLowerCase ( ) ?? 'undefined' ) . concat ( problem . getSupplyDemands ( ) . map ( sd => sd . getName ( ) . toLowerCase ( ) ) ) ; return allActionNames . includes ( happening . getAction ( ) . toLowerCase ( ) ) ; } private isTimeMonotonociallyIncreasing ( first , second ) { return first . getTime ( ) <= second . getTime ( ) ; } } class HappeningsValidationOutcome { constructor ( public readonly happeningsInfo , private diagnostics , private error ? ) { } getError ( ) { return this . error ; } getDiagnostics ( ) { const diagnostics = new Map < string , Diagnostic [ ] > ( ) ; diagnostics . set ( this . happeningsInfo . fileUri . toString ( ) , this . diagnostics ) ; return diagnostics ; } static goalNotAttained ( happeningsInfo ) { const errorLine = happeningsInfo . getHappenings ( ) . length > 0 ? happeningsInfo . getHappenings ( ) . slice ( - 1 ) . pop ( ) ?. lineIndex ?? 0 + 1 : 0 ; const error = "Plan does not reach the goal." ; const diagnostics = [ createDiagnostic ( errorLine , 0 , error , DiagnosticSeverity . Warning ) ] ; return new HappeningsValidationOutcome ( happeningsInfo , diagnostics , error ) ; } /**
     * Creates validation outcomes for invalid plan i.e. plans that do not parse or do not correspond to the domain/problem file.
     */ static invalidPlanDescription ( happeningsInfo ) { const error = "Invalid plan description." ; const diagnostics = [ createDiagnostic ( 0 , 0 , error , DiagnosticSeverity . Error ) ] ; return new HappeningsValidationOutcome ( happeningsInfo , diagnostics , error ) ; } /**
     * Creates validation outcomes for valid plan, which does not reach the goal.
     */ static valid ( happeningsInfo ) { return new HappeningsValidationOutcome ( happeningsInfo , [ ] , undefined ) ; } static failed ( happeningsInfo , error ) { const message = "Validate tool failed. " + error . message ; const diagnostics = [ createDiagnostic ( 0 , 0 , message , DiagnosticSeverity . Error ) ] ; return new HappeningsValidationOutcome ( happeningsInfo , diagnostics , message ) ; } static info ( happeningsInfo , error ) { const message = error . message ; const diagnostics = [ createDiagnostic ( 0 , 0 , message , DiagnosticSeverity . Information ) ] ; return new HappeningsValidationOutcome ( happeningsInfo , diagnostics , message ) ; } static failedWithDiagnostics ( happeningsInfo , diagnostics ) { return new HappeningsValidationOutcome ( happeningsInfo , diagnostics ) ; } static failedAtTime ( happeningsInfo , timeStamp , repairHints ) { let errorLine = 0 ; const stepAtTimeStamp = happeningsInfo . getHappenings ( ) . find ( happening => PlanStep . equalsWithin ( happening . getTime ( ) , timeStamp , 1e-4 ) ) ; if ( stepAtTimeStamp && stepAtTimeStamp . lineIndex !== undefined ) { errorLine = stepAtTimeStamp . lineIndex ; } const diagnostics = repairHints . map ( hint => new Diagnostic ( createRangeFromLine ( errorLine ) , hint , DiagnosticSeverity . Warning ) ) ; return new HappeningsValidationOutcome ( happeningsInfo , diagnostics ) ; } static unknown ( happeningsInfo , message = "Unknown error." ) { const diagnostics = [ new Diagnostic ( createRangeFromLine ( 0 ) , message , DiagnosticSeverity . Warning ) ] ; return new HappeningsValidationOutcome ( happeningsInfo , diagnostics , message ) ; } }