From 73841bda7492404767cff2787311173175ffbdf0 Mon Sep 17 00:00:00 2001 From: gsv Date: Wed, 6 Nov 2024 20:16:24 +0300 Subject: [PATCH 01/12] All about AI searcher in single one commit. --- .DS_Store | Bin 8196 -> 0 bytes .travis.yml | 3 +- VSharp.API/VSharp.cs | 8 +- VSharp.API/VSharpOptions.cs | 21 +- VSharp.Explorer/AISearcher.fs | 322 ++++++++++++++++ VSharp.Explorer/Explorer.fs | 30 +- VSharp.Explorer/Options.fs | 29 ++ VSharp.Explorer/Statistics.fs | 4 + VSharp.Explorer/VSharp.Explorer.fsproj | 6 + VSharp.Explorer/models/model.onnx | Bin 0 -> 392513 bytes VSharp.IL/CFG.fs | 273 +++++++++++--- VSharp.IL/CallGraph.fs | 0 VSharp.IL/ILRewriter.fs | 12 +- VSharp.IL/MethodBody.fs | 12 +- VSharp.IL/OpCodes.fs | 4 +- VSharp.IL/Serializer.fs | 350 ++++++++++++++++++ VSharp.IL/VSharp.IL.fsproj | 5 +- VSharp.ML.GameServer.Runner/Main.fs | 262 +++++++++++++ .../VSharp.ML.GameServer.Runner.fsproj | 29 ++ VSharp.ML.GameServer/Messages.fs | 291 +++++++++++++++ .../VSharp.ML.GameServer.fsproj | 19 + VSharp.Runner/RunnerProgram.cs | 141 ++++++- VSharp.Runner/VSharp.Runner.csproj | 1 + VSharp.SILI/CILState.fs | 46 ++- VSharp.SILI/Interpreter.fs | 14 +- VSharp.SILI/VSharp.SILI.fsproj | 2 +- VSharp.Test/Benchmarks/Benchmarks.cs | 4 +- VSharp.Test/IntegrationTests.cs | 30 +- VSharp.Test/Tests/ControlFlow.cs | 17 +- VSharp.Test/Tests/LoanExam.cs | 2 +- VSharp.Test/Tests/Mocking.cs | 1 - VSharp.Utils/GraphUtils.fs | 6 + VSharp.Utils/Prelude.fs | 4 +- VSharp.Utils/UnitTests.fs | 4 +- VSharp.sln | 40 +- 35 files changed, 1864 insertions(+), 128 deletions(-) delete mode 100644 .DS_Store create mode 100644 VSharp.Explorer/AISearcher.fs create mode 100644 VSharp.Explorer/models/model.onnx delete mode 100644 VSharp.IL/CallGraph.fs create mode 100644 VSharp.IL/Serializer.fs create mode 100644 VSharp.ML.GameServer.Runner/Main.fs create mode 100644 VSharp.ML.GameServer.Runner/VSharp.ML.GameServer.Runner.fsproj create mode 100644 VSharp.ML.GameServer/Messages.fs create mode 100644 VSharp.ML.GameServer/VSharp.ML.GameServer.fsproj diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index e330b3264e4bfea39576171b8647a56910e4ee14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM(P|Sx6upy1-J~rBDHsr8UwjK8K}+d_5YzM_pdfCo4=U}hS=zv6H)J;rHAeF8 z7wCh(;CJ`|;>1^7fEol`;sssgQ91*`&A0jq#jz$)-BDu8D;o3rA%ug+R;6|f5YmkRLy z;G!`$G)@)DtpiS$0KhzkrJ;{HKyqA-4UJQUl8TtBdJvi_bcrDp9p_!14s2+gDpYh5 zicUhWEOdn;s4uQ=IAM9)TM~h`55B15o61U(WV&8 z5D{ajj#F?7N2T(3e7AP5;?}oolZrdu-QB6U+dH+% zq~P4VQ@`In?w!9LznQ#!kBfu}y*Cpl)vSHM!7qh7^9FI0#4I?+DH=kjxTW#DxH2I> z&LrfmWgOg^7G6qaNVO22lPsnaN-;i!N8%V!IcaC3ZW>WNM2qB?#bC-*on@LSc7WAV z%I4P-oS47Cx~z$@LL?&{HjJCFYPa}ih-shy5X72QV2J{2YOKul|2}qT`yOD4NOs67 zaLE-A`Igse;v#?kelxF^Yi$?p7>$kRO%+NCoGizIvK$9q{9%Z`i>b_MXq+m<2+Y0+ NkTO_j75J+P`~U)XNT~n- diff --git a/.travis.yml b/.travis.yml index 072ac0b86..ba4bb3ae0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: csharp mono: none -dotnet: 3.0.100 +dotnet: 6.0 solution: VSharp.sln install: - dotnet restore + - dotnet tool restore script: - dotnet test -c Release diff --git a/VSharp.API/VSharp.cs b/VSharp.API/VSharp.cs index 07743fd0a..434b3a874 100644 --- a/VSharp.API/VSharp.cs +++ b/VSharp.API/VSharp.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Text; +using Microsoft.FSharp.Core; using VSharp.CoverageTool; using VSharp.CSharpUtils; using VSharp.Interpreter.IL; @@ -178,6 +179,7 @@ private static Statistics StartExploration( explorationMode: explorationMode.NewTestCoverageMode( coverageZone, options.Timeout > 0 ? searchMode.NewFairMode(baseSearchMode) : baseSearchMode + ), recThreshold: options.RecursionThreshold, solverTimeout: options.SolverTimeout, @@ -188,8 +190,9 @@ private static Statistics StartExploration( checkAttributes: true, stopOnCoverageAchieved: 100, randomSeed: options.RandomSeed, - stepsLimit: options.StepsLimit - ); + stepsLimit: options.StepsLimit, + aiAgentTrainingOptions: options.AIAgentTrainingOptions == null ? FSharpOption.None :FSharpOption.Some(options.AIAgentTrainingOptions), + pathToModel: options.PathToModel == null ? FSharpOption.None : FSharpOption.Some(options.PathToModel)); var fuzzerOptions = new FuzzerOptions( @@ -279,6 +282,7 @@ private static searchMode ToSiliMode(this SearchStrategy searchStrategy) SearchStrategy.ExecutionTree => searchMode.ExecutionTreeMode, SearchStrategy.ExecutionTreeContributedCoverage => searchMode.NewInterleavedMode(searchMode.ExecutionTreeMode, 1, searchMode.ContributedCoverageMode, 1), SearchStrategy.Interleaved => searchMode.NewInterleavedMode(searchMode.ShortestDistanceBasedMode, 1, searchMode.ContributedCoverageMode, 9), + SearchStrategy.AI => searchMode.AIMode, _ => throw new UnreachableException("Unknown search strategy") }; } diff --git a/VSharp.API/VSharpOptions.cs b/VSharp.API/VSharpOptions.cs index 91541e2e9..9132eaf6f 100644 --- a/VSharp.API/VSharpOptions.cs +++ b/VSharp.API/VSharpOptions.cs @@ -1,4 +1,6 @@ using System.IO; +using Microsoft.FSharp.Core; +using VSharp.Explorer; namespace VSharp; @@ -38,7 +40,8 @@ public enum SearchStrategy /// /// Interleaves and strategies. /// - Interleaved + Interleaved, + AI } /// @@ -96,6 +99,7 @@ public readonly record struct VSharpOptions private const bool DefaultReleaseBranches = true; private const int DefaultRandomSeed = -1; private const uint DefaultStepsLimit = 0; + private const string DefaultPathToModel = "models/model.onnx"; public readonly int Timeout = DefaultTimeout; public readonly int SolverTimeout = DefaultSolverTimeout; @@ -109,6 +113,8 @@ public readonly record struct VSharpOptions public readonly bool ReleaseBranches = DefaultReleaseBranches; public readonly int RandomSeed = DefaultRandomSeed; public readonly uint StepsLimit = DefaultStepsLimit; + public readonly AIAgentTrainingOptions AIAgentTrainingOptions = null; + public readonly string PathToModel = DefaultPathToModel; /// /// Symbolic virtual machine options. @@ -125,6 +131,8 @@ public readonly record struct VSharpOptions /// If true and timeout is specified, a part of allotted time in the end is given to execute remaining states without branching. /// Fixed seed for random operations. Used if greater than or equal to zero. /// Number of symbolic machine steps to stop execution after. Zero value means no limit. + /// Settings for AI searcher training. + /// Path to ONNX file with model to use in AI searcher. public VSharpOptions( int timeout = DefaultTimeout, int solverTimeout = DefaultSolverTimeout, @@ -137,7 +145,9 @@ public VSharpOptions( ExplorationMode explorationMode = DefaultExplorationMode, bool releaseBranches = DefaultReleaseBranches, int randomSeed = DefaultRandomSeed, - uint stepsLimit = DefaultStepsLimit) + uint stepsLimit = DefaultStepsLimit, + AIAgentTrainingOptions aiAgentTrainingOptions = null, + string pathToModel = DefaultPathToModel) { Timeout = timeout; SolverTimeout = solverTimeout; @@ -151,6 +161,8 @@ public VSharpOptions( ReleaseBranches = releaseBranches; RandomSeed = randomSeed; StepsLimit = stepsLimit; + AIAgentTrainingOptions = aiAgentTrainingOptions; + PathToModel = pathToModel; } /// @@ -158,4 +170,9 @@ public VSharpOptions( /// public DirectoryInfo RenderedTestsDirectoryInfo => Directory.Exists(RenderedTestsDirectory) ? new DirectoryInfo(RenderedTestsDirectory) : null; + + public string GetDefaultPathToModel() + { + return DefaultPathToModel; + } } diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs new file mode 100644 index 000000000..3ab737062 --- /dev/null +++ b/VSharp.Explorer/AISearcher.fs @@ -0,0 +1,322 @@ +namespace VSharp.Explorer + +open System.Collections.Generic +open Microsoft.ML.OnnxRuntime +open VSharp +open VSharp.IL.Serializer +open VSharp.ML.GameServer.Messages + +type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option) = + let stepsToSwitchToAI = + match aiAgentTrainingOptions with + | None -> 0u + | Some options -> options.stepsToSwitchToAI + + let stepsToPlay = + match aiAgentTrainingOptions with + | None -> 0u + | Some options -> options.stepsToPlay + + let mutable lastCollectedStatistics = Statistics() + let mutable defaultSearcherSteps = 0u + let mutable (gameState:Option) = None + let mutable useDefaultSearcher = stepsToSwitchToAI > 0u + let mutable afterFirstAIPeek = false + let mutable incorrectPredictedStateId = false + let defaultSearcher = + match aiAgentTrainingOptions with + | None -> BFSSearcher() :> IForwardSearcher + | Some options -> + match options.defaultSearchStrategy with + | BFSMode -> BFSSearcher() :> IForwardSearcher + | DFSMode -> DFSSearcher() :> IForwardSearcher + | x -> failwithf $"Unexpected default searcher {x}. DFS and BFS supported for now." + let mutable stepsPlayed = 0u + let isInAIMode () = (not useDefaultSearcher) && afterFirstAIPeek + let q = ResizeArray<_>() + let availableStates = HashSet<_>() + let updateGameState (delta:GameState) = + match gameState with + | None -> + gameState <- Some delta + | Some s -> + let updatedBasicBlocks = delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet + let updatedStates = delta.States |> Array.map (fun s -> s.Id) |> HashSet + let vertices = + s.GraphVertices + |> Array.filter (fun v -> updatedBasicBlocks.Contains v.Id |> not) + |> ResizeArray<_> + vertices.AddRange delta.GraphVertices + let edges = + s.Map + |> Array.filter (fun e -> updatedBasicBlocks.Contains e.VertexFrom |> not) + |> ResizeArray<_> + edges.AddRange delta.Map + let activeStates = vertices |> Seq.collect (fun v -> v.States) |> HashSet + + let states = + let part1 = + s.States + |> Array.filter (fun s -> activeStates.Contains s.Id && (not <| updatedStates.Contains s.Id)) + |> ResizeArray<_> + + part1.AddRange delta.States + + part1.ToArray() + |> Array.map (fun s -> State(s.Id + , s.Position + , s.PathConditionSize + , s.VisitedAgainVertices + , s.VisitedNotCoveredVerticesInZone + , s.VisitedNotCoveredVerticesOutOfZone + , s.StepWhenMovedLastTime + , s.InstructionsVisitedInCurrentBlock + , s.History + , s.Children |> Array.filter activeStates.Contains) + ) + + gameState <- Some <| GameState (vertices.ToArray(), states, edges.ToArray()) + + + let init states = + q.AddRange states + defaultSearcher.Init q + states |> Seq.iter (availableStates.Add >> ignore) + let reset () = + defaultSearcher.Reset() + defaultSearcherSteps <- 0u + lastCollectedStatistics <- Statistics() + gameState <- None + afterFirstAIPeek <- false + incorrectPredictedStateId <- false + useDefaultSearcher <- stepsToSwitchToAI > 0u + q.Clear() + availableStates.Clear() + let update (parent, newSates) = + if useDefaultSearcher + then defaultSearcher.Update (parent,newSates) + newSates |> Seq.iter (availableStates.Add >> ignore) + let remove state = + if useDefaultSearcher + then defaultSearcher.Remove state + let removed = availableStates.Remove state + assert removed + for bb in state._history do bb.Key.AssociatedStates.Remove state |> ignore + + let inTrainMode = aiAgentTrainingOptions.IsSome + + let pick selector = + if useDefaultSearcher + then + defaultSearcherSteps <- defaultSearcherSteps + 1u + if Seq.length availableStates > 0 + then + let gameStateDelta = collectGameStateDelta () + updateGameState gameStateDelta + let statistics = computeStatistics gameState.Value + Application.applicationGraphDelta.Clear() + lastCollectedStatistics <- statistics + useDefaultSearcher <- defaultSearcherSteps < stepsToSwitchToAI + defaultSearcher.Pick() + elif Seq.length availableStates = 0 + then None + elif Seq.length availableStates = 1 + then Some (Seq.head availableStates) + else + let gameStateDelta = collectGameStateDelta () + updateGameState gameStateDelta + let statistics = computeStatistics gameState.Value + if isInAIMode() + then + let reward = computeReward lastCollectedStatistics statistics + oracle.Feedback (Feedback.MoveReward reward) + Application.applicationGraphDelta.Clear() + if inTrainMode && stepsToPlay = stepsPlayed + then None + else + let toPredict = + if inTrainMode && stepsPlayed > 0u + then gameStateDelta + else gameState.Value + let stateId = oracle.Predict toPredict + afterFirstAIPeek <- true + let state = availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) + lastCollectedStatistics <- statistics + stepsPlayed <- stepsPlayed + 1u + match state with + | Some state -> + Some state + | None -> + incorrectPredictedStateId <- true + oracle.Feedback (Feedback.IncorrectPredictedStateId stateId) + None + new (pathToONNX:string) = + let numOfVertexAttributes = 7 + let numOfStateAttributes = 7 + let numOfHistoryEdgeAttributes = 2 + let createOracle (pathToONNX: string) = + let sessionOptions = new SessionOptions() + sessionOptions.ExecutionMode <- ExecutionMode.ORT_PARALLEL + sessionOptions.GraphOptimizationLevel <- GraphOptimizationLevel.ORT_ENABLE_ALL + let session = new InferenceSession(pathToONNX, sessionOptions) + let runOptions = new RunOptions() + let feedback (x:Feedback) = () + let predict (gameState:GameState) = + let stateIds = Dictionary,int>() + let verticesIds = Dictionary,int>() + let networkInput = + let res = Dictionary<_,_>() + let gameVertices = + let shape = [| int64 gameState.GraphVertices.Length; numOfVertexAttributes |] + let attributes = Array.zeroCreate (gameState.GraphVertices.Length * numOfVertexAttributes) + for i in 0..gameState.GraphVertices.Length - 1 do + let v = gameState.GraphVertices.[i] + verticesIds.Add(v.Id, i) + let j = i * numOfVertexAttributes + attributes.[j] <- float32 <| if v.InCoverageZone then 1u else 0u + attributes.[j + 1] <- float32 <| v.BasicBlockSize + attributes.[j + 2] <- float32 <| if v.CoveredByTest then 1u else 0u + attributes.[j + 3] <- float32 <| if v.VisitedByState then 1u else 0u + attributes.[j + 4] <- float32 <| if v.TouchedByState then 1u else 0u + attributes.[j + 5] <- float32 <| if v.ContainsCall then 1u else 0u + attributes.[j + 6] <- float32 <| if v.ContainsThrow then 1u else 0u + OrtValue.CreateTensorValueFromMemory(attributes, shape) + + let states, numOfParentOfEdges, numOfHistoryEdges = + let mutable numOfParentOfEdges = 0 + let mutable numOfHistoryEdges = 0 + let shape = [| int64 gameState.States.Length; numOfStateAttributes |] + let attributes = Array.zeroCreate (gameState.States.Length * numOfStateAttributes) + for i in 0..gameState.States.Length - 1 do + let v = gameState.States.[i] + numOfHistoryEdges <- numOfHistoryEdges + v.History.Length + numOfParentOfEdges <- numOfParentOfEdges + v.Children.Length + stateIds.Add(v.Id,i) + let j = i * numOfStateAttributes + attributes.[j] <- float32 v.Position + attributes.[j + 1] <- float32 v.PathConditionSize + attributes.[j + 2] <- float32 v.VisitedAgainVertices + attributes.[j + 3] <- float32 v.VisitedNotCoveredVerticesInZone + attributes.[j + 4] <- float32 v.VisitedNotCoveredVerticesOutOfZone + attributes.[j + 5] <- float32 v.StepWhenMovedLastTime + attributes.[j + 6] <- float32 v.InstructionsVisitedInCurrentBlock + OrtValue.CreateTensorValueFromMemory(attributes, shape) + ,numOfParentOfEdges + ,numOfHistoryEdges + + let vertexToVertexEdgesIndex,vertexToVertexEdgesAttributes = + let shapeOfIndex = [| 2L; gameState.Map.Length |] + let shapeOfAttributes = [| int64 gameState.Map.Length |] + let index = Array.zeroCreate (2 * gameState.Map.Length) + let attributes = Array.zeroCreate gameState.Map.Length + gameState.Map + |> Array.iteri ( + fun i e -> + index[i] <- int64 verticesIds[e.VertexFrom] + index[gameState.Map.Length + i] <- int64 verticesIds[e.VertexTo] + attributes[i] <- int64 e.Label.Token + ) + + OrtValue.CreateTensorValueFromMemory(index, shapeOfIndex) + , OrtValue.CreateTensorValueFromMemory(attributes, shapeOfAttributes) + + let historyEdgesIndex_vertexToState, historyEdgesAttributes, parentOfEdges = + let shapeOfParentOf = [| 2L; numOfParentOfEdges |] + let parentOf = Array.zeroCreate (2 * numOfParentOfEdges) + let shapeOfHistory = [|2L; numOfHistoryEdges|] + let historyIndex_vertexToState = Array.zeroCreate (2 * numOfHistoryEdges) + let shapeOfHistoryAttributes = [| int64 numOfHistoryEdges; int64 numOfHistoryEdgeAttributes |] + let historyAttributes = Array.zeroCreate (2 * numOfHistoryEdges) + let mutable firstFreePositionInParentsOf = 0 + let mutable firstFreePositionInHistoryIndex = 0 + let mutable firstFreePositionInHistoryAttributes = 0 + gameState.States + |> Array.iter (fun state -> + state.Children + |> Array.iteri (fun i children -> + let j = firstFreePositionInParentsOf + i + parentOf[j] <- int64 stateIds[state.Id] + parentOf[numOfParentOfEdges + j] <- int64 stateIds[children] + ) + firstFreePositionInParentsOf <- firstFreePositionInParentsOf + state.Children.Length + state.History + |> Array.iteri (fun i historyElem -> + let j = firstFreePositionInHistoryIndex + i + historyIndex_vertexToState[j] <- int64 verticesIds[historyElem.GraphVertexId] + historyIndex_vertexToState[numOfHistoryEdges + j] <- int64 stateIds[state.Id] + + let j = firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * i + historyAttributes[j] <- int64 historyElem.NumOfVisits + historyAttributes[j + 1] <- int64 historyElem.StepWhenVisitedLastTime + ) + firstFreePositionInHistoryIndex <- firstFreePositionInHistoryIndex + state.History.Length + firstFreePositionInHistoryAttributes <- firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * state.History.Length + ) + + OrtValue.CreateTensorValueFromMemory(historyIndex_vertexToState, shapeOfHistory) + , OrtValue.CreateTensorValueFromMemory(historyAttributes, shapeOfHistoryAttributes) + , OrtValue.CreateTensorValueFromMemory(parentOf, shapeOfParentOf) + + let statePosition_stateToVertex, statePosition_vertexToState = + let data_stateToVertex = Array.zeroCreate (2 * gameState.States.Length) + let data_vertexToState = Array.zeroCreate (2 * gameState.States.Length) + let shape = [|2L; gameState.States.Length|] + let mutable firstFreePosition = 0 + gameState.GraphVertices + |> Array.iter ( + fun v -> + v.States + |> Array.iteri (fun i stateId -> + let j = firstFreePosition + i + let stateIndex = int64 stateIds[stateId] + let vertexIndex = int64 verticesIds[v.Id] + data_stateToVertex[j] <- stateIndex + data_stateToVertex[stateIds.Count + j] <- vertexIndex + + data_vertexToState[j] <- vertexIndex + data_vertexToState[stateIds.Count + j] <- stateIndex + ) + firstFreePosition <- firstFreePosition + v.States.Length + ) + OrtValue.CreateTensorValueFromMemory(data_stateToVertex, shape) + ,OrtValue.CreateTensorValueFromMemory(data_vertexToState, shape) + + res.Add ("game_vertex", gameVertices) + res.Add ("state_vertex", states) + + res.Add ("gamevertex_to_gamevertex_index", vertexToVertexEdgesIndex) + res.Add ("gamevertex_to_gamevertex_type", vertexToVertexEdgesAttributes) + + res.Add ("gamevertex_history_statevertex_index", historyEdgesIndex_vertexToState) + res.Add ("gamevertex_history_statevertex_attrs", historyEdgesAttributes) + + res.Add ("gamevertex_in_statevertex", statePosition_vertexToState) + res.Add ("statevertex_parentof_statevertex", parentOfEdges) + + res + + let output = session.Run(runOptions, networkInput, session.OutputNames) + let weighedStates = output[0].GetTensorDataAsSpan().ToArray() + + let id = + weighedStates + |> Array.mapi (fun i v -> i,v) + |> Array.maxBy snd + |> fst + stateIds + |> Seq.find (fun kvp -> kvp.Value = id) + |> fun x -> x.Key + + Oracle(predict,feedback) + + AISearcher(createOracle pathToONNX, None) + + interface IForwardSearcher with + override x.Init states = init states + override x.Pick() = pick (always true) + override x.Pick selector = pick selector + override x.Update (parent, newStates) = update (parent, newStates) + override x.States() = availableStates + override x.Reset() = reset() + override x.Remove cilState = remove cilState + override x.StatesCount with get() = availableStates.Count diff --git a/VSharp.Explorer/Explorer.fs b/VSharp.Explorer/Explorer.fs index 847c78dfc..c1ddc51de 100644 --- a/VSharp.Explorer/Explorer.fs +++ b/VSharp.Explorer/Explorer.fs @@ -12,7 +12,9 @@ open VSharp.Core open VSharp.Interpreter.IL open CilState open VSharp.Explorer +open VSharp.ML.GameServer.Messages open VSharp.Solver +open VSharp.IL.Serializer type IReporter = abstract member ReportFinished: UnitTest -> unit @@ -42,9 +44,11 @@ type private IExplorer = abstract member StartExploration: (Method * state) list -> (Method * EntryPointConfiguration * state) list -> Task type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVMStatistics, reporter: IReporter) = - - let options = explorationOptions.svmOptions - + let options = explorationOptions.svmOptions + let folderToStoreSerializationResult = + match options.aiAgentTrainingOptions with + | None -> "" + | Some options -> getFolderToStoreSerializationResult (Path.GetDirectoryName explorationOptions.outputDirectory.FullName) options.mapName let hasTimeout = explorationOptions.timeout.TotalMilliseconds > 0 let solverTimeout = @@ -92,6 +96,16 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM let rec mkForwardSearcher mode = let getRandomSeedOption() = if options.randomSeed < 0 then None else Some options.randomSeed match mode with + | AIMode -> + match options.aiAgentTrainingOptions with + | Some aiOptions -> + match aiOptions.oracle with + | Some oracle -> AISearcher(oracle, options.aiAgentTrainingOptions) :> IForwardSearcher + | None -> failwith "Empty oracle for AI searcher." + | None -> + match options.pathToModel with + | Some s -> AISearcher s + | None -> failwith "Empty model for AI searcher." | BFSMode -> BFSSearcher() :> IForwardSearcher | DFSMode -> DFSSearcher() :> IForwardSearcher | ShortestDistanceBasedMode -> ShortestDistanceBasedSearcher statistics :> IForwardSearcher @@ -327,7 +341,9 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM goodStates @ iieStates @ errors | _ -> sIsStopped <- true - goodStates @ iieStates @ errors + goodStates @ iieStates @ errors + + s.children <- s.children @ newStates Application.moveState loc s (Seq.cast<_> newStates) statistics.TrackFork s newStates searcher.UpdateStates s newStates @@ -352,7 +368,9 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM Logger.trace "UNSAT for pob = %O and s'.PC = %s" p' (API.Print.PrintPC s'.state.pc) member private x.BidirectionalSymbolicExecution() = + let mutable action = Stop + let pick() = match searcher.Pick() with | Stop -> false @@ -365,6 +383,10 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM match action with | GoFront s -> try + if options.aiAgentTrainingOptions.IsSome && options.aiAgentTrainingOptions.Value.serializeSteps + then + dumpGameState (Path.Combine(folderToStoreSerializationResult, string firstFreeEpisodeNumber)) s.internalId + firstFreeEpisodeNumber <- firstFreeEpisodeNumber + 1 x.Forward(s) with | e -> reportStateInternalFail s e diff --git a/VSharp.Explorer/Options.fs b/VSharp.Explorer/Options.fs index 8a138d72c..e78bac048 100644 --- a/VSharp.Explorer/Options.fs +++ b/VSharp.Explorer/Options.fs @@ -2,6 +2,7 @@ namespace VSharp.Explorer open System.Diagnostics open System.IO +open VSharp.ML.GameServer.Messages type searchMode = | DFSMode @@ -12,6 +13,7 @@ type searchMode = | ExecutionTreeMode | FairMode of searchMode | InterleavedMode of searchMode * int * searchMode * int + | AIMode type coverageZone = | MethodZone @@ -30,6 +32,31 @@ type FuzzerOptions = { coverageZone: coverageZone } +[] +type Oracle = + val Predict: GameState -> uint + val Feedback: Feedback -> unit + new (predict, feedback) = {Predict=predict; Feedback = feedback} + +/// +/// Options used in AI agent training. +/// +/// Number of steps of default searcher prior to switch to AI mode. +/// Number of steps to play in AI mode. +/// Default searcher that will be used to play few initial steps. +/// Determine whether steps should be serialized. +/// Name of map to play. +/// Name of map to play. +type AIAgentTrainingOptions = + { + stepsToSwitchToAI: uint + stepsToPlay: uint + defaultSearchStrategy: searchMode + serializeSteps: bool + mapName: string + oracle: Option + } + type SVMOptions = { explorationMode : explorationMode recThreshold : uint @@ -42,6 +69,8 @@ type SVMOptions = { stopOnCoverageAchieved : int randomSeed : int stepsLimit : uint + aiAgentTrainingOptions: Option + pathToModel: Option } type explorationModeOptions = diff --git a/VSharp.Explorer/Statistics.fs b/VSharp.Explorer/Statistics.fs index f3a1c1037..1e21b3b43 100644 --- a/VSharp.Explorer/Statistics.fs +++ b/VSharp.Explorer/Statistics.fs @@ -13,6 +13,7 @@ open FSharpx.Collections open VSharp open VSharp.Core open VSharp.Interpreter.IL +open VSharp.ML.GameServer.Messages open VSharp.Utils open CilState @@ -159,6 +160,7 @@ type public SVMStatistics(entryMethods : Method seq, generalizeGenericsCoverage member x.TrackStepForward (s : cilState) (ip : instructionPointer) (stackSize : int) = stepsCount <- stepsCount + 1u + s.stepWhenMovedLastTime <- stepsCount * 1u Logger.traceWithTag Logger.stateTraceTag $"{stepsCount} FORWARD: {s.internalId}" let setCoveredIfNeeded (loc : codeLocation) = @@ -227,6 +229,8 @@ type public SVMStatistics(entryMethods : Method seq, generalizeGenericsCoverage let mutable hasNewCoverage = false let blocks = Seq.distinct blocks for block in blocks do + block.BasicBlock.IsCovered <- true + let added = Application.applicationGraphDelta.TouchedBasicBlocks.Add block.BasicBlock let generalizedMethod = generalizeIfNeeded block.method let method = block.method let mutable isNewBlock = false diff --git a/VSharp.Explorer/VSharp.Explorer.fsproj b/VSharp.Explorer/VSharp.Explorer.fsproj index 23cc6c3f3..53fbb88ab 100644 --- a/VSharp.Explorer/VSharp.Explorer.fsproj +++ b/VSharp.Explorer/VSharp.Explorer.fsproj @@ -32,7 +32,11 @@ + + + PreserveNewest + @@ -45,5 +49,7 @@ + + diff --git a/VSharp.Explorer/models/model.onnx b/VSharp.Explorer/models/model.onnx new file mode 100644 index 0000000000000000000000000000000000000000..238d24e77d9c979fd3d7ccbb8e8507403625be72 GIT binary patch literal 392513 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 z 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
T;xeIYt_U+ddUYY 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 -module CallGraph = - let callGraphDistanceFrom = Dictionary>() - let callGraphDistanceTo = Dictionary>() +type coverageType = + | ByTest + | ByEntryPointTest type [] terminalSymbol -type ICfgNode = - inherit IGraphNode - abstract Offset : offset +module CallGraph = + let callGraphDistanceFrom = Dictionary>() + let callGraphDistanceTo = Dictionary>() + let dummyTerminalForCallShortcut = 3 + +type ICfgNode = + inherit IGraphNode + abstract Offset : offset + +type IInterproceduralCfgNode = + inherit IGraphNode + abstract IsCovered : bool with get + abstract IsVisited : bool with get + abstract IsTouched : bool with get + abstract IsGoal : bool with get + abstract IsSink : bool with get + + +[] +type EdgeType = + | CFG + | ShortcutForCall + | Call of int + | Return of int + +[] +type EdgeLabel = + val EdgeType: EdgeType + val IsCovered: bool + val IsVisited: bool + [] type internal temporaryCallInfo = {callee: MethodWithBody; callFrom: offset; returnTo: offset} -type BasicBlock (method: MethodWithBody, startOffset: offset) = +type BasicBlock (method: MethodWithBody, startOffset: offset, id:uint) = let mutable finalOffset = startOffset + let mutable containsCall = false + let mutable containsThrow = false let mutable startOffset = startOffset let mutable isGoal = false let mutable isCovered = false + let mutable isVisited = false + let mutable isSink = false + let mutable visitedInstructions = 0u let associatedStates = HashSet() let incomingCFGEdges = HashSet() let incomingCallEdges = HashSet() let outgoingEdges = Dictionary, HashSet>() + member this.Id = id member this.StartOffset with get () = startOffset and internal set v = startOffset <- v @@ -45,12 +80,23 @@ type BasicBlock (method: MethodWithBody, startOffset: offset) = member this.IncomingCFGEdges = incomingCFGEdges member this.IncomingCallEdges = incomingCallEdges member this.AssociatedStates = associatedStates + member this.VisitedInstructions + with get () = visitedInstructions + and set v = visitedInstructions <- v member this.IsCovered with get () = isCovered and set v = isCovered <- v + member this.IsVisited + with get () = isVisited + and set v = isVisited <- v member this.IsGoal with get () = isGoal and set v = isGoal <- v + member this.IsTouched + with get () = visitedInstructions > 0u + member this.IsSink + with get () = isSink + and set v = isSink <- v member this.HasSiblings with get () = let siblings = HashSet() @@ -60,10 +106,18 @@ type BasicBlock (method: MethodWithBody, startOffset: offset) = siblings.Count > 1 member this.FinalOffset - with get () = finalOffset - and internal set (v : offset) = finalOffset <- v - - member private this.GetInstructions() = + with get () = finalOffset + and set (v: offset) = finalOffset <- v + + member this.ContainsCall + with get () = containsCall + and set (v: bool) = containsCall <- v + + member this.ContainsThrow + with get () = containsThrow + and set (v: bool) = containsThrow <- v + + member this.GetInstructions() = let parsedInstructions = method.ParsedInstructions let mutable instr = parsedInstructions[this.StartOffset] assert(Offset.from (int instr.offset) = this.StartOffset) @@ -83,16 +137,34 @@ type BasicBlock (method: MethodWithBody, startOffset: offset) = member this.BlockSize with get() = this.GetInstructions() |> Seq.length - + interface ICfgNode with member this.OutgoingEdges with get () = - let exists, cfgEdges = outgoingEdges.TryGetValue CfgInfo.TerminalForCFGEdge - if exists - then cfgEdges |> Seq.cast - else Seq.empty + let exists1,cfgEdges = outgoingEdges.TryGetValue CfgInfo.TerminalForCFGEdge + let exists2,cfgSpecialEdges = outgoingEdges.TryGetValue CallGraph.dummyTerminalForCallShortcut + seq{ + if exists1 + then yield! cfgEdges |> Seq.cast + if exists2 + then yield! cfgSpecialEdges |> Seq.cast + } member this.Offset = startOffset - + + interface IInterproceduralCfgNode with + member this.OutgoingEdges + with get () = + seq{ + for kvp in outgoingEdges do + if kvp.Key <> CallGraph.dummyTerminalForCallShortcut + then yield! kvp.Value |> Seq.cast + } + member this.IsCovered with get() = this.IsCovered + member this.IsVisited with get() = this.IsVisited + member this.IsTouched with get() = this.IsTouched + member this.IsGoal with get() = this.IsGoal + member this.IsSink with get() = this.IsSink + and [] CallInfo = val Callee: Method val CallFrom: offset @@ -102,10 +174,10 @@ and [] CallInfo = Callee = callee CallFrom = callFrom ReturnTo = returnTo - } + } -and CfgInfo internal (method : MethodWithBody) = - let () = assert method.HasBody +and CfgInfo internal (method : MethodWithBody, getNextBasicBlockGlobalId: unit -> uint) = + let () = assert method.HasBody let ilBytes = method.ILBytes let exceptionHandlers = method.ExceptionHandlers let sortedBasicBlocks = ResizeArray() @@ -142,7 +214,7 @@ and CfgInfo internal (method : MethodWithBody) = let splitBasicBlock (block : BasicBlock) intermediatePoint = - let newBlock = BasicBlock(method, block.StartOffset) + let newBlock = BasicBlock(method, block.StartOffset, getNextBasicBlockGlobalId()) addBasicBlock newBlock block.StartOffset <- intermediatePoint @@ -170,7 +242,7 @@ and CfgInfo internal (method : MethodWithBody) = let makeNewBasicBlock startVertex = match vertexToBasicBlock[int startVertex] with | None -> - let newBasicBlock = BasicBlock(method, startVertex) + let newBasicBlock = BasicBlock(method, startVertex, getNextBasicBlockGlobalId()) vertexToBasicBlock[int startVertex] <- Some newBasicBlock addBasicBlock newBasicBlock newBasicBlock @@ -213,6 +285,7 @@ and CfgInfo internal (method : MethodWithBody) = let processCall (callee : MethodWithBody) callFrom returnTo k = calls.Add(currentBasicBlock, CallInfo(callee :?> Method, callFrom, returnTo)) currentBasicBlock.FinalOffset <- callFrom + currentBasicBlock.ContainsThrow <- true let newBasicBlock = makeNewBasicBlock returnTo addEdge currentBasicBlock newBasicBlock dfs' newBasicBlock returnTo k @@ -273,7 +346,7 @@ and CfgInfo internal (method : MethodWithBody) = if i.Value <> infinity then distFromNode.Add(i.Key, i.Value) distFromNode) - + let resolveBasicBlockIndex offset = let rec binSearch (sortedOffsets : ResizeArray) offset l r = if l >= r then l @@ -290,11 +363,11 @@ and CfgInfo internal (method : MethodWithBody) = binSearch sortedBasicBlocks offset 0 (sortedBasicBlocks.Count - 1) let resolveBasicBlock offset = sortedBasicBlocks[resolveBasicBlockIndex offset] - + do let startVertices = [| - yield 0 + yield 0 for handler in exceptionHandlers do yield handler.handlerOffset match handler.ehcType with @@ -322,12 +395,12 @@ and CfgInfo internal (method : MethodWithBody) = let basicBlock = resolveBasicBlock offset basicBlock.HasSiblings -and Method internal (m : MethodBase) as this = +and Method internal (m : MethodBase,getNextBasicBlockGlobalId) as this = inherit MethodWithBody(m) let cfg = lazy( if this.HasBody then Logger.trace $"Add CFG for {this}." - let cfg = CfgInfo this + let cfg = CfgInfo(this, getNextBasicBlockGlobalId) Method.ReportCFGLoaded this cfg else internalfailf $"Getting CFG of method {this} without body (extern or abstract)") @@ -445,14 +518,38 @@ and and IGraphTrackableState = abstract member CodeLocation: codeLocation abstract member CallStack: list + abstract member Id: uint + abstract member PathConditionSize: uint + abstract member VisitedNotCoveredVerticesInZone: uint with get + abstract member VisitedNotCoveredVerticesOutOfZone: uint with get + abstract member VisitedAgainVertices: uint with get + abstract member InstructionsVisitedInCurrentBlock : uint with get, set + abstract member History: Dictionary + abstract member Children: array + abstract member StepWhenMovedLastTime: uint with get module public CodeLocation = let hasSiblings (blockStart : codeLocation) = blockStart.method.CFG.HasSiblings blockStart.offset -type ApplicationGraph() = - +type ApplicationGraphDelta() = + let loadedMethods = ResizeArray() + let touchedBasicBlocks = HashSet() + let touchedStates = HashSet() + let removedStates = HashSet() + member this.LoadedMethods = loadedMethods + member this.TouchedBasicBlocks = touchedBasicBlocks + member this.TouchedStates = touchedStates + member this.RemovedStates = removedStates + member this.Clear() = + loadedMethods.Clear() + touchedBasicBlocks.Clear() + touchedStates.Clear() + removedStates.Clear() + +type ApplicationGraph(getNextBasicBlockGlobalId,applicationGraphDelta:ApplicationGraphDelta) = + let dummyTerminalForCallEdge = 1 let dummyTerminalForReturnEdge = 2 @@ -462,43 +559,90 @@ type ApplicationGraph() = let callFrom = callSource.BasicBlock let callTo = calledMethodCfgInfo.EntryPoint let exists, location = callerMethodCfgInfo.Calls.TryGetValue callSource.BasicBlock + if not <| callTo.IncomingCallEdges.Contains callFrom then let mutable returnTo = callFrom // if not exists then it should be from exception mechanism - if not callTarget.method.IsStaticConstructor && exists then + if (not callTarget.method.IsStaticConstructor) && exists then returnTo <- callerMethodCfgInfo.ResolveBasicBlock location.ReturnTo - let exists, callEdges = callFrom.OutgoingEdges.TryGetValue dummyTerminalForCallEdge - if exists then - let added = callEdges.Add callTo - assert added - else callFrom.OutgoingEdges.Add(dummyTerminalForCallEdge, Seq.singleton callTo |> HashSet) - - for returnFrom in calledMethodCfgInfo.Sinks do - let outgoingEdges = returnFrom.OutgoingEdges - let exists, returnEdges = outgoingEdges.TryGetValue dummyTerminalForReturnEdge - if exists then + + let exists, callEdges = callFrom.OutgoingEdges.TryGetValue dummyTerminalForCallEdge + if exists then + let added = callEdges.Add callTo + assert added + else + callFrom.OutgoingEdges.Add(dummyTerminalForCallEdge, HashSet [|callTo|]) + + for returnFrom in calledMethodCfgInfo.Sinks do + let exists,returnEdges = returnFrom.OutgoingEdges.TryGetValue dummyTerminalForReturnEdge + if exists + then let added = returnEdges.Add returnTo assert added - else outgoingEdges.Add(dummyTerminalForReturnEdge, Seq.singleton returnTo |> HashSet) + else + returnFrom.OutgoingEdges.Add(dummyTerminalForReturnEdge, HashSet [|returnTo|]) let added = returnTo.IncomingCallEdges.Add returnFrom assert added - - // 'returnFrom' may be equal to 'callFrom' - callTo.IncomingCallEdges.Add callFrom |> ignore - - let moveState (initialPosition : codeLocation) (stateWithNewPosition : IGraphTrackableState) = - // Not implemented yet - () - - let addStates (parentState : Option) (states : array) = - // Not implemented yet - () + let added = applicationGraphDelta.TouchedBasicBlocks.Add returnFrom + () + + let added = callTo.IncomingCallEdges.Add callFrom + assert added + else () + + let moveState (initialPosition: codeLocation) (stateWithNewPosition: IGraphTrackableState) = + let added = applicationGraphDelta.TouchedBasicBlocks.Add initialPosition.BasicBlock + let added = applicationGraphDelta.TouchedBasicBlocks.Add stateWithNewPosition.CodeLocation.BasicBlock + let removed = initialPosition.BasicBlock.AssociatedStates.Remove stateWithNewPosition + if removed + then + let added = stateWithNewPosition.CodeLocation.BasicBlock.AssociatedStates.Add stateWithNewPosition + assert added + if stateWithNewPosition.History.ContainsKey stateWithNewPosition.CodeLocation.BasicBlock + then + if initialPosition.BasicBlock <> stateWithNewPosition.CodeLocation.BasicBlock + then + let history = stateWithNewPosition.History[stateWithNewPosition.CodeLocation.BasicBlock] + stateWithNewPosition.History[stateWithNewPosition.CodeLocation.BasicBlock] + <- StateHistoryElem(stateWithNewPosition.CodeLocation.BasicBlock.Id, history.NumOfVisits + 1u, stateWithNewPosition.StepWhenMovedLastTime) + else stateWithNewPosition.InstructionsVisitedInCurrentBlock <- stateWithNewPosition.InstructionsVisitedInCurrentBlock + 1u + else stateWithNewPosition.History.Add(stateWithNewPosition.CodeLocation.BasicBlock, StateHistoryElem(stateWithNewPosition.CodeLocation.BasicBlock.Id, 1u, stateWithNewPosition.StepWhenMovedLastTime)) + stateWithNewPosition.CodeLocation.BasicBlock.VisitedInstructions <- + max + stateWithNewPosition.CodeLocation.BasicBlock.VisitedInstructions + (uint ((stateWithNewPosition.CodeLocation.BasicBlock.GetInstructions() + |> Seq.findIndex (fun instr -> Offset.from (int instr.offset) = stateWithNewPosition.CodeLocation.offset)) + 1)) + stateWithNewPosition.CodeLocation.BasicBlock.IsVisited <- + stateWithNewPosition.CodeLocation.BasicBlock.IsVisited + || stateWithNewPosition.CodeLocation.offset = stateWithNewPosition.CodeLocation.BasicBlock.FinalOffset + + let addStates (parentState:Option) (states:array) = + //Option.iter (applicationGraphDelta.TouchedStates.Add >> ignore) parentState + parentState |> Option.iter (fun v -> applicationGraphDelta.TouchedBasicBlocks.Add v.CodeLocation.BasicBlock |> ignore) + for newState in states do + //let added = applicationGraphDelta.TouchedStates.Add newState + let added = applicationGraphDelta.TouchedBasicBlocks.Add newState.CodeLocation.BasicBlock + let added = newState.CodeLocation.BasicBlock.AssociatedStates.Add newState + if newState.History.ContainsKey newState.CodeLocation.BasicBlock + then + let history = newState.History[newState.CodeLocation.BasicBlock] + newState.History[newState.CodeLocation.BasicBlock] <- StateHistoryElem(newState.CodeLocation.BasicBlock.Id, history.NumOfVisits + 1u, newState.StepWhenMovedLastTime) + else newState.History.Add(newState.CodeLocation.BasicBlock, StateHistoryElem(newState.CodeLocation.BasicBlock.Id, 1u, newState.StepWhenMovedLastTime)) + newState.CodeLocation.BasicBlock.VisitedInstructions <- + max + newState.CodeLocation.BasicBlock.VisitedInstructions + (uint ((newState.CodeLocation.BasicBlock.GetInstructions() + |> Seq.findIndex (fun instr -> Offset.from (int instr.offset) = newState.CodeLocation.offset)) + 1)) + newState.CodeLocation.BasicBlock.IsVisited <- + newState.CodeLocation.BasicBlock.IsVisited + || newState.CodeLocation.offset = newState.CodeLocation.BasicBlock.FinalOffset let getShortestDistancesToGoals (states : array) = __notImplemented__() member this.RegisterMethod (method : Method) = assert method.HasBody + applicationGraphDelta.LoadedMethods.Add method member this.AddCallEdge (sourceLocation : codeLocation) (targetLocation : codeLocation) = addCallEdge sourceLocation targetLocation @@ -540,16 +684,27 @@ type NullVisualizer() = override x.VisualizeStep _ _ _ = () module Application = + let applicationGraphDelta = ApplicationGraphDelta() + let mutable basicBlockGlobalCount = 0u + let getNextBasicBlockGlobalId () = + let r = basicBlockGlobalCount + basicBlockGlobalCount <- basicBlockGlobalCount + 1u + r let private methods = ConcurrentDictionary() let private _loadedMethods = ConcurrentDictionary() let loadedMethods = _loadedMethods :> seq<_> - let graph = ApplicationGraph() - // TODO: if visualizer is not set, decline all 'ApplicationGraph' calls + let mutable graph = ApplicationGraph(getNextBasicBlockGlobalId, applicationGraphDelta) let mutable visualizer : IVisualizer = NullVisualizer() + let reset () = + applicationGraphDelta.Clear() + basicBlockGlobalCount <- 0u + graph <- ApplicationGraph(getNextBasicBlockGlobalId, applicationGraphDelta) + methods.Clear() + _loadedMethods.Clear() let getMethod (m : MethodBase) : Method = let desc = Reflection.getMethodDescriptor m - Dict.getValueOrUpdate methods desc (fun () -> Method(m)) + Dict.getValueOrUpdate methods desc (fun () -> Method(m,getNextBasicBlockGlobalId)) let setCoverageZone (zone : Method -> bool) = Method.CoverageZone <- zone @@ -570,8 +725,12 @@ module Application = graph.AddForkedStates toState forked visualizer.VisualizeStep fromLoc toState forked - let terminateState state = + let terminateState (state: IGraphTrackableState) = // TODO: gsv: propagate this into application graph + let removed = state.CodeLocation.BasicBlock.AssociatedStates.Remove state + let added = applicationGraphDelta.TouchedBasicBlocks.Add state.CodeLocation.BasicBlock + //let added = applicationGraphDelta.TouchedStates.Add state + //let added = applicationGraphDelta.RemovedStates state visualizer.TerminateState state let addCallEdge = graph.AddCallEdge @@ -584,4 +743,4 @@ module Application = Method.ReportCFGLoaded <- fun m -> graph.RegisterMethod m let added = _loadedMethods.TryAdd(m, ()) - assert added + assert added \ No newline at end of file diff --git a/VSharp.IL/CallGraph.fs b/VSharp.IL/CallGraph.fs deleted file mode 100644 index e69de29bb..000000000 diff --git a/VSharp.IL/ILRewriter.fs b/VSharp.IL/ILRewriter.fs index e177d7e34..3acc43ca6 100644 --- a/VSharp.IL/ILRewriter.fs +++ b/VSharp.IL/ILRewriter.fs @@ -817,7 +817,7 @@ module internal ILRewriter = offsetToInstr[codeSize] <- il let mutable branch = false - let mutable offset = 0 + let mutable offset = 0 let codeSize : offset = Offset.from codeSize while offset < codeSize do let startOffset = offset @@ -827,11 +827,11 @@ module internal ILRewriter = let size = match op.OperandType with | OperandType.InlineNone - | OperandType.InlineSwitch -> 0 + | OperandType.InlineSwitch -> 0 | OperandType.ShortInlineVar | OperandType.ShortInlineI - | OperandType.ShortInlineBrTarget -> 1 - | OperandType.InlineVar -> 2 + | OperandType.ShortInlineBrTarget -> 1 + | OperandType.InlineVar -> 2 | OperandType.InlineI | OperandType.InlineMethod | OperandType.InlineType @@ -840,9 +840,9 @@ module internal ILRewriter = | OperandType.InlineTok | OperandType.ShortInlineR | OperandType.InlineField - | OperandType.InlineBrTarget -> 4 + | OperandType.InlineBrTarget -> 4 | OperandType.InlineI8 - | OperandType.InlineR -> 8 + | OperandType.InlineR -> 8 | _ -> __unreachable__() if offset + size > codeSize then invalidProgram "IL stream unexpectedly ended!" diff --git a/VSharp.IL/MethodBody.fs b/VSharp.IL/MethodBody.fs index 692f23fe7..25d0bd1ac 100644 --- a/VSharp.IL/MethodBody.fs +++ b/VSharp.IL/MethodBody.fs @@ -389,10 +389,10 @@ module MethodBody = let private operandType2operandSize = [| - 4; 4; 4; 8; 4 - 0; -1; 8; 4; 4 - 4; 4; 4; 4; 2 - 1; 1; 4; 1 + 4; 4; 4; 8; 4 + 0; -1; 8; 4; 4 + 4; 4; 4; 4; 2 + 1; 1; 4; 1 |] let private jumpTargetsForNext (opCode : OpCode) _ (pos : offset) = @@ -420,9 +420,9 @@ module MethodBody = let private inlineSwitch (opCode : OpCode) ilBytes (pos : offset) = let opcodeSize = Offset.from opCode.Size let n = NumberCreator.extractUnsignedInt32 ilBytes (pos + opcodeSize) |> int - let nextInstruction = pos + opcodeSize + 4 * n + 4 + let nextInstruction = pos + opcodeSize + 4 * n + 4 let nextOffsets = - List.init n (fun x -> nextInstruction + Offset.from (NumberCreator.extractInt32 ilBytes (pos + opcodeSize + 4 * (x + 1)))) + List.init n (fun x -> nextInstruction + Offset.from (NumberCreator.extractInt32 ilBytes (pos + opcodeSize + 4 * (x + 1)))) ConditionalBranch(nextInstruction, nextOffsets) let private jumpTargetsForReturn _ _ _ = Return diff --git a/VSharp.IL/OpCodes.fs b/VSharp.IL/OpCodes.fs index e36b14874..4bfc1fce9 100644 --- a/VSharp.IL/OpCodes.fs +++ b/VSharp.IL/OpCodes.fs @@ -233,9 +233,7 @@ type OpCodeValues = | Refanytype = 0xFE1Ds | Readonly_ = 0xFE1Es -[] -type offsets -type offset = int +type offset = int module Offset = let from (x : int) : offset = LanguagePrimitives.Int32WithMeasure x diff --git a/VSharp.IL/Serializer.fs b/VSharp.IL/Serializer.fs new file mode 100644 index 000000000..dc25cfd75 --- /dev/null +++ b/VSharp.IL/Serializer.fs @@ -0,0 +1,350 @@ +module VSharp.IL.Serializer + +open System +open System.Collections.Generic +open System.IO +open System.Reflection +open System.Text.Json +open Microsoft.FSharp.Collections +open VSharp +open VSharp.GraphUtils +open VSharp.ML.GameServer.Messages +open FSharpx.Collections + + +[] +type Statistics = + val CoveredVerticesInZone: uint + val CoveredVerticesOutOfZone: uint + val VisitedVerticesInZone: uint + val VisitedVerticesOutOfZone: uint + val VisitedInstructionsInZone: uint + val TouchedVerticesInZone: uint + val TouchedVerticesOutOfZone: uint + val TotalVisibleVerticesInZone: uint + new (coveredVerticesInZone, coveredVerticesOutOfZone, visitedVerticesInZone, visitedVerticesOutOfZone + , visitedInstructionsInZone, touchedVerticesInZone, touchedVerticesOutOfZone, totalVisibleVerticesInZone) = + { + CoveredVerticesInZone = coveredVerticesInZone + CoveredVerticesOutOfZone = coveredVerticesOutOfZone + VisitedVerticesInZone = visitedVerticesInZone + VisitedVerticesOutOfZone = visitedVerticesOutOfZone + VisitedInstructionsInZone = visitedInstructionsInZone + TouchedVerticesInZone = touchedVerticesInZone + TouchedVerticesOutOfZone = touchedVerticesOutOfZone + TotalVisibleVerticesInZone = totalVisibleVerticesInZone + } + +[] +type StateMetrics = + val StateId: uint + val NextInstructionIsUncoveredInZone: float + val ChildNumber: uint + val VisitedVerticesInZone: uint + val HistoryLength: uint + val DistanceToNearestUncovered: uint + val DistanceToNearestNotVisited: uint + val DistanceToNearestReturn: uint + new + ( + stateId, + nextInstructionIsUncoveredInZone, + childNumber, + visitedVerticesInZone, + historyLength, + distanceToNearestUncovered, + distanceToNearestNotVisited, + distanceTuNearestReturn + ) = + { + StateId = stateId + NextInstructionIsUncoveredInZone = nextInstructionIsUncoveredInZone + ChildNumber = childNumber + VisitedVerticesInZone = visitedVerticesInZone + HistoryLength = historyLength + DistanceToNearestUncovered = distanceToNearestUncovered + DistanceToNearestNotVisited = distanceToNearestNotVisited + DistanceToNearestReturn = distanceTuNearestReturn + } + +[] +type StateInfoToDump = + val StateId: uint + val NextInstructionIsUncoveredInZone: float + val ChildNumberNormalized: float + val VisitedVerticesInZoneNormalized: float + val Productivity: float + val DistanceToReturnNormalized: float + val DistanceToUncoveredNormalized: float + val DistanceToNotVisitedNormalized: float + val ExpectedWeight: float + new + ( + stateId, + nextInstructionIsUncoveredInZone, + childNumber, + visitedVerticesInZone, + productivity, + distanceToReturn, + distanceToUncovered, + distanceToNotVisited + ) = + { + StateId = stateId + NextInstructionIsUncoveredInZone = nextInstructionIsUncoveredInZone + ChildNumberNormalized = childNumber + VisitedVerticesInZoneNormalized = visitedVerticesInZone + Productivity = productivity + DistanceToReturnNormalized = distanceToReturn + DistanceToUncoveredNormalized = distanceToUncovered + DistanceToNotVisitedNormalized = distanceToNotVisited + ExpectedWeight = nextInstructionIsUncoveredInZone + childNumber + visitedVerticesInZone + distanceToReturn + distanceToUncovered + distanceToNotVisited + productivity + } + +let mutable firstFreeEpisodeNumber = 0 + +let calculateStateMetrics interproceduralGraphDistanceFrom (state:IGraphTrackableState) = + let currentBasicBlock = state.CodeLocation.BasicBlock + let distances = + let assembly = currentBasicBlock.Method.Module.Assembly + let callGraphDist = Dict.getValueOrUpdate interproceduralGraphDistanceFrom assembly (fun () -> Dictionary<_, _>()) + Dict.getValueOrUpdate callGraphDist (currentBasicBlock :> IInterproceduralCfgNode) (fun () -> + let dist = incrementalSourcedShortestDistanceBfs (currentBasicBlock :> IInterproceduralCfgNode) callGraphDist + let distFromNode = Dictionary() + for i in dist do + if i.Value <> infinity then + distFromNode.Add(i.Key, i.Value) + distFromNode) + + let childCountStore = Dictionary<_,HashSet<_>>() + let rec childCount (state:IGraphTrackableState) = + if childCountStore.ContainsKey state + then childCountStore[state] + else + let cnt = Array.fold (fun (cnt:HashSet<_>) n -> cnt.UnionWith (childCount n); cnt) (HashSet<_>(state.Children)) state.Children + childCountStore.Add (state,cnt) + cnt + let childNumber = uint (childCount state).Count + let visitedVerticesInZone = state.History |> Seq.fold (fun cnt kvp -> if kvp.Key.IsGoal && not kvp.Key.IsCovered then cnt + 1u else cnt) 0u + let nextInstructionIsUncoveredInZone = + let notTouchedFollowingBlocs, notVisitedFollowingBlocs, notCoveredFollowingBlocs = + let mutable notCoveredBasicBlocksInZone = 0 + let mutable notVisitedBasicBlocksInZone = 0 + let mutable notTouchedBasicBlocksInZone = 0 + let basicBlocks = HashSet<_>() + currentBasicBlock.OutgoingEdges.Values + |> Seq.iter basicBlocks.UnionWith + basicBlocks + |> Seq.iter (fun basicBlock -> if basicBlock.IsGoal + then if not basicBlock.IsTouched + then notTouchedBasicBlocksInZone <- notTouchedBasicBlocksInZone + 1 + elif not basicBlock.IsVisited + then notVisitedBasicBlocksInZone <- notVisitedBasicBlocksInZone + 1 + elif not basicBlock.IsCovered + then notCoveredBasicBlocksInZone <- notCoveredBasicBlocksInZone + 1) + notTouchedBasicBlocksInZone, notVisitedBasicBlocksInZone, notCoveredBasicBlocksInZone + if state.CodeLocation.offset <> currentBasicBlock.FinalOffset && currentBasicBlock.IsGoal + then if not currentBasicBlock.IsVisited + then 1.0 + elif not currentBasicBlock.IsCovered + then 0.5 + else 0.0 + elif state.CodeLocation.offset = currentBasicBlock.FinalOffset + then if notTouchedFollowingBlocs > 0 + then 1.0 + elif notVisitedFollowingBlocs > 0 + then 0.5 + elif notCoveredFollowingBlocs > 0 + then 0.3 + else 0.0 + else 0.0 + + let historyLength = state.History |> Seq.fold (fun cnt kvp -> cnt + kvp.Value.NumOfVisits) 0u + + let getMinBy cond = + let s = distances |> Seq.filter cond + if Seq.isEmpty s + then UInt32.MaxValue + else s |> Seq.minBy (fun x -> x.Value) |> fun x -> x.Value + + let distanceToNearestUncovered = getMinBy (fun kvp -> kvp.Key.IsGoal && not kvp.Key.IsCovered) + let distanceToNearestNotVisited = getMinBy (fun kvp -> kvp.Key.IsGoal && not kvp.Key.IsVisited) + let distanceToNearestReturn = getMinBy (fun kvp -> kvp.Key.IsGoal && kvp.Key.IsSink) + + StateMetrics(state.Id, nextInstructionIsUncoveredInZone, childNumber, visitedVerticesInZone, historyLength + , distanceToNearestUncovered, distanceToNearestNotVisited, distanceToNearestReturn) + +let getFolderToStoreSerializationResult (prefix:string) suffix = + let folderName = Path.Combine(Path.Combine(prefix, "SerializedEpisodes"), suffix) + folderName + +let computeStatistics (gameState:GameState) = + let mutable coveredVerticesInZone = 0u + let mutable coveredVerticesOutOfZone = 0u + let mutable visitedVerticesInZone = 0u + let mutable visitedVerticesOutOfZone = 0u + let mutable visitedInstructionsInZone = 0u + let mutable touchedVerticesInZone = 0u + let mutable touchedVerticesOutOfZone = 0u + let mutable totalVisibleVerticesInZone = 0u + for v in gameState.GraphVertices do + if v.CoveredByTest && v.InCoverageZone + then coveredVerticesInZone <- coveredVerticesInZone + 1u + if v.CoveredByTest && not v.InCoverageZone + then coveredVerticesOutOfZone <- coveredVerticesOutOfZone + 1u + + if v.VisitedByState && v.InCoverageZone + then visitedVerticesInZone <- visitedVerticesInZone + 1u + if v.VisitedByState && not v.InCoverageZone + then visitedVerticesOutOfZone <- visitedVerticesOutOfZone + 1u + + if v.InCoverageZone + then + totalVisibleVerticesInZone <- totalVisibleVerticesInZone + 1u + visitedInstructionsInZone <- visitedInstructionsInZone + v.BasicBlockSize + + if v.TouchedByState && v.InCoverageZone + then touchedVerticesInZone <- touchedVerticesInZone + 1u + if v.TouchedByState && not v.InCoverageZone + then touchedVerticesOutOfZone <- touchedVerticesOutOfZone + 1u + + Statistics(coveredVerticesInZone,coveredVerticesOutOfZone,visitedVerticesInZone,visitedVerticesOutOfZone,visitedInstructionsInZone,touchedVerticesInZone,touchedVerticesOutOfZone, totalVisibleVerticesInZone) + +let collectStatesInfoToDump (basicBlocks:ResizeArray) = + let statesMetrics = ResizeArray<_>() + for currentBasicBlock in basicBlocks do + let interproceduralGraphDistanceFrom = Dictionary>() + currentBasicBlock.AssociatedStates + |> Seq.iter (fun s -> statesMetrics.Add (calculateStateMetrics interproceduralGraphDistanceFrom s)) + + let statesInfoToDump = + let mutable maxVisitedVertices = UInt32.MinValue + let mutable maxChildNumber = UInt32.MinValue + let mutable minDistToUncovered = UInt32.MaxValue + let mutable minDistToNotVisited = UInt32.MaxValue + let mutable minDistToReturn = UInt32.MaxValue + + statesMetrics + |> ResizeArray.iter (fun s -> + if s.VisitedVerticesInZone > maxVisitedVertices + then maxVisitedVertices <- s.VisitedVerticesInZone + if s.ChildNumber > maxChildNumber + then maxChildNumber <- s.ChildNumber + if s.DistanceToNearestUncovered < minDistToUncovered + then minDistToUncovered <- s.DistanceToNearestUncovered + if s.DistanceToNearestNotVisited < minDistToNotVisited + then minDistToNotVisited <- s.DistanceToNearestNotVisited + if s.DistanceToNearestReturn < minDistToReturn + then minDistToReturn <- s.DistanceToNearestReturn + ) + let normalize minV v (sm:StateMetrics) = + if v = minV || (v = UInt32.MaxValue && sm.DistanceToNearestReturn = UInt32.MaxValue) + then 1.0 + elif v = UInt32.MaxValue + then 0.0 + else float (1u + minV) / float (1u + v) + + statesMetrics + |> ResizeArray.map (fun m -> StateInfoToDump (m.StateId + , m.NextInstructionIsUncoveredInZone + , if maxChildNumber = 0u then 0.0 else float m.ChildNumber / float maxChildNumber + , if maxVisitedVertices = 0u then 0.0 else float m.VisitedVerticesInZone / float maxVisitedVertices + , float m.VisitedVerticesInZone / float m.HistoryLength + , normalize minDistToReturn m.DistanceToNearestReturn m + , normalize minDistToUncovered m.DistanceToNearestUncovered m + , normalize minDistToUncovered m.DistanceToNearestNotVisited m)) + statesInfoToDump + +let collectGameState (basicBlocks:ResizeArray) filterStates = + + let vertices = ResizeArray<_>() + let allStates = HashSet<_>() + + let activeStates = + basicBlocks + |> Seq.collect (fun basicBlock -> basicBlock.AssociatedStates) + |> Seq.map (fun s -> s.Id) + |> fun x -> HashSet x + + for currentBasicBlock in basicBlocks do + let states = + currentBasicBlock.AssociatedStates + |> Seq.map (fun s -> + State(s.Id, + (uint <| s.CodeLocation.offset - currentBasicBlock.StartOffset + 1) * 1u, + s.PathConditionSize, + s.VisitedAgainVertices, + s.VisitedNotCoveredVerticesInZone, + s.VisitedNotCoveredVerticesOutOfZone, + s.StepWhenMovedLastTime, + s.InstructionsVisitedInCurrentBlock, + s.History |> Seq.map (fun kvp -> kvp.Value) |> Array.ofSeq, + s.Children |> Array.map (fun s -> s.Id) + |> (fun x -> if filterStates + then Array.filter activeStates.Contains x + else x) + ) + |> allStates.Add + |> ignore + s.Id + ) + |> Array.ofSeq + + + GameMapVertex( + currentBasicBlock.Id, + currentBasicBlock.IsGoal, + uint <| currentBasicBlock.FinalOffset - currentBasicBlock.StartOffset + 1, + currentBasicBlock.IsCovered, + currentBasicBlock.IsVisited, + currentBasicBlock.IsTouched, + currentBasicBlock.ContainsCall, + currentBasicBlock.ContainsThrow, + states) + |> vertices.Add + + let edges = ResizeArray<_>() + + for basicBlock in basicBlocks do + for outgoingEdges in basicBlock.OutgoingEdges do + for targetBasicBlock in outgoingEdges.Value do + GameMapEdge (basicBlock.Id, + targetBasicBlock.Id, + GameEdgeLabel (int outgoingEdges.Key)) + |> edges.Add + + GameState (vertices.ToArray(), allStates |> Array.ofSeq, edges.ToArray()) + +let collectGameStateDelta () = + let basicBlocks = HashSet<_>(Application.applicationGraphDelta.TouchedBasicBlocks) + for method in Application.applicationGraphDelta.LoadedMethods do + for basicBlock in method.BasicBlocks do + basicBlock.IsGoal <- method.InCoverageZone + let added = basicBlocks.Add(basicBlock) + () + collectGameState (ResizeArray basicBlocks) false + +let dumpGameState fileForResultWithoutExtension (movedStateId:uint) = + let basicBlocks = ResizeArray<_>() + for method in Application.loadedMethods do + for basicBlock in method.Key.BasicBlocks do + basicBlock.IsGoal <- method.Key.InCoverageZone + basicBlocks.Add(basicBlock) + + let gameState = collectGameState basicBlocks true + let statesInfoToDump = collectStatesInfoToDump basicBlocks + let gameStateJson = JsonSerializer.Serialize gameState + let statesInfoJson = JsonSerializer.Serialize statesInfoToDump + File.WriteAllText(fileForResultWithoutExtension + "_gameState", gameStateJson) + File.WriteAllText(fileForResultWithoutExtension + "_statesInfo", statesInfoJson) + File.WriteAllText(fileForResultWithoutExtension + "_movedState", string movedStateId) + +let computeReward (statisticsBeforeStep:Statistics) (statisticsAfterStep:Statistics) = + let rewardForCoverage = + (statisticsAfterStep.CoveredVerticesInZone - statisticsBeforeStep.CoveredVerticesInZone) * 1u + let rewardForVisitedInstructions = + (statisticsAfterStep.VisitedInstructionsInZone - statisticsBeforeStep.VisitedInstructionsInZone) * 1u + let maxPossibleReward = (statisticsBeforeStep.TotalVisibleVerticesInZone - statisticsBeforeStep.CoveredVerticesInZone) * 1u + + Reward (rewardForCoverage, rewardForVisitedInstructions, maxPossibleReward) + \ No newline at end of file diff --git a/VSharp.IL/VSharp.IL.fsproj b/VSharp.IL/VSharp.IL.fsproj index 1648a2174..0a9b9a825 100644 --- a/VSharp.IL/VSharp.IL.fsproj +++ b/VSharp.IL/VSharp.IL.fsproj @@ -1,4 +1,4 @@ - + net7.0 @@ -20,14 +20,17 @@ + + + diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs new file mode 100644 index 000000000..7c4e8a177 --- /dev/null +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -0,0 +1,262 @@ +open System.IO +open System.Reflection +open Argu +open Microsoft.FSharp.Core +open Suave +open Suave.Operators +open Suave.Filters +open Suave.Logging +open Suave.Sockets +open Suave.Sockets.Control +open Suave.WebSocket +open VSharp +open VSharp.Core +open VSharp.Explorer +open VSharp.IL +open VSharp.ML.GameServer.Messages +open VSharp.Runner + + +[] +type ExplorationResult = + val ActualCoverage: uint + val TestsCount: uint + val ErrorsCount: uint + val StepsCount: uint + new (actualCoverage, testsCount, errorsCount, stepsCount) = + { + ActualCoverage = actualCoverage + TestsCount = testsCount + ErrorsCount = errorsCount + StepsCount = stepsCount + } + +type Mode = + | Server = 0 + | Generator = 1 +type CliArguments = + | [] Port of int + | [] DatasetBasePath of string + | [] DatasetDescription of string + | [] Mode of Mode + | [] OutFolder of string + | [] StepsToSerialize of uint + interface IArgParserTemplate with + member s.Usage = + match s with + | Port _ -> "Port to communicate with game client." + | DatasetBasePath _ -> "Full path to dataset root directory. Dll location is /" + | DatasetDescription _ -> "Full paths to JSON-file with dataset description." + | Mode _ -> "Mode to run application. Server --- to train network, Generator --- to generate data for training." + | OutFolder _ -> "Folder to store generated data." + | StepsToSerialize _ -> "Maximal number of steps for each method to serialize." + +let mutable inTrainMode = true + +let explore (gameMap:GameMap) options = + let assembly = RunnerProgram.TryLoadAssembly <| FileInfo gameMap.AssemblyFullName + let method = RunnerProgram.ResolveMethod(assembly, gameMap.NameOfObjectToCover) + let statistics = TestGenerator.Cover(method, options) + let actualCoverage = + try + let testsDir = statistics.OutputDir + let _expectedCoverage = 100 + let exploredMethodInfo = AssemblyManager.NormalizeMethod method + let status,actualCoverage,message = VSharp.Test.TestResultChecker.Check(testsDir, exploredMethodInfo :?> MethodInfo, _expectedCoverage) + printfn $"Actual coverage for {gameMap.MapName}: {actualCoverage}" + if actualCoverage < 0 then 0u else uint actualCoverage * 1u + with + e -> + printfn $"Coverage checking problem:{e.Message} \n {e.StackTrace}" + 0u + + ExplorationResult(actualCoverage, statistics.TestsCount * 1u, statistics.ErrorsCount *1u, statistics.StepsCount * 1u) + + +let loadGameMaps (datasetDescriptionFilePath:string) = + let jsonString = File.ReadAllText datasetDescriptionFilePath + let maps = ResizeArray() + for map in System.Text.Json.JsonSerializer.Deserialize jsonString do + maps.Add map + maps + +let ws port outputDirectory (webSocket : WebSocket) (context: HttpContext) = + let mutable loop = true + + socket { + + let sendResponse (message:OutgoingMessage) = + let byteResponse = + serializeOutgoingMessage message + |> System.Text.Encoding.UTF8.GetBytes + |> ByteSegment + webSocket.send Text byteResponse true + + let oracle = + let feedback = + fun (feedback: Feedback) -> + let res = + socket { + let message = + match feedback with + | Feedback.ServerError s -> OutgoingMessage.ServerError s + | Feedback.MoveReward reward -> OutgoingMessage.MoveReward reward + | Feedback.IncorrectPredictedStateId i -> OutgoingMessage.IncorrectPredictedStateId i + do! sendResponse message + } + match Async.RunSynchronously res with + | Choice1Of2 () -> () + | Choice2Of2 error -> failwithf $"Error: %A{error}" + + let predict = + let mutable cnt = 0u + fun (gameState:GameState) -> + let toDot drawHistory = + let file = Path.Join ("dot",$"{cnt}.dot") + gameState.ToDot file drawHistory + cnt <- cnt + 1u + //toDot false + let res = + socket { + do! sendResponse (ReadyForNextStep gameState) + let! msg = webSocket.read() + let res = + match msg with + | (Text, data, true) -> + let msg = deserializeInputMessage data + match msg with + | Step stepParams -> (stepParams.StateId) + | _ -> failwithf $"Unexpected message: %A{msg}" + | _ -> failwithf $"Unexpected message: %A{msg}" + return res + } + match Async.RunSynchronously res with + | Choice1Of2 i -> i + | Choice2Of2 error -> failwithf $"Error: %A{error}" + + Oracle(predict,feedback) + + while loop do + let! msg = webSocket.read() + match msg with + | (Text, data, true) -> + let message = deserializeInputMessage data + match message with + | ServerStop -> loop <- false + | Start gameMap -> + printfn $"Start map {gameMap.MapName}, port {port}" + let aiTrainingOptions = + { + stepsToSwitchToAI = gameMap.StepsToStart + stepsToPlay = gameMap.StepsToPlay + defaultSearchStrategy = + match gameMap.DefaultSearcher with + | searcher.BFS -> BFSMode + | searcher.DFS -> DFSMode + | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." + serializeSteps = false + mapName = gameMap.MapName + oracle = Some oracle + } + let options = VSharpOptions(timeout = 15 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.AI, aiAgentTrainingOptions = aiTrainingOptions, solverTimeout=2) + let explorationResult = explore gameMap options + + Application.reset() + API.Reset() + HashMap.hashMap.Clear() + do! sendResponse (GameOver (explorationResult.ActualCoverage, explorationResult.TestsCount, explorationResult.ErrorsCount)) + printfn $"Finish map {gameMap.MapName}, port {port}" + | x -> failwithf $"Unexpected message: %A{x}" + + | (Close, _, _) -> + let emptyResponse = [||] |> ByteSegment + do! webSocket.send Close emptyResponse true + loop <- false + | _ -> () + } + +let app port outputDirectory : WebPart = + choose [ + path "/gameServer" >=> handShake (ws port outputDirectory) + ] + +let generateDataForPretraining outputDirectory datasetBasePath (maps:ResizeArray) stepsToSerialize = + for map in maps do + if map.StepsToStart = 0u + then + printfn $"Generation for {map.MapName} started." + let map = GameMap(map.StepsToPlay, map.StepsToStart, Path.Combine (datasetBasePath, map.AssemblyFullName), map.DefaultSearcher, map.NameOfObjectToCover, map.MapName) + let aiTrainingOptions = + { + stepsToSwitchToAI = 0u + stepsToPlay = 0u + defaultSearchStrategy = searchMode.BFSMode + serializeSteps = true + mapName = map.MapName + oracle = None + } + + let options = VSharpOptions(timeout = 5 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.ExecutionTreeContributedCoverage, stepsLimit = stepsToSerialize, solverTimeout=2, aiAgentTrainingOptions = aiTrainingOptions) + let folderForResults = Serializer.getFolderToStoreSerializationResult outputDirectory map.MapName + if Directory.Exists folderForResults + then Directory.Delete(folderForResults, true) + let _ = Directory.CreateDirectory folderForResults + + let explorationResult = explore map options + File.WriteAllText(Path.Join(folderForResults, "result"), $"{explorationResult.ActualCoverage} {explorationResult.TestsCount} {explorationResult.StepsCount} {explorationResult.ErrorsCount}") + printfn $"Generation for {map.MapName} finished with coverage {explorationResult.ActualCoverage}, tests {explorationResult.TestsCount}, steps {explorationResult.StepsCount},errors {explorationResult.ErrorsCount}." + Application.reset() + API.Reset() + HashMap.hashMap.Clear() + +[] +let main args = + let parser = ArgumentParser.Create(programName = "VSharp.ML.GameServer.Runner.exe") + let args = parser.Parse args + + let mode = args.GetResult <@Mode@> + + let port = + match args.TryGetResult <@Port@> with + | Some port -> port + | None -> 8100 + + let datasetBasePath = + match args.TryGetResult <@DatasetBasePath@> with + | Some path -> path + | None -> "" + + let datasetDescription = + match args.TryGetResult <@DatasetDescription@> with + | Some path -> path + | None -> "" + + let stepsToSerialize = + match args.TryGetResult <@StepsToSerialize@> with + | Some steps -> steps + | None -> 500u + + let outputDirectory = + Path.Combine(Directory.GetCurrentDirectory(), string port) + + if Directory.Exists outputDirectory + then Directory.Delete(outputDirectory,true) + let testsDirInfo = Directory.CreateDirectory outputDirectory + printfn $"outputDir: {outputDirectory}" + + match mode with + | Mode.Server -> + try + startWebServer {defaultConfig with + logger = Targets.create Verbose [||] + bindings = [HttpBinding.createSimple HTTP "127.0.0.1" port]} (app port outputDirectory) + with + | e -> + printfn $"Failed on port {port}" + printfn $"{e.Message}" + | Mode.Generator -> + let maps = loadGameMaps datasetDescription + generateDataForPretraining outputDirectory datasetBasePath maps stepsToSerialize + | x -> failwithf $"Unexpected mode {x}." + + 0 \ No newline at end of file diff --git a/VSharp.ML.GameServer.Runner/VSharp.ML.GameServer.Runner.fsproj b/VSharp.ML.GameServer.Runner/VSharp.ML.GameServer.Runner.fsproj new file mode 100644 index 000000000..65c8644a3 --- /dev/null +++ b/VSharp.ML.GameServer.Runner/VSharp.ML.GameServer.Runner.fsproj @@ -0,0 +1,29 @@ + + + + Exe + net7.0 + VSharp.ML.GameServer + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VSharp.ML.GameServer/Messages.fs b/VSharp.ML.GameServer/Messages.fs new file mode 100644 index 000000000..aa5fdedf4 --- /dev/null +++ b/VSharp.ML.GameServer/Messages.fs @@ -0,0 +1,291 @@ +module VSharp.ML.GameServer.Messages + +open System.Text +open System.Text.Json +open System.Text.Json.Serialization +open VSharp + +type searcher = + | BFS = 0 + | DFS = 1 + +[] +type RawInputMessage = + val MessageType: string + val MessageBody: string + [] + new (messageType, messageBody) = {MessageBody = messageBody; MessageType = messageType} + +type IRawOutgoingMessageBody = interface end + +type [] test +type [] error +type [] step +type [] percent +type [] basicBlockGlobalId +type [] instruction + +[] +type GameOverMessageBody = + interface IRawOutgoingMessageBody + val ActualCoverage: uint + val TestsCount: uint32 + val ErrorsCount: uint32 + new (actualCoverage, testsCount, errorsCount) = {ActualCoverage = actualCoverage; TestsCount = testsCount; ErrorsCount = errorsCount} + +[] +type RawOutgoingMessage = + val MessageType: string + val MessageBody: obj + new (messageType, messageBody) = {MessageBody = messageBody; MessageType = messageType} + +type [] stateId + +[] +type GameStep = + val StateId: uint + + [] + new (stateId) = {StateId = stateId} + + +[] +type StateHistoryElem = + val GraphVertexId: uint + val NumOfVisits: uint + val StepWhenVisitedLastTime: uint + new (graphVertexId, numOfVisits, stepWhenVisitedLastTime) = + { + GraphVertexId = graphVertexId + NumOfVisits = numOfVisits + StepWhenVisitedLastTime = stepWhenVisitedLastTime + } + +[] +type State = + val Id: uint + val Position: uint // to basic block id + val PathConditionSize: uint + val VisitedAgainVertices: uint + val VisitedNotCoveredVerticesInZone: uint + val VisitedNotCoveredVerticesOutOfZone: uint + val StepWhenMovedLastTime: uint + val InstructionsVisitedInCurrentBlock: uint + val History: array + val Children: array> + new(id, + position, + pathConditionSize, + visitedAgainVertices, + visitedNotCoveredVerticesInZone, + visitedNotCoveredVerticesOutOfZone, + stepWhenMovedLastTime, + instructionsVisitedInCurrentBlock, + history, + children) = + { + Id = id + Position = position + PathConditionSize = pathConditionSize + VisitedAgainVertices = visitedAgainVertices + VisitedNotCoveredVerticesInZone = visitedNotCoveredVerticesInZone + VisitedNotCoveredVerticesOutOfZone = visitedNotCoveredVerticesOutOfZone + StepWhenMovedLastTime = stepWhenMovedLastTime + InstructionsVisitedInCurrentBlock = instructionsVisitedInCurrentBlock + History = history + Children = children + } + +[] +type GameMapVertex = + val Id: uint + val InCoverageZone: bool + val BasicBlockSize: uint // instructions + val CoveredByTest: bool + val VisitedByState: bool + val TouchedByState: bool + val ContainsCall: bool + val ContainsThrow: bool + val States: uint[] + new (id, + inCoverageZone, + basicBlockSize, + containsCall, + containsThrow, + coveredByTest, + visitedByState, + touchedByState, + states) = + { + Id = id + InCoverageZone = inCoverageZone + BasicBlockSize = basicBlockSize + CoveredByTest = coveredByTest + VisitedByState = visitedByState + TouchedByState = touchedByState + ContainsCall = containsCall + ContainsThrow = containsThrow + States = states + } + +[] +type GameEdgeLabel = + val Token: int + new (token) = {Token = token} + +[] +type GameMapEdge = + val VertexFrom: uint + val VertexTo: uint + val Label: GameEdgeLabel + new (vFrom, vTo, label) = {VertexFrom = vFrom; VertexTo = vTo; Label = label} + +[] +type GameState = + interface IRawOutgoingMessageBody + val GraphVertices: GameMapVertex[] + val States: State[] + val Map: GameMapEdge[] + new (graphVertices, states, map) = {GraphVertices = graphVertices; States = states; Map = map} + + member this.ToDot file drawHistoryEdges = + let vertices = ResizeArray<_>() + let edges = ResizeArray<_>() + for v in this.GraphVertices do + let color = if v.CoveredByTest + then "green" + elif v.VisitedByState + then "red" + elif v.TouchedByState + then "yellow" + else "white" + vertices.Add($"{v.Id} [label={v.Id}, shape=box, style=filled, fillcolor={color}]") + for s in v.States do + edges.Add($"99{s}00 -> {v.Id} [label=L]") + for s in this.States do + vertices.Add($"99{s.Id}00 [label={s.Id}, shape=circle]") + for v in s.Children do + edges.Add($"99{s.Id}00 -> 99{v}00 [label=ch]") + if drawHistoryEdges + then + for v in s.History do + edges.Add($"99{s.Id}00 -> {v.GraphVertexId} [label={v.NumOfVisits}]") + for e in this.Map do + edges.Add($"{e.VertexFrom}->{e.VertexTo}[label={e.Label.Token}]") + let dot = + seq + { + "digraph g{" + yield! vertices + yield! edges + "}" + } + System.IO.File.WriteAllLines(file, dot) + +type [] coverageReward +type [] visitedInstructionsReward +type [] maxPossibleReward + +[] +type MoveReward = + val ForCoverage: uint + val ForVisitedInstructions: uint + new (forCoverage, forVisitedInstructions) = {ForCoverage = forCoverage; ForVisitedInstructions = forVisitedInstructions} + +[] +type Reward = + interface IRawOutgoingMessageBody + val ForMove: MoveReward + val MaxPossibleReward: uint + new (forMove, maxPossibleReward) = {ForMove = forMove; MaxPossibleReward = maxPossibleReward} + new (forCoverage, forVisitedInstructions, maxPossibleReward) = {ForMove = MoveReward(forCoverage,forVisitedInstructions); MaxPossibleReward = maxPossibleReward} + +type Feedback = + | MoveReward of Reward + | IncorrectPredictedStateId of uint + | ServerError of string + +[] +type GameMap = + val StepsToPlay: uint + val StepsToStart: uint + [)>] + val DefaultSearcher: searcher + val AssemblyFullName: string + val NameOfObjectToCover: string + val MapName: string + new (stepsToPlay, stepsToStart, assembly, defaultSearcher, objectToCover) = + { + StepsToPlay = stepsToPlay + StepsToStart = stepsToStart + AssemblyFullName = assembly + NameOfObjectToCover = objectToCover + DefaultSearcher = defaultSearcher + MapName = $"{objectToCover}_{defaultSearcher}_{stepsToStart}" + } + + [] + new (stepsToPlay, stepsToStart, assemblyFullName, defaultSearcher, nameOfObjectToCover, mapName) = + { + StepsToPlay = stepsToPlay + StepsToStart = stepsToStart + AssemblyFullName = assemblyFullName + DefaultSearcher = defaultSearcher + NameOfObjectToCover = nameOfObjectToCover + MapName = mapName + } + +type InputMessage = + | ServerStop + | Start of GameMap + | Step of GameStep + +[] + type ServerErrorMessageBody = + interface IRawOutgoingMessageBody + val ErrorMessage: string + new (errorMessage) = {ErrorMessage = errorMessage} + +[] + type IncorrectPredictedStateIdMessageBody = + interface IRawOutgoingMessageBody + val StateId: uint + new (stateId) = {StateId = stateId} + +type OutgoingMessage = + | GameOver of uint*uint32*uint32 + | MoveReward of Reward + | IncorrectPredictedStateId of uint + | ReadyForNextStep of GameState + | ServerError of string + +let (|MsgTypeStart|MsgTypeStep|MsgStop|) (str:string) = + let normalized = str.ToLowerInvariant().Trim() + if normalized = "start" + then MsgTypeStart + elif normalized = "step" + then MsgTypeStep + elif normalized = "stop" + then MsgStop + else failwithf $"Unexpected message type %s{str}" + +let deserializeInputMessage (messageData:byte[]) = + let rawInputMessage = + let str = Encoding.UTF8.GetString messageData + str |> JsonSerializer.Deserialize + match rawInputMessage.MessageType with + | MsgStop -> ServerStop + | MsgTypeStart -> Start (JsonSerializer.Deserialize rawInputMessage.MessageBody) + | MsgTypeStep -> Step (JsonSerializer.Deserialize(rawInputMessage.MessageBody)) + +let serializeOutgoingMessage (message:OutgoingMessage) = + match message with + | GameOver (actualCoverage,testsCount, errorsCount) -> RawOutgoingMessage("GameOver", box (GameOverMessageBody (actualCoverage, testsCount, errorsCount))) + | MoveReward reward -> RawOutgoingMessage("MoveReward", reward) + | IncorrectPredictedStateId stateId -> RawOutgoingMessage("IncorrectPredictedStateId", IncorrectPredictedStateIdMessageBody stateId) + | ReadyForNextStep state -> RawOutgoingMessage("ReadyForNextStep", state) + | ServerError errorMessage -> RawOutgoingMessage("ServerError", ServerErrorMessageBody errorMessage) + |> JsonSerializer.Serialize + + + \ No newline at end of file diff --git a/VSharp.ML.GameServer/VSharp.ML.GameServer.fsproj b/VSharp.ML.GameServer/VSharp.ML.GameServer.fsproj new file mode 100644 index 000000000..0d398a74b --- /dev/null +++ b/VSharp.ML.GameServer/VSharp.ML.GameServer.fsproj @@ -0,0 +1,19 @@ + + + + net7.0 + + + + + + + + + + + + + + + diff --git a/VSharp.Runner/RunnerProgram.cs b/VSharp.Runner/RunnerProgram.cs index f41e3515b..a3b841c3c 100644 --- a/VSharp.Runner/RunnerProgram.cs +++ b/VSharp.Runner/RunnerProgram.cs @@ -14,7 +14,7 @@ namespace VSharp.Runner public static class RunnerProgram { - private static Assembly? TryLoadAssembly(FileInfo assemblyPath) + public static Assembly? TryLoadAssembly(FileInfo assemblyPath) { try { @@ -28,6 +28,93 @@ public static class RunnerProgram } } + public static Type? ResolveType(Assembly assembly, string? classArgumentValue) + { + if (classArgumentValue == null) + { + Console.Error.WriteLine("Specified class can not be null"); + return null; + } + + var specificClass = + assembly.GetType(classArgumentValue) ?? + assembly.GetTypes() + .Where(t => (t.FullName ?? t.Name).Contains(classArgumentValue)) + .MinBy(t => t.FullName?.Length ?? t.Name.Length); + if (specificClass == null) + { + Console.Error.WriteLine("I did not found type you specified {0} in assembly {1}", classArgumentValue, + assembly.Location); + return null; + } + + return specificClass; + } + + public static MethodBase? ResolveMethod(Assembly assembly, string? methodArgumentValue) + { + if (methodArgumentValue == null) + { + Console.Error.WriteLine("Specified method can not be null"); + return null; + } + + MethodBase? method = null; + if (Int32.TryParse(methodArgumentValue, out var metadataToken)) + { + foreach (var module in assembly.GetModules()) + { + try + { + method = module.ResolveMethod(metadataToken); + if (method != null) break; + } + catch + { + // ignored + } + } + + if (method == null) + { + Console.Error.WriteLine("I did not found method you specified by token {0} in assembly {1}", + metadataToken, assembly.Location); + return null; + } + } + else + { + foreach (var type in assembly.GetTypes()) + { + try + { + var t = methodArgumentValue.Split('.'); + var className = t.Length == 1 ? "" : t[t.Length - 2]; + var methodName = t.Last(); + var x = type.GetMethods(Reflection.allBindingFlags); + method ??= x + .Where(m => type.FullName.Split('.').Last().Contains(className) && m.Name.Contains(methodName)) + .MinBy(m => m.Name.Length); + if (method != null) + break; + } + catch (Exception) + { + // ignored + } + } + + if (method == null) + { + Console.Error.WriteLine("I did not found method you specified by name {0} in assembly {1}", + methodArgumentValue, assembly.Location); + return null; + } + } + + return method; + } + private static void PostProcess(Statistics statistics) { statistics.GenerateReport(Console.Out); @@ -44,7 +131,8 @@ private static void EntryPointHandler( SearchStrategy strat, Verbosity verbosity, uint recursionThreshold, - ExplorationMode explorationMode) + ExplorationMode explorationMode, + string pathToModel) { var assembly = TryLoadAssembly(assemblyPath); var options = @@ -56,7 +144,8 @@ private static void EntryPointHandler( searchStrategy: strat, verbosity: verbosity, recursionThreshold: recursionThreshold, - explorationMode: explorationMode); + explorationMode: explorationMode, + pathToModel: pathToModel); if (assembly == null) return; @@ -149,7 +238,8 @@ private static void AllPublicMethodsHandler( SearchStrategy strat, Verbosity verbosity, uint recursionThreshold, - ExplorationMode explorationMode) + ExplorationMode explorationMode, + string pathToModel) { var assembly = TryLoadAssembly(assemblyPath); var options = @@ -161,7 +251,8 @@ private static void AllPublicMethodsHandler( searchStrategy: strat, verbosity: verbosity, recursionThreshold: recursionThreshold, - explorationMode: explorationMode); + explorationMode: explorationMode, + pathToModel: pathToModel); if (assembly == null) return; @@ -184,7 +275,8 @@ private static void PublicMethodsOfTypeHandler( SearchStrategy strat, Verbosity verbosity, uint recursionThreshold, - ExplorationMode explorationMode) + ExplorationMode explorationMode, + string pathToModel) { var assembly = TryLoadAssembly(assemblyPath); if (assembly == null) return; @@ -205,7 +297,8 @@ private static void PublicMethodsOfTypeHandler( searchStrategy: strat, verbosity: verbosity, recursionThreshold: recursionThreshold, - explorationMode: explorationMode); + explorationMode: explorationMode, + pathToModel: pathToModel); Statistics statistics; if (runTests) @@ -227,7 +320,8 @@ private static void SpecificMethodHandler( SearchStrategy strat, Verbosity verbosity, uint recursionThreshold, - ExplorationMode explorationMode) + ExplorationMode explorationMode, + string pathToModel) { var assembly = TryLoadAssembly(assemblyPath); if (assembly == null) return; @@ -264,7 +358,8 @@ private static void SpecificMethodHandler( searchStrategy: strat, verbosity: verbosity, recursionThreshold: recursionThreshold, - explorationMode: explorationMode); + explorationMode: explorationMode, + pathToModel: pathToModel); Statistics statistics; if (runTests || checkCoverage) @@ -285,7 +380,8 @@ private static void NamespaceHandler( SearchStrategy strat, Verbosity verbosity, uint recursionThreshold, - ExplorationMode explorationMode) + ExplorationMode explorationMode, + string pathToModel) { var assembly = TryLoadAssembly(assemblyPath); if (assembly == null) return; @@ -306,7 +402,8 @@ private static void NamespaceHandler( searchStrategy: strat, verbosity: verbosity, recursionThreshold: recursionThreshold, - explorationMode: explorationMode); + explorationMode: explorationMode, + pathToModel: pathToModel); Statistics statistics; if (runTests) @@ -326,6 +423,10 @@ public static int Main(string[] args) aliases: new[] { "--timeout", "-t" }, () => -1, "Time for test generation in seconds. Negative value means no timeout."); + var pathToModelOption = new Option( + aliases: new[] { "--model", "-m" }, + () => defaultOptions.GetDefaultPathToModel(), + "Path to ONNX file with model for AI searcher."); var solverTimeoutOption = new Option( aliases: new[] { "--solver-timeout", "-st" }, () => -1, @@ -371,6 +472,7 @@ public static int Main(string[] args) rootCommand.AddGlobalOption(verbosityOption); rootCommand.AddGlobalOption(recursionThresholdOption); rootCommand.AddGlobalOption(explorationModeOption); + rootCommand.AddGlobalOption(pathToModelOption); var entryPointCommand = new Command("--entry-point", "Generate test coverage from the entry point of assembly (assembly must contain Main method)"); @@ -396,7 +498,8 @@ public static int Main(string[] args) parseResult.GetValueForOption(searchStrategyOption), parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), - parseResult.GetValueForOption(explorationModeOption) + parseResult.GetValueForOption(explorationModeOption), + parseResult.GetValueForOption(pathToModelOption) ); }); @@ -457,7 +560,8 @@ public static int Main(string[] args) parseResult.GetValueForOption(searchStrategyOption), parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), - parseResult.GetValueForOption(explorationModeOption) + parseResult.GetValueForOption(explorationModeOption), + parseResult.GetValueForOption(pathToModelOption) ); }); @@ -484,7 +588,8 @@ public static int Main(string[] args) parseResult.GetValueForOption(searchStrategyOption), parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), - parseResult.GetValueForOption(explorationModeOption) + parseResult.GetValueForOption(explorationModeOption), + parseResult.GetValueForOption(pathToModelOption) ); }); @@ -499,6 +604,7 @@ public static int Main(string[] args) specificMethodCommand.SetHandler(context => { var parseResult = context.ParseResult; + var pathToModel = parseResult.GetValueForOption(pathToModelOption); var output = parseResult.GetValueForOption(outputOption); Debug.Assert(output is not null); SpecificMethodHandler( @@ -513,7 +619,8 @@ public static int Main(string[] args) parseResult.GetValueForOption(searchStrategyOption), parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), - parseResult.GetValueForOption(explorationModeOption) + parseResult.GetValueForOption(explorationModeOption), + pathToModel ); }); @@ -528,6 +635,7 @@ public static int Main(string[] args) { var parseResult = context.ParseResult; var output = parseResult.GetValueForOption(outputOption); + var pathToModel = parseResult.GetValueForOption(pathToModelOption); Debug.Assert(output is not null); NamespaceHandler( parseResult.GetValueForArgument(assemblyPathArgument), @@ -540,7 +648,8 @@ public static int Main(string[] args) parseResult.GetValueForOption(searchStrategyOption), parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), - parseResult.GetValueForOption(explorationModeOption) + parseResult.GetValueForOption(explorationModeOption), + pathToModel ); }); diff --git a/VSharp.Runner/VSharp.Runner.csproj b/VSharp.Runner/VSharp.Runner.csproj index cbb961513..da7a132da 100644 --- a/VSharp.Runner/VSharp.Runner.csproj +++ b/VSharp.Runner/VSharp.Runner.csproj @@ -21,6 +21,7 @@ + diff --git a/VSharp.SILI/CILState.fs b/VSharp.SILI/CILState.fs index d7fc576cf..44a90cb96 100644 --- a/VSharp.SILI/CILState.fs +++ b/VSharp.SILI/CILState.fs @@ -7,16 +7,17 @@ open System.Collections.Generic open VSharp.Core open VSharp.Interpreter.IL open IpOperations +open VSharp.ML.GameServer.Messages module CilState = type prefix = | Constrained of System.Type - let mutable currentStateId = 0u + let mutable currentStateId = 0u let getNextStateId() = let nextId = currentStateId - currentStateId <- currentStateId + 1u + currentStateId <- currentStateId + 1u nextId type public ErrorReporter internal (cilState : cilState) = @@ -112,12 +113,19 @@ module CilState = /// /// Deterministic state id. /// - internalId : uint + mutable internalId : uint + mutable visitedAgainVertices: uint + mutable visitedNotCoveredVerticesInZone: uint + mutable visitedNotCoveredVerticesOutOfZone: uint + mutable stepWhenMovedLastTime: uint + mutable instructionsVisitedInCurrentBlock: uint + mutable _history: Dictionary + mutable children: list webConfiguration : webConfiguration option } static member private CommonCreateInitial (m : Method) (state : state) webConfiguration = - let ip = Instruction(0, m) + let ip = Instruction(0, m) let approximateLoc = ip.ToCodeLocation() |> Option.get { ipStack = List.singleton ip @@ -137,6 +145,13 @@ module CilState = history = Set.empty entryMethod = Some m internalId = getNextStateId() + visitedAgainVertices = 0u + visitedNotCoveredVerticesInZone = 0u + visitedNotCoveredVerticesOutOfZone = 0u + stepWhenMovedLastTime = 0u + instructionsVisitedInCurrentBlock = 0u + _history = Dictionary() + children = [] webConfiguration = webConfiguration } @@ -158,7 +173,7 @@ module CilState = member x.StartsFromMethodBeginning with get() = match x.startingIP with - | Instruction (0, _) -> true + | Instruction (0, _) -> true | _ -> false member x.SetCurrentTime time = x.state.currentTime <- time @@ -466,6 +481,7 @@ module CilState = let mkCilState state' = if LanguagePrimitives.PhysicalEquality state' x.state then x else clone.Copy(state') + StatedConditionalExecution x.state conditionInvocation (fun state k -> thenBranch (mkCilState state) k) (fun state k -> elseBranch (mkCilState state) k) @@ -501,7 +517,14 @@ module CilState = // -------------------- Changing inner state -------------------- member x.Copy(state : state) = - { x with state = state; internalId = getNextStateId() } + let historyCopy = Dictionary<_,_>() + for kvp in x._history do historyCopy.Add(kvp.Key, kvp.Value) + { x with state = state + internalId = getNextStateId() + children = [] + _history = historyCopy + stepWhenMovedLastTime = x.stepWhenMovedLastTime + } // This function copies cilState, instead of mutation member x.ChangeState state' : cilState = @@ -520,6 +543,17 @@ module CilState = interface IGraphTrackableState with override this.CodeLocation = this.approximateLoc override this.CallStack = Memory.StackTrace this.state.memory.Stack |> List.map (fun m -> m :?> Method) + override this.Id = this.internalId + override this.PathConditionSize with get () = PersistentSet.cardinality this.state.pc |> uint32 + override this.VisitedAgainVertices with get () = this.visitedAgainVertices + override this.VisitedNotCoveredVerticesInZone with get () = this.visitedNotCoveredVerticesInZone + override this.VisitedNotCoveredVerticesOutOfZone with get () = this.visitedNotCoveredVerticesOutOfZone + override this.StepWhenMovedLastTime with get () = this.stepWhenMovedLastTime + override this.InstructionsVisitedInCurrentBlock + with get() = this.instructionsVisitedInCurrentBlock + and set v = this.instructionsVisitedInCurrentBlock <- v + override this.History with get () = this._history + override this.Children with get () = this.children |> Seq.cast<_> |> Array.ofSeq module CilStateOperations = open CilState diff --git a/VSharp.SILI/Interpreter.fs b/VSharp.SILI/Interpreter.fs index 32d540ce0..b46d93ba7 100644 --- a/VSharp.SILI/Interpreter.fs +++ b/VSharp.SILI/Interpreter.fs @@ -661,7 +661,7 @@ type ILInterpreter() as this = static member InitFunctionFrameCIL (cilState : cilState) (method : Method) this paramValues = Memory.InitFunctionFrame cilState.state method this (paramValues |> Option.bind (List.map Some >> Some)) - Instruction(0, method) |> cilState.PushToIp + Instruction(0, method) |> cilState.PushToIp static member CheckDisallowNullAttribute (method : Method) (argumentsOpt : term list option) (cilState : cilState) shouldReportError k = if not <| method.CheckAttributes then @@ -869,7 +869,7 @@ type ILInterpreter() as this = | _ -> __unreachable__() let bodyForModel = Memory.AllocateString " " modelState let modelStates = Memory.Write modelState bodyArgRef bodyForModel - Instruction(0, method) |> cilState.PushToIp + Instruction(0, method) |> cilState.PushToIp List.singleton cilState member private x.ConfigureAspNet (cilState : cilState) thisOption = @@ -884,7 +884,7 @@ type ILInterpreter() as this = let contentRootPath = Memory.AllocateString webConfig.contentRootPath.FullName cilState.state let parameters = Some [Some webAppOptions; Some environmentName; Some applicationName; Some contentRootPath] Memory.InitFunctionFrame cilState.state method None parameters - Instruction(0, method) |> cilState.PushToIp + Instruction(0, method) |> cilState.PushToIp List.singleton cilState member private x.ExecutorExecute (cilState : cilState) thisOption args = @@ -904,7 +904,7 @@ type ILInterpreter() as this = |> Application.getMethod let args = Some (List.map Some args) Memory.InitFunctionFrame cilState.state invokeMethod None args - Instruction(0, invokeMethod) |> cilState.PushToIp + Instruction(0, invokeMethod) |> cilState.PushToIp List.singleton cilState | _ -> internalfail $"ExecutorExecute: unexpected 'this' {thisOption}" @@ -1944,11 +1944,11 @@ type ILInterpreter() as this = executeAllInstructions ([],[],[]) cilState id member private x.IncrementLevelIfNeeded (m : Method) (offset : offset) (cilState : cilState) = - if offset = 0 || m.CFG.IsLoopEntry offset then + if offset = 0 || m.CFG.IsLoopEntry offset then cilState.IncrementLevel {offset = offset; method = m} member private x.DecrementMethodLevel (cilState : cilState) method = - let key = {offset = 0; method = method} + let key = {offset = 0; method = method} cilState.DecrementLevel key member private x.InTryBlock offset (clause : exceptionHandlingClause) = @@ -2001,7 +2001,7 @@ type ILInterpreter() as this = // Normal execution // Also on entering try block we push new exception register | Instruction(offset, m) -> - if offset = 0 then Logger.info $"Starting to explore method {m}" + if offset = 0 then Logger.info $"Starting to explore method {m}" x.PushExceptionRegisterIfNeeded cilState m offset x.ExecuteInstruction m offset cilState |> k // Exiting method diff --git a/VSharp.SILI/VSharp.SILI.fsproj b/VSharp.SILI/VSharp.SILI.fsproj index 5cf1a1e6e..d68969867 100644 --- a/VSharp.SILI/VSharp.SILI.fsproj +++ b/VSharp.SILI/VSharp.SILI.fsproj @@ -1,4 +1,4 @@ - + net7.0 diff --git a/VSharp.Test/Benchmarks/Benchmarks.cs b/VSharp.Test/Benchmarks/Benchmarks.cs index b1ebd7d67..677e8ed8e 100644 --- a/VSharp.Test/Benchmarks/Benchmarks.cs +++ b/VSharp.Test/Benchmarks/Benchmarks.cs @@ -92,7 +92,9 @@ public static BenchmarkResult Run( checkAttributes: false, stopOnCoverageAchieved: -1, randomSeed: randomSeed, - stepsLimit: stepsLimit + stepsLimit: stepsLimit, + aiAgentTrainingOptions: null, + pathToModel: null ); var fuzzerOptions = new FuzzerOptions( diff --git a/VSharp.Test/IntegrationTests.cs b/VSharp.Test/IntegrationTests.cs index 955b71205..13ce410fc 100644 --- a/VSharp.Test/IntegrationTests.cs +++ b/VSharp.Test/IntegrationTests.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading; +using Microsoft.FSharp.Collections; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; @@ -28,7 +29,8 @@ public enum SearchStrategy ContributedCoverage, ExecutionTree, ExecutionTreeContributedCoverage, - Interleaved + Interleaved, + AI } public enum CoverageZone @@ -130,6 +132,7 @@ static TestSvmAttribute() private readonly ExplorationMode _explorationMode; private readonly int _randomSeed; private readonly uint _stepsLimit; + private readonly string _pathToModel; public TestSvmAttribute( int expectedCoverage = -1, @@ -140,13 +143,14 @@ public TestSvmAttribute( SearchStrategy strat = SearchStrategy.BFS, CoverageZone coverageZone = CoverageZone.Class, TestsCheckerMode testsCheckerMode = TestsCheckerMode.RenderAndRun, - bool checkAttributes = true, bool hasExternMocking = false, + bool checkAttributes = true, OsType supportedOs = OsType.All, FuzzerIsolation fuzzerIsolation = FuzzerIsolation.Process, ExplorationMode explorationMode = ExplorationMode.Sili, int randomSeed = 0, - uint stepsLimit = 0) + uint stepsLimit = 0, + string pathToModel = "models/model.onnx") { if (expectedCoverage < 0) _expectedCoverage = null; @@ -160,12 +164,14 @@ public TestSvmAttribute( _strat = strat; _coverageZone = coverageZone; _testsCheckerMode = testsCheckerMode; + _hasExternMocking = hasExternMocking; _checkAttributes = checkAttributes; _hasExternMocking = hasExternMocking; _supportedOs = supportedOs; _fuzzerIsolation = fuzzerIsolation; _explorationMode = explorationMode; _randomSeed = randomSeed; + _pathToModel = pathToModel; _stepsLimit = stepsLimit; } @@ -182,12 +188,13 @@ public TestCommand Wrap(TestCommand command) _coverageZone, _testsCheckerMode, _checkAttributes, - _hasExternMocking, _supportedOs, _fuzzerIsolation, _explorationMode, _randomSeed, - _stepsLimit + _stepsLimit, + _pathToModel, + _hasExternMocking ); } @@ -211,6 +218,7 @@ private class TestSvmCommand : DelegatingTestCommand private readonly ExplorationMode _explorationMode; private readonly int _randomSeed; private readonly uint _stepsLimit; + private readonly string _pathToModel; private class Reporter: IReporter { @@ -239,12 +247,13 @@ public TestSvmCommand( CoverageZone coverageZone, TestsCheckerMode testsCheckerMode, bool checkAttributes, - bool hasExternMocking, OsType supportedOs, FuzzerIsolation fuzzerIsolation, ExplorationMode explorationMode, int randomSeed, - uint stepsLimit) : base(innerCommand) + uint stepsLimit, + string pathToModel, + bool hasExternMocking) : base(innerCommand) { _baseCoverageZone = coverageZone; _baseSearchStrat = TestContext.Parameters[SearchStrategyParameterName] == null ? @@ -274,6 +283,7 @@ public TestSvmCommand( SearchStrategy.ExecutionTree => searchMode.ExecutionTreeMode, SearchStrategy.ExecutionTreeContributedCoverage => searchMode.NewInterleavedMode(searchMode.ExecutionTreeMode, 1, searchMode.ContributedCoverageMode, 1), SearchStrategy.Interleaved => searchMode.NewInterleavedMode(searchMode.ShortestDistanceBasedMode, 1, searchMode.ContributedCoverageMode, 9), + SearchStrategy.AI => searchMode.AIMode, _ => throw new ArgumentOutOfRangeException(nameof(strat), strat, null) }; @@ -296,6 +306,7 @@ public TestSvmCommand( _explorationMode = explorationMode; _randomSeed = randomSeed; _stepsLimit = stepsLimit; + _pathToModel = pathToModel; } private TestResult IgnoreTest(TestExecutionContext context) @@ -451,7 +462,9 @@ private TestResult Explore(TestExecutionContext context) checkAttributes: _checkAttributes, stopOnCoverageAchieved: _expectedCoverage ?? -1, randomSeed: _randomSeed, - stepsLimit: _stepsLimit + stepsLimit: _stepsLimit, + aiAgentTrainingOptions:null, + pathToModel: _pathToModel ); var fuzzerOptions = new FuzzerOptions( @@ -474,6 +487,7 @@ private TestResult Explore(TestExecutionContext context) ); using var explorer = new Explorer.Explorer(explorationOptions, new Reporter(unitTests)); + Application.reset(); explorer.StartExploration( new [] { exploredMethodInfo }, global::System.Array.Empty>() diff --git a/VSharp.Test/Tests/ControlFlow.cs b/VSharp.Test/Tests/ControlFlow.cs index 54234e21d..22b49fbda 100644 --- a/VSharp.Test/Tests/ControlFlow.cs +++ b/VSharp.Test/Tests/ControlFlow.cs @@ -1,4 +1,4 @@ -using System; +using System; using NUnit.Framework; using VSharp.Test; @@ -375,6 +375,21 @@ public static int NestedForsSimple(int x) return res; } + + [TestSvm(100, strat: SearchStrategy.AI)] + public static int NestedForsSimple_AI(int x) + { + int res = 0; + for (int i = 0; i < x; i++) + { + for (int j = 0; j < x; j++) + { + res++; + } + } + + return res; + } [Ignore("Looping due to non-terminating paths")] public static int NestedForsHard(int x) diff --git a/VSharp.Test/Tests/LoanExam.cs b/VSharp.Test/Tests/LoanExam.cs index f2ecd760b..dfece59f1 100644 --- a/VSharp.Test/Tests/LoanExam.cs +++ b/VSharp.Test/Tests/LoanExam.cs @@ -1,4 +1,4 @@ -using System; +using System; using VSharp.Test; namespace IntegrationTests; diff --git a/VSharp.Test/Tests/Mocking.cs b/VSharp.Test/Tests/Mocking.cs index 2ba995330..bf3441d8e 100644 --- a/VSharp.Test/Tests/Mocking.cs +++ b/VSharp.Test/Tests/Mocking.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using NUnit.Framework; using VSharp.Test; diff --git a/VSharp.Utils/GraphUtils.fs b/VSharp.Utils/GraphUtils.fs index 9ec54799b..6978a65a6 100644 --- a/VSharp.Utils/GraphUtils.fs +++ b/VSharp.Utils/GraphUtils.fs @@ -5,6 +5,12 @@ open System.Collections.Generic module GraphUtils = type graph<'a> = Dictionary<'a, HashSet<'a>> type distanceCache<'a> = Dictionary<'a, Dictionary<'a, uint>> + + type ICallGraphNode = + abstract OutgoingEdges : seq with get + + type IReversedCallGraphNode = + abstract OutgoingEdges : seq with get type IGraphNode<'t> = abstract OutgoingEdges : seq<'t> with get diff --git a/VSharp.Utils/Prelude.fs b/VSharp.Utils/Prelude.fs index 24415e6e7..0aa28e977 100644 --- a/VSharp.Utils/Prelude.fs +++ b/VSharp.Utils/Prelude.fs @@ -13,7 +13,9 @@ type InsufficientInformationException(msg : string) = [] module public Prelude = - + + type [] byte_offset + let public internalfail message = raise (InternalException message) let public internalfailf format = Printf.ksprintf internalfail format let undefinedBehaviour reason = internalfail $"Undefined behaviour: %s{reason}" diff --git a/VSharp.Utils/UnitTests.fs b/VSharp.Utils/UnitTests.fs index 5f7848453..2d6ec572c 100644 --- a/VSharp.Utils/UnitTests.fs +++ b/VSharp.Utils/UnitTests.fs @@ -32,11 +32,11 @@ type UnitTests(outputDir : string) = member x.GenerateTest (test : UnitTest) = testNumber <- testNumber + 1u - generateTest test ("test" + testNumber.ToString()) + generateTest test (test.Method.Name + ".test" + testNumber.ToString()) member x.GenerateError (test : UnitTest) = errorNumber <- errorNumber + 1u - generateTest test ("error" + errorNumber.ToString()) + generateTest test (test.Method.Name + ".error" + errorNumber.ToString()) member x.WriteReport (reporter : Action) = let reportFileName = $"%s{currentDir.FullName}%c{Path.DirectorySeparatorChar}report.txt" diff --git a/VSharp.sln b/VSharp.sln index b98c7da26..b097d73b8 100644 --- a/VSharp.sln +++ b/VSharp.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VSharp.CSharpUtils", "VSharp.CSharpUtils\VSharp.CSharpUtils.csproj", "{C3AC6243-43EB-4312-AF41-03D36CED52DE}" EndProject @@ -28,6 +28,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VSharp.TestExtensions", "VS EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSharp.TestGenerator", "VSharp.TestGenerator\VSharp.TestGenerator.fsproj", "{8FC15B9A-7563-4480-9E3A-30DE29CD9C5F}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSharp.ML.GameServer.Runner", "VSharp.ML.GameServer.Runner\VSharp.ML.GameServer.Runner.fsproj", "{68921FA7-1FBD-43E5-9459-EA01303A4DC8}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSharp.ML.GameServer", "VSharp.ML.GameServer\VSharp.ML.GameServer.fsproj", "{44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}" +EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSharp.Fuzzer", "VSharp.Fuzzer\VSharp.Fuzzer.fsproj", "{70A99736-8F51-40EA-9AB3-37E64697D3B8}" EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSharp.Explorer", "VSharp.Explorer\VSharp.Explorer.fsproj", "{9455C456-51A6-4CA3-B33A-4F168CBADADA}" @@ -125,6 +129,36 @@ Global {8FC15B9A-7563-4480-9E3A-30DE29CD9C5F}.Release|Any CPU.Build.0 = Release|Any CPU {8FC15B9A-7563-4480-9E3A-30DE29CD9C5F}.DebugTailRec|Any CPU.ActiveCfg = DebugTailRec|Any CPU {8FC15B9A-7563-4480-9E3A-30DE29CD9C5F}.DebugTailRec|Any CPU.Build.0 = DebugTailRec|Any CPU + {8FC15B9A-7563-4480-9E3A-30DE29CD9C5F}.DebugTailRec|Any CPU.ActiveCfg = Debug|Any CPU + {8FC15B9A-7563-4480-9E3A-30DE29CD9C5F}.DebugTailRec|Any CPU.Build.0 = Debug|Any CPU + {4C5B924D-8934-44F2-847A-BE91416DB557}.ReleaseConcolic|Any CPU.ActiveCfg = Release|Any CPU + {4C5B924D-8934-44F2-847A-BE91416DB557}.DebugConcolic|Any CPU.ActiveCfg = Debug|Any CPU + {4C5B924D-8934-44F2-847A-BE91416DB557}.DebugConcolic|Any CPU.Build.0 = Debug|Any CPU + {4C5B924D-8934-44F2-847A-BE91416DB557}.ReleaseConcolic|Any CPU.Build.0 = Release|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.Release|Any CPU.Build.0 = Release|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.DebugTailRec|Any CPU.ActiveCfg = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.DebugTailRec|Any CPU.Build.0 = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.ReleaseConcolic|Any CPU.ActiveCfg = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.ReleaseConcolic|Any CPU.Build.0 = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.DebugConcolic|Any CPU.ActiveCfg = Debug|Any CPU + {68921FA7-1FBD-43E5-9459-EA01303A4DC8}.DebugConcolic|Any CPU.Build.0 = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.Release|Any CPU.Build.0 = Release|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.DebugTailRec|Any CPU.ActiveCfg = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.DebugTailRec|Any CPU.Build.0 = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.ReleaseConcolic|Any CPU.ActiveCfg = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.ReleaseConcolic|Any CPU.Build.0 = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.DebugConcolic|Any CPU.ActiveCfg = Debug|Any CPU + {44586A7C-12E4-4765-ADF0-20DE2ED2EEC7}.DebugConcolic|Any CPU.Build.0 = Debug|Any CPU + {AEB2ABD0-5045-4A28-BBEB-2F353BD543FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEB2ABD0-5045-4A28-BBEB-2F353BD543FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEB2ABD0-5045-4A28-BBEB-2F353BD543FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEB2ABD0-5045-4A28-BBEB-2F353BD543FE}.Release|Any CPU.Build.0 = Release|Any CPU {70A99736-8F51-40EA-9AB3-37E64697D3B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70A99736-8F51-40EA-9AB3-37E64697D3B8}.Debug|Any CPU.Build.0 = Debug|Any CPU {70A99736-8F51-40EA-9AB3-37E64697D3B8}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -143,5 +177,9 @@ Global {CA15A9BA-17DC-4D0C-853E-11E1A4B8D29F}.Release|Any CPU.Build.0 = Release|Any CPU {CA15A9BA-17DC-4D0C-853E-11E1A4B8D29F}.DebugTailRec|Any CPU.ActiveCfg = Debug|Any CPU {CA15A9BA-17DC-4D0C-853E-11E1A4B8D29F}.DebugTailRec|Any CPU.Build.0 = Debug|Any CPU + {AEB2ABD0-5045-4A28-BBEB-2F353BD543FE}.DebugTailRec|Any CPU.ActiveCfg = Debug|Any CPU + {AEB2ABD0-5045-4A28-BBEB-2F353BD543FE}.DebugTailRec|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution EndGlobalSection EndGlobal From 5f0796aa0c324099b116554bd86ac6f1a1c90c53 Mon Sep 17 00:00:00 2001 From: gsv Date: Wed, 6 Nov 2024 21:17:42 +0300 Subject: [PATCH 02/12] Update ONNX runtime to 1.20 to support CUDA 12.x --- VSharp.Explorer/VSharp.Explorer.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VSharp.Explorer/VSharp.Explorer.fsproj b/VSharp.Explorer/VSharp.Explorer.fsproj index 53fbb88ab..234fdb506 100644 --- a/VSharp.Explorer/VSharp.Explorer.fsproj +++ b/VSharp.Explorer/VSharp.Explorer.fsproj @@ -49,7 +49,7 @@ - + From 3acd9304dd1443277fa01daea4ec7dfe042c0cce Mon Sep 17 00:00:00 2001 From: David Akhmedov <113194669+Parzival-05@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:53:40 +0300 Subject: [PATCH 03/12] Fix step count (#90) --- VSharp.ML.GameServer.Runner/Main.fs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs index 7c4e8a177..d6414fb81 100644 --- a/VSharp.ML.GameServer.Runner/Main.fs +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -145,10 +145,12 @@ let ws port outputDirectory (webSocket : WebSocket) (context: HttpContext) = | ServerStop -> loop <- false | Start gameMap -> printfn $"Start map {gameMap.MapName}, port {port}" + let stepsToStart= gameMap.StepsToStart + let stepsToPlay = gameMap.StepsToPlay let aiTrainingOptions = { - stepsToSwitchToAI = gameMap.StepsToStart - stepsToPlay = gameMap.StepsToPlay + stepsToSwitchToAI = stepsToStart + stepsToPlay = stepsToPlay defaultSearchStrategy = match gameMap.DefaultSearcher with | searcher.BFS -> BFSMode @@ -158,7 +160,7 @@ let ws port outputDirectory (webSocket : WebSocket) (context: HttpContext) = mapName = gameMap.MapName oracle = Some oracle } - let options = VSharpOptions(timeout = 15 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.AI, aiAgentTrainingOptions = aiTrainingOptions, solverTimeout=2) + let options = VSharpOptions(timeout = 15 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.AI, aiAgentTrainingOptions = aiTrainingOptions, stepsLimit = uint(stepsToPlay + stepsToStart), solverTimeout=2) let explorationResult = explore gameMap options Application.reset() From 6b59816123a3c0c069d34fe98383665f2d00ff38 Mon Sep 17 00:00:00 2001 From: David Akhmedov <113194669+Parzival-05@users.noreply.github.com> Date: Wed, 26 Feb 2025 23:16:14 +0300 Subject: [PATCH 04/12] GPU support (#88) * Format * GPU support * Add description of parameters * Apply suggestions from review * Use OnnxRuntime with GPU --- VSharp.API/VSharp.cs | 15 +- VSharp.API/VSharpOptions.cs | 10 +- VSharp.Explorer/AISearcher.fs | 481 ++++++++++++---------- VSharp.Explorer/Explorer.fs | 548 +++++++++++++++---------- VSharp.Explorer/Options.fs | 68 +-- VSharp.Explorer/VSharp.Explorer.fsproj | 2 +- VSharp.ML.GameServer.Runner/Main.fs | 376 ++++++++++------- VSharp.Runner/RunnerProgram.cs | 75 +++- VSharp.Test/Benchmarks/Benchmarks.cs | 8 +- VSharp.Test/IntegrationTests.cs | 35 +- 10 files changed, 964 insertions(+), 654 deletions(-) diff --git a/VSharp.API/VSharp.cs b/VSharp.API/VSharp.cs index 434b3a874..f6b186161 100644 --- a/VSharp.API/VSharp.cs +++ b/VSharp.API/VSharp.cs @@ -111,7 +111,7 @@ public IEnumerable Results() public static class TestGenerator { - private class Reporter: IReporter + private class Reporter : IReporter { private readonly UnitTests _unitTests; private readonly bool _isQuiet; @@ -124,7 +124,7 @@ public Reporter(UnitTests unitTests, bool isQuiet) public void ReportFinished(UnitTest unitTest) => _unitTests.GenerateTest(unitTest); public void ReportException(UnitTest unitTest) => _unitTests.GenerateError(unitTest); - public void ReportIIE(InsufficientInformationException iie) {} + public void ReportIIE(InsufficientInformationException iie) { } public void ReportInternalFail(Method? method, Exception exn) { @@ -179,7 +179,7 @@ private static Statistics StartExploration( explorationMode: explorationMode.NewTestCoverageMode( coverageZone, options.Timeout > 0 ? searchMode.NewFairMode(baseSearchMode) : baseSearchMode - + ), recThreshold: options.RecursionThreshold, solverTimeout: options.SolverTimeout, @@ -191,8 +191,11 @@ private static Statistics StartExploration( stopOnCoverageAchieved: 100, randomSeed: options.RandomSeed, stepsLimit: options.StepsLimit, - aiAgentTrainingOptions: options.AIAgentTrainingOptions == null ? FSharpOption.None :FSharpOption.Some(options.AIAgentTrainingOptions), - pathToModel: options.PathToModel == null ? FSharpOption.None : FSharpOption.Some(options.PathToModel)); + aiAgentTrainingOptions: options.AIAgentTrainingOptions == null ? FSharpOption.None : FSharpOption.Some(options.AIAgentTrainingOptions), + pathToModel: options.PathToModel == null ? FSharpOption.None : FSharpOption.Some(options.PathToModel), + useGPU: options.UseGPU == null ? FSharpOption.None : FSharpOption.Some(options.UseGPU), + optimize: options.Optimize == null ? FSharpOption.None : FSharpOption.Some(options.Optimize) + ); var fuzzerOptions = new FuzzerOptions( @@ -326,7 +329,7 @@ private static int CheckCoverage( public static Statistics Cover(MethodBase method, VSharpOptions options = new()) { AssemblyManager.LoadCopy(method.Module.Assembly); - var methods = new List {method}; + var methods = new List { method }; var statistics = StartExploration(methods, coverageZone.MethodZone, options); if (options.RenderTests) diff --git a/VSharp.API/VSharpOptions.cs b/VSharp.API/VSharpOptions.cs index 9132eaf6f..7d6d7901b 100644 --- a/VSharp.API/VSharpOptions.cs +++ b/VSharp.API/VSharpOptions.cs @@ -115,6 +115,8 @@ public readonly record struct VSharpOptions public readonly uint StepsLimit = DefaultStepsLimit; public readonly AIAgentTrainingOptions AIAgentTrainingOptions = null; public readonly string PathToModel = DefaultPathToModel; + public readonly bool UseGPU = false; + public readonly bool Optimize = false; /// /// Symbolic virtual machine options. @@ -133,6 +135,8 @@ public readonly record struct VSharpOptions /// Number of symbolic machine steps to stop execution after. Zero value means no limit. /// Settings for AI searcher training. /// Path to ONNX file with model to use in AI searcher. + /// Specifies whether the ONNX execution session should use a CUDA-enabled GPU. + /// Enabling options like parallel execution and various graph transformations to enhance performance of ONNX. public VSharpOptions( int timeout = DefaultTimeout, int solverTimeout = DefaultSolverTimeout, @@ -147,7 +151,9 @@ public VSharpOptions( int randomSeed = DefaultRandomSeed, uint stepsLimit = DefaultStepsLimit, AIAgentTrainingOptions aiAgentTrainingOptions = null, - string pathToModel = DefaultPathToModel) + string pathToModel = DefaultPathToModel, + bool useGPU = false, + bool optimize = false) { Timeout = timeout; SolverTimeout = solverTimeout; @@ -163,6 +169,8 @@ public VSharpOptions( StepsLimit = stepsLimit; AIAgentTrainingOptions = aiAgentTrainingOptions; PathToModel = pathToModel; + UseGPU = useGPU; + Optimize = optimize; } /// diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs index 3ab737062..e1a31ab0e 100644 --- a/VSharp.Explorer/AISearcher.fs +++ b/VSharp.Explorer/AISearcher.fs @@ -11,167 +11,203 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option 0u | Some options -> options.stepsToSwitchToAI - + let stepsToPlay = match aiAgentTrainingOptions with | None -> 0u | Some options -> options.stepsToPlay - - let mutable lastCollectedStatistics = Statistics() + + let mutable lastCollectedStatistics = + Statistics () let mutable defaultSearcherSteps = 0u - let mutable (gameState:Option) = None - let mutable useDefaultSearcher = stepsToSwitchToAI > 0u + let mutable (gameState: Option) = + None + let mutable useDefaultSearcher = + stepsToSwitchToAI > 0u let mutable afterFirstAIPeek = false - let mutable incorrectPredictedStateId = false + let mutable incorrectPredictedStateId = + false let defaultSearcher = match aiAgentTrainingOptions with - | None -> BFSSearcher() :> IForwardSearcher + | None -> BFSSearcher () :> IForwardSearcher | Some options -> match options.defaultSearchStrategy with - | BFSMode -> BFSSearcher() :> IForwardSearcher - | DFSMode -> DFSSearcher() :> IForwardSearcher - | x -> failwithf $"Unexpected default searcher {x}. DFS and BFS supported for now." + | BFSMode -> BFSSearcher () :> IForwardSearcher + | DFSMode -> DFSSearcher () :> IForwardSearcher + | x -> failwithf $"Unexpected default searcher {x}. DFS and BFS supported for now." let mutable stepsPlayed = 0u - let isInAIMode () = (not useDefaultSearcher) && afterFirstAIPeek - let q = ResizeArray<_>() - let availableStates = HashSet<_>() - let updateGameState (delta:GameState) = - match gameState with - | None -> - gameState <- Some delta - | Some s -> - let updatedBasicBlocks = delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet - let updatedStates = delta.States |> Array.map (fun s -> s.Id) |> HashSet - let vertices = - s.GraphVertices - |> Array.filter (fun v -> updatedBasicBlocks.Contains v.Id |> not) - |> ResizeArray<_> - vertices.AddRange delta.GraphVertices - let edges = - s.Map - |> Array.filter (fun e -> updatedBasicBlocks.Contains e.VertexFrom |> not) + let isInAIMode () = + (not useDefaultSearcher) && afterFirstAIPeek + let q = ResizeArray<_> () + let availableStates = HashSet<_> () + let updateGameState (delta: GameState) = + match gameState with + | None -> gameState <- Some delta + | Some s -> + let updatedBasicBlocks = + delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet + let updatedStates = + delta.States |> Array.map (fun s -> s.Id) |> HashSet + let vertices = + s.GraphVertices + |> Array.filter (fun v -> updatedBasicBlocks.Contains v.Id |> not) + |> ResizeArray<_> + vertices.AddRange delta.GraphVertices + let edges = + s.Map + |> Array.filter (fun e -> updatedBasicBlocks.Contains e.VertexFrom |> not) + |> ResizeArray<_> + edges.AddRange delta.Map + let activeStates = + vertices |> Seq.collect (fun v -> v.States) |> HashSet + + let states = + let part1 = + s.States + |> Array.filter (fun s -> activeStates.Contains s.Id && (not <| updatedStates.Contains s.Id)) |> ResizeArray<_> - edges.AddRange delta.Map - let activeStates = vertices |> Seq.collect (fun v -> v.States) |> HashSet - - let states = - let part1 = - s.States - |> Array.filter (fun s -> activeStates.Contains s.Id && (not <| updatedStates.Contains s.Id)) - |> ResizeArray<_> - - part1.AddRange delta.States - - part1.ToArray() - |> Array.map (fun s -> State(s.Id - , s.Position - , s.PathConditionSize - , s.VisitedAgainVertices - , s.VisitedNotCoveredVerticesInZone - , s.VisitedNotCoveredVerticesOutOfZone - , s.StepWhenMovedLastTime - , s.InstructionsVisitedInCurrentBlock - , s.History - , s.Children |> Array.filter activeStates.Contains) + + part1.AddRange delta.States + + part1.ToArray () + |> Array.map (fun s -> + State ( + s.Id, + s.Position, + s.PathConditionSize, + s.VisitedAgainVertices, + s.VisitedNotCoveredVerticesInZone, + s.VisitedNotCoveredVerticesOutOfZone, + s.StepWhenMovedLastTime, + s.InstructionsVisitedInCurrentBlock, + s.History, + s.Children |> Array.filter activeStates.Contains ) - - gameState <- Some <| GameState (vertices.ToArray(), states, edges.ToArray()) - - + ) + + gameState <- Some <| GameState (vertices.ToArray (), states, edges.ToArray ()) + + let init states = q.AddRange states - defaultSearcher.Init q + defaultSearcher.Init q states |> Seq.iter (availableStates.Add >> ignore) let reset () = - defaultSearcher.Reset() + defaultSearcher.Reset () defaultSearcherSteps <- 0u - lastCollectedStatistics <- Statistics() + lastCollectedStatistics <- Statistics () gameState <- None afterFirstAIPeek <- false incorrectPredictedStateId <- false useDefaultSearcher <- stepsToSwitchToAI > 0u - q.Clear() - availableStates.Clear() + q.Clear () + availableStates.Clear () let update (parent, newSates) = - if useDefaultSearcher - then defaultSearcher.Update (parent,newSates) + if useDefaultSearcher then + defaultSearcher.Update (parent, newSates) newSates |> Seq.iter (availableStates.Add >> ignore) let remove state = - if useDefaultSearcher - then defaultSearcher.Remove state + if useDefaultSearcher then + defaultSearcher.Remove state let removed = availableStates.Remove state - assert removed - for bb in state._history do bb.Key.AssociatedStates.Remove state |> ignore - - let inTrainMode = aiAgentTrainingOptions.IsSome - + assert removed + for bb in state._history do + bb.Key.AssociatedStates.Remove state |> ignore + + let inTrainMode = + aiAgentTrainingOptions.IsSome + let pick selector = - if useDefaultSearcher - then + if useDefaultSearcher then defaultSearcherSteps <- defaultSearcherSteps + 1u - if Seq.length availableStates > 0 - then - let gameStateDelta = collectGameStateDelta () + if Seq.length availableStates > 0 then + let gameStateDelta = + collectGameStateDelta () updateGameState gameStateDelta - let statistics = computeStatistics gameState.Value - Application.applicationGraphDelta.Clear() + let statistics = + computeStatistics gameState.Value + Application.applicationGraphDelta.Clear () lastCollectedStatistics <- statistics useDefaultSearcher <- defaultSearcherSteps < stepsToSwitchToAI - defaultSearcher.Pick() - elif Seq.length availableStates = 0 - then None - elif Seq.length availableStates = 1 - then Some (Seq.head availableStates) + defaultSearcher.Pick () + elif Seq.length availableStates = 0 then + None + elif Seq.length availableStates = 1 then + Some (Seq.head availableStates) else - let gameStateDelta = collectGameStateDelta () + let gameStateDelta = + collectGameStateDelta () updateGameState gameStateDelta - let statistics = computeStatistics gameState.Value - if isInAIMode() - then - let reward = computeReward lastCollectedStatistics statistics + let statistics = + computeStatistics gameState.Value + if isInAIMode () then + let reward = + computeReward lastCollectedStatistics statistics oracle.Feedback (Feedback.MoveReward reward) - Application.applicationGraphDelta.Clear() - if inTrainMode && stepsToPlay = stepsPlayed - then None + Application.applicationGraphDelta.Clear () + if inTrainMode && stepsToPlay = stepsPlayed then + None else - let toPredict = - if inTrainMode && stepsPlayed > 0u - then gameStateDelta - else gameState.Value + let toPredict = + if inTrainMode && stepsPlayed > 0u then + gameStateDelta + else + gameState.Value let stateId = oracle.Predict toPredict afterFirstAIPeek <- true - let state = availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) + let state = + availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) lastCollectedStatistics <- statistics stepsPlayed <- stepsPlayed + 1u match state with - | Some state -> - Some state + | Some state -> Some state | None -> incorrectPredictedStateId <- true oracle.Feedback (Feedback.IncorrectPredictedStateId stateId) None - new (pathToONNX:string) = + + new(pathToONNX: string, useGPU: bool, optimize: bool) = let numOfVertexAttributes = 7 let numOfStateAttributes = 7 let numOfHistoryEdgeAttributes = 2 + let createOracle (pathToONNX: string) = - let sessionOptions = new SessionOptions() - sessionOptions.ExecutionMode <- ExecutionMode.ORT_PARALLEL - sessionOptions.GraphOptimizationLevel <- GraphOptimizationLevel.ORT_ENABLE_ALL - let session = new InferenceSession(pathToONNX, sessionOptions) - let runOptions = new RunOptions() - let feedback (x:Feedback) = () - let predict (gameState:GameState) = - let stateIds = Dictionary,int>() - let verticesIds = Dictionary,int>() + let sessionOptions = + if useGPU then + SessionOptions.MakeSessionOptionWithCudaProvider (0) + else + new SessionOptions () + + if optimize then + sessionOptions.ExecutionMode <- ExecutionMode.ORT_PARALLEL + sessionOptions.GraphOptimizationLevel <- GraphOptimizationLevel.ORT_ENABLE_ALL + else + sessionOptions.GraphOptimizationLevel <- GraphOptimizationLevel.ORT_ENABLE_BASIC + + let session = + new InferenceSession (pathToONNX, sessionOptions) + let runOptions = new RunOptions () + let feedback (x: Feedback) = () + + let predict (gameState: GameState) = + let stateIds = + Dictionary, int> () + let verticesIds = + Dictionary, int> () + let networkInput = - let res = Dictionary<_,_>() + let res = Dictionary<_, _> () let gameVertices = - let shape = [| int64 gameState.GraphVertices.Length; numOfVertexAttributes |] - let attributes = Array.zeroCreate (gameState.GraphVertices.Length * numOfVertexAttributes) - for i in 0..gameState.GraphVertices.Length - 1 do + let shape = + [| + int64 gameState.GraphVertices.Length + numOfVertexAttributes + |] + let attributes = + Array.zeroCreate (gameState.GraphVertices.Length * numOfVertexAttributes) + for i in 0 .. gameState.GraphVertices.Length - 1 do let v = gameState.GraphVertices.[i] - verticesIds.Add(v.Id, i) + verticesIds.Add (v.Id, i) let j = i * numOfVertexAttributes attributes.[j] <- float32 <| if v.InCoverageZone then 1u else 0u attributes.[j + 1] <- float32 <| v.BasicBlockSize @@ -179,19 +215,24 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option Array.iteri ( - fun i e -> - index[i] <- int64 verticesIds[e.VertexFrom] - index[gameState.Map.Length + i] <- int64 verticesIds[e.VertexTo] - attributes[i] <- int64 e.Label.Token - ) - - OrtValue.CreateTensorValueFromMemory(index, shapeOfIndex) - , OrtValue.CreateTensorValueFromMemory(attributes, shapeOfAttributes) - + |> Array.iteri (fun i e -> + index[i] <- int64 verticesIds[e.VertexFrom] + index[gameState.Map.Length + i] <- int64 verticesIds[e.VertexTo] + attributes[i] <- int64 e.Label.Token + ) + + OrtValue.CreateTensorValueFromMemory (index, shapeOfIndex), + OrtValue.CreateTensorValueFromMemory (attributes, shapeOfAttributes) + let historyEdgesIndex_vertexToState, historyEdgesAttributes, parentOfEdges = - let shapeOfParentOf = [| 2L; numOfParentOfEdges |] - let parentOf = Array.zeroCreate (2 * numOfParentOfEdges) - let shapeOfHistory = [|2L; numOfHistoryEdges|] - let historyIndex_vertexToState = Array.zeroCreate (2 * numOfHistoryEdges) - let shapeOfHistoryAttributes = [| int64 numOfHistoryEdges; int64 numOfHistoryEdgeAttributes |] - let historyAttributes = Array.zeroCreate (2 * numOfHistoryEdges) + let shapeOfParentOf = + [| 2L ; numOfParentOfEdges |] + let parentOf = + Array.zeroCreate (2 * numOfParentOfEdges) + let shapeOfHistory = + [| 2L ; numOfHistoryEdges |] + let historyIndex_vertexToState = + Array.zeroCreate (2 * numOfHistoryEdges) + let shapeOfHistoryAttributes = + [| + int64 numOfHistoryEdges + int64 numOfHistoryEdgeAttributes + |] + let historyAttributes = + Array.zeroCreate (2 * numOfHistoryEdges) let mutable firstFreePositionInParentsOf = 0 - let mutable firstFreePositionInHistoryIndex = 0 - let mutable firstFreePositionInHistoryAttributes = 0 + let mutable firstFreePositionInHistoryIndex = + 0 + let mutable firstFreePositionInHistoryAttributes = + 0 gameState.States |> Array.iter (fun state -> - state.Children - |> Array.iteri (fun i children -> - let j = firstFreePositionInParentsOf + i - parentOf[j] <- int64 stateIds[state.Id] - parentOf[numOfParentOfEdges + j] <- int64 stateIds[children] - ) - firstFreePositionInParentsOf <- firstFreePositionInParentsOf + state.Children.Length - state.History - |> Array.iteri (fun i historyElem -> - let j = firstFreePositionInHistoryIndex + i - historyIndex_vertexToState[j] <- int64 verticesIds[historyElem.GraphVertexId] - historyIndex_vertexToState[numOfHistoryEdges + j] <- int64 stateIds[state.Id] - - let j = firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * i - historyAttributes[j] <- int64 historyElem.NumOfVisits - historyAttributes[j + 1] <- int64 historyElem.StepWhenVisitedLastTime - ) - firstFreePositionInHistoryIndex <- firstFreePositionInHistoryIndex + state.History.Length - firstFreePositionInHistoryAttributes <- firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * state.History.Length - ) - - OrtValue.CreateTensorValueFromMemory(historyIndex_vertexToState, shapeOfHistory) - , OrtValue.CreateTensorValueFromMemory(historyAttributes, shapeOfHistoryAttributes) - , OrtValue.CreateTensorValueFromMemory(parentOf, shapeOfParentOf) - + state.Children + |> Array.iteri (fun i children -> + let j = firstFreePositionInParentsOf + i + parentOf[j] <- int64 stateIds[state.Id] + parentOf[numOfParentOfEdges + j] <- int64 stateIds[children] + ) + firstFreePositionInParentsOf <- firstFreePositionInParentsOf + state.Children.Length + state.History + |> Array.iteri (fun i historyElem -> + let j = firstFreePositionInHistoryIndex + i + historyIndex_vertexToState[j] <- int64 verticesIds[historyElem.GraphVertexId] + historyIndex_vertexToState[numOfHistoryEdges + j] <- int64 stateIds[state.Id] + + let j = + firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * i + historyAttributes[j] <- int64 historyElem.NumOfVisits + historyAttributes[j + 1] <- int64 historyElem.StepWhenVisitedLastTime + ) + firstFreePositionInHistoryIndex <- firstFreePositionInHistoryIndex + state.History.Length + firstFreePositionInHistoryAttributes <- + firstFreePositionInHistoryAttributes + + numOfHistoryEdgeAttributes * state.History.Length + ) + + OrtValue.CreateTensorValueFromMemory (historyIndex_vertexToState, shapeOfHistory), + OrtValue.CreateTensorValueFromMemory (historyAttributes, shapeOfHistoryAttributes), + OrtValue.CreateTensorValueFromMemory (parentOf, shapeOfParentOf) + let statePosition_stateToVertex, statePosition_vertexToState = - let data_stateToVertex = Array.zeroCreate (2 * gameState.States.Length) - let data_vertexToState = Array.zeroCreate (2 * gameState.States.Length) - let shape = [|2L; gameState.States.Length|] + let data_stateToVertex = + Array.zeroCreate (2 * gameState.States.Length) + let data_vertexToState = + Array.zeroCreate (2 * gameState.States.Length) + let shape = + [| 2L ; gameState.States.Length |] let mutable firstFreePosition = 0 gameState.GraphVertices - |> Array.iter ( - fun v -> - v.States - |> Array.iteri (fun i stateId -> - let j = firstFreePosition + i - let stateIndex = int64 stateIds[stateId] - let vertexIndex = int64 verticesIds[v.Id] - data_stateToVertex[j] <- stateIndex - data_stateToVertex[stateIds.Count + j] <- vertexIndex - - data_vertexToState[j] <- vertexIndex - data_vertexToState[stateIds.Count + j] <- stateIndex - ) - firstFreePosition <- firstFreePosition + v.States.Length + |> Array.iter (fun v -> + v.States + |> Array.iteri (fun i stateId -> + let j = firstFreePosition + i + let stateIndex = int64 stateIds[stateId] + let vertexIndex = int64 verticesIds[v.Id] + data_stateToVertex[j] <- stateIndex + data_stateToVertex[stateIds.Count + j] <- vertexIndex + + data_vertexToState[j] <- vertexIndex + data_vertexToState[stateIds.Count + j] <- stateIndex ) - OrtValue.CreateTensorValueFromMemory(data_stateToVertex, shape) - ,OrtValue.CreateTensorValueFromMemory(data_vertexToState, shape) - + firstFreePosition <- firstFreePosition + v.States.Length + ) + OrtValue.CreateTensorValueFromMemory (data_stateToVertex, shape), + OrtValue.CreateTensorValueFromMemory (data_vertexToState, shape) + res.Add ("game_vertex", gameVertices) - res.Add ("state_vertex", states) - + res.Add ("state_vertex", states) + res.Add ("gamevertex_to_gamevertex_index", vertexToVertexEdgesIndex) res.Add ("gamevertex_to_gamevertex_type", vertexToVertexEdgesAttributes) - + res.Add ("gamevertex_history_statevertex_index", historyEdgesIndex_vertexToState) res.Add ("gamevertex_history_statevertex_attrs", historyEdgesAttributes) - + res.Add ("gamevertex_in_statevertex", statePosition_vertexToState) res.Add ("statevertex_parentof_statevertex", parentOfEdges) - + res - - let output = session.Run(runOptions, networkInput, session.OutputNames) - let weighedStates = output[0].GetTensorDataAsSpan().ToArray() + + let output = + session.Run (runOptions, networkInput, session.OutputNames) + let weighedStates = + output[0].GetTensorDataAsSpan().ToArray () let id = - weighedStates - |> Array.mapi (fun i v -> i,v) - |> Array.maxBy snd - |> fst - stateIds - |> Seq.find (fun kvp -> kvp.Value = id) - |> fun x -> x.Key - - Oracle(predict,feedback) - - AISearcher(createOracle pathToONNX, None) - + weighedStates |> Array.mapi (fun i v -> i, v) |> Array.maxBy snd |> fst + stateIds |> Seq.find (fun kvp -> kvp.Value = id) |> (fun x -> x.Key) + + Oracle (predict, feedback) + + AISearcher (createOracle pathToONNX, None) + interface IForwardSearcher with override x.Init states = init states - override x.Pick() = pick (always true) + override x.Pick () = pick (always true) override x.Pick selector = pick selector override x.Update (parent, newStates) = update (parent, newStates) - override x.States() = availableStates - override x.Reset() = reset() + override x.States () = availableStates + override x.Reset () = reset () override x.Remove cilState = remove cilState - override x.StatesCount with get() = availableStates.Count + override x.StatesCount = availableStates.Count diff --git a/VSharp.Explorer/Explorer.fs b/VSharp.Explorer/Explorer.fs index c1ddc51de..ec79cca9f 100644 --- a/VSharp.Explorer/Explorer.fs +++ b/VSharp.Explorer/Explorer.fs @@ -18,21 +18,21 @@ open VSharp.IL.Serializer type IReporter = abstract member ReportFinished: UnitTest -> unit - abstract member ReportException : UnitTest -> unit - abstract member ReportIIE : InsufficientInformationException -> unit - abstract member ReportInternalFail : Method -> Exception -> unit - abstract member ReportCrash : Exception -> unit + abstract member ReportException: UnitTest -> unit + abstract member ReportIIE: InsufficientInformationException -> unit + abstract member ReportInternalFail: Method -> Exception -> unit + abstract member ReportCrash: Exception -> unit -type EntryPointConfiguration(mainArguments : string[]) = +type EntryPointConfiguration(mainArguments: string[]) = - member x.Args with get() = - if mainArguments = null then None - else Some mainArguments + member x.Args = + if mainArguments = null then None else Some mainArguments -type WebConfiguration(mainArguments : string[], environmentName : string, contentRootPath : DirectoryInfo, applicationName : string) = +type WebConfiguration + (mainArguments: string[], environmentName: string, contentRootPath: DirectoryInfo, applicationName: string) = inherit EntryPointConfiguration(mainArguments) - member internal x.ToWebConfig() = + member internal x.ToWebConfig () = { environmentName = environmentName contentRootPath = contentRootPath @@ -44,23 +44,31 @@ type private IExplorer = abstract member StartExploration: (Method * state) list -> (Method * EntryPointConfiguration * state) list -> Task type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVMStatistics, reporter: IReporter) = - let options = explorationOptions.svmOptions + let options = explorationOptions.svmOptions let folderToStoreSerializationResult = match options.aiAgentTrainingOptions with | None -> "" - | Some options -> getFolderToStoreSerializationResult (Path.GetDirectoryName explorationOptions.outputDirectory.FullName) options.mapName - let hasTimeout = explorationOptions.timeout.TotalMilliseconds > 0 + | Some options -> + getFolderToStoreSerializationResult + (Path.GetDirectoryName explorationOptions.outputDirectory.FullName) + options.mapName + let hasTimeout = + explorationOptions.timeout.TotalMilliseconds > 0 let solverTimeout = - if options.solverTimeout > 0 then options.solverTimeout * 1000 + if options.solverTimeout > 0 then + options.solverTimeout * 1000 // Setting timeout / 2 as solver's timeout doesn't guarantee that SILI // stops exactly in timeout. To guarantee that we need to pass timeout // based on remaining time to solver dynamically. - elif hasTimeout then int explorationOptions.timeout.TotalMilliseconds / 2 - else -1 + elif hasTimeout then + int explorationOptions.timeout.TotalMilliseconds / 2 + else + -1 let branchReleaseTimeout = - let doubleTimeout = double explorationOptions.timeout.TotalMilliseconds + let doubleTimeout = + double explorationOptions.timeout.TotalMilliseconds if not hasTimeout then Double.PositiveInfinity elif not options.releaseBranches then doubleTimeout else doubleTimeout * 80.0 / 100.0 @@ -68,20 +76,23 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM let hasStepsLimit = options.stepsLimit > 0u do - API.ConfigureSolver(SolverPool.mkSolver(solverTimeout)) - VSharp.System.SetUp.ConfigureInternalCalls() - API.ConfigureChars(options.prettyChars) + API.ConfigureSolver (SolverPool.mkSolver (solverTimeout)) + VSharp.System.SetUp.ConfigureInternalCalls () + API.ConfigureChars (options.prettyChars) let mutable branchesReleased = false let mutable isStopped = false - let mutable isCoverageAchieved : unit -> bool = always false + let mutable isCoverageAchieved: unit -> bool = + always false - let emptyState = Memory.EmptyIsolatedState() - let interpreter = ILInterpreter() + let emptyState = + Memory.EmptyIsolatedState () + let interpreter = ILInterpreter () do if options.visualize then - DotVisualizer explorationOptions.outputDirectory :> IVisualizer |> Application.setVisualizer + DotVisualizer explorationOptions.outputDirectory :> IVisualizer + |> Application.setVisualizer SetMaxBuferSize options.maxBufferSize TestGenerator.setMaxBufferSize options.maxBufferSize @@ -94,92 +105,120 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM | _ -> false let rec mkForwardSearcher mode = - let getRandomSeedOption() = if options.randomSeed < 0 then None else Some options.randomSeed + let getRandomSeedOption () = + if options.randomSeed < 0 then + None + else + Some options.randomSeed match mode with | AIMode -> match options.aiAgentTrainingOptions with | Some aiOptions -> - match aiOptions.oracle with - | Some oracle -> AISearcher(oracle, options.aiAgentTrainingOptions) :> IForwardSearcher - | None -> failwith "Empty oracle for AI searcher." + match aiOptions.oracle with + | Some oracle -> AISearcher (oracle, options.aiAgentTrainingOptions) :> IForwardSearcher + | None -> failwith "Empty oracle for AI searcher." | None -> match options.pathToModel with - | Some s -> AISearcher s + | Some s -> + let useGPU = + options.useGPU.IsSome && options.useGPU.Value + let optimize = + options.optimize.IsSome && options.optimize.Value + AISearcher (s, useGPU, optimize) | None -> failwith "Empty model for AI searcher." - | BFSMode -> BFSSearcher() :> IForwardSearcher - | DFSMode -> DFSSearcher() :> IForwardSearcher + | BFSMode -> BFSSearcher () :> IForwardSearcher + | DFSMode -> DFSSearcher () :> IForwardSearcher | ShortestDistanceBasedMode -> ShortestDistanceBasedSearcher statistics :> IForwardSearcher - | RandomShortestDistanceBasedMode -> RandomShortestDistanceBasedSearcher(statistics, getRandomSeedOption()) :> IForwardSearcher + | RandomShortestDistanceBasedMode -> + RandomShortestDistanceBasedSearcher (statistics, getRandomSeedOption ()) :> IForwardSearcher | ContributedCoverageMode -> DFSSortedByContributedCoverageSearcher statistics :> IForwardSearcher - | ExecutionTreeMode -> ExecutionTreeSearcher(getRandomSeedOption()) + | ExecutionTreeMode -> ExecutionTreeSearcher (getRandomSeedOption ()) | FairMode baseMode -> - FairSearcher((fun _ -> mkForwardSearcher baseMode), uint branchReleaseTimeout, statistics) :> IForwardSearcher - | InterleavedMode(base1, stepCount1, base2, stepCount2) -> - InterleavedSearcher([mkForwardSearcher base1, stepCount1; mkForwardSearcher base2, stepCount2]) :> IForwardSearcher - - let mutable searcher : IBidirectionalSearcher = + FairSearcher ((fun _ -> mkForwardSearcher baseMode), uint branchReleaseTimeout, statistics) + :> IForwardSearcher + | InterleavedMode (base1, stepCount1, base2, stepCount2) -> + InterleavedSearcher ( + [ + mkForwardSearcher base1, stepCount1 + mkForwardSearcher base2, stepCount2 + ] + ) + :> IForwardSearcher + + let mutable searcher: IBidirectionalSearcher = match options.explorationMode with - | TestCoverageMode(_, searchMode) -> + | TestCoverageMode (_, searchMode) -> let baseSearcher = if options.recThreshold > 0u then - GuidedSearcher(mkForwardSearcher searchMode, RecursionBasedTargetManager(statistics, options.recThreshold)) :> IForwardSearcher + GuidedSearcher ( + mkForwardSearcher searchMode, + RecursionBasedTargetManager (statistics, options.recThreshold) + ) + :> IForwardSearcher else mkForwardSearcher searchMode - BidirectionalSearcher(baseSearcher, BackwardSearcher(), DummyTargetedSearcher.DummyTargetedSearcher()) :> IBidirectionalSearcher - | StackTraceReproductionMode _ -> __notImplemented__() + BidirectionalSearcher (baseSearcher, BackwardSearcher (), DummyTargetedSearcher.DummyTargetedSearcher ()) + :> IBidirectionalSearcher + | StackTraceReproductionMode _ -> __notImplemented__ () - let releaseBranches() = + let releaseBranches () = if not branchesReleased then branchesReleased <- true - statistics.OnBranchesReleased() - ReleaseBranches() - let dfsSearcher = DFSSortedByContributedCoverageSearcher statistics :> IForwardSearcher - let bidirectionalSearcher = OnlyForwardSearcher(dfsSearcher) - dfsSearcher.Init <| searcher.States() + statistics.OnBranchesReleased () + ReleaseBranches () + let dfsSearcher = + DFSSortedByContributedCoverageSearcher statistics :> IForwardSearcher + let bidirectionalSearcher = + OnlyForwardSearcher (dfsSearcher) + dfsSearcher.Init <| searcher.States () searcher <- bidirectionalSearcher - let reportStateIncomplete (state : cilState) = + let reportStateIncomplete (state: cilState) = searcher.Remove state - statistics.IncompleteStates.Add(state) + statistics.IncompleteStates.Add (state) Application.terminateState state reporter.ReportIIE state.iie.Value let reportIncomplete = reporter.ReportIIE - let reportState (suite : testSuite) (cilState : cilState) = + let reportState (suite: testSuite) (cilState: cilState) = try - let isNewHistory() = - let methodHistory = Set.filter (fun h -> h.method.InCoverageZone) cilState.history + let isNewHistory () = + let methodHistory = + Set.filter (fun h -> h.method.InCoverageZone) cilState.history Set.isEmpty methodHistory || Set.exists (not << statistics.IsBasicBlockCoveredByTest) methodHistory let isError = suite.IsErrorSuite let isNewTest = match suite with - | Test -> isNewHistory() - | Error(msg, isFatal) -> statistics.IsNewError cilState msg isFatal + | Test -> isNewHistory () + | Error (msg, isFatal) -> statistics.IsNewError cilState msg isFatal if isNewTest then let state = cilState.state - let callStackSize = Memory.CallStackSize state + let callStackSize = + Memory.CallStackSize state let entryMethod = cilState.EntryMethod - let hasException = cilState.IsUnhandledException + let hasException = + cilState.IsUnhandledException if isError && not hasException then if entryMethod.HasParameterOnStack then Memory.ForcePopFrames (callStackSize - 2) state - else Memory.ForcePopFrames (callStackSize - 1) state + else + Memory.ForcePopFrames (callStackSize - 1) state match TestGenerator.state2test suite entryMethod state with | Some test -> - statistics.TrackFinished(cilState, isError) + statistics.TrackFinished (cilState, isError) match suite with | Test -> reporter.ReportFinished test | Error _ -> reporter.ReportException test - if isCoverageAchieved() then + if isCoverageAchieved () then isStopped <- true | None -> () with :? InsufficientInformationException as e -> cilState.SetIIE e reportStateIncomplete cilState - let reportStateInternalFail (state : cilState) (e : Exception) = + let reportStateInternalFail (state: cilState) (e: Exception) = match e with | :? InsufficientInformationException as e -> if not state.IsIIEState then @@ -187,114 +226,136 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM reportStateIncomplete state | _ -> searcher.Remove state - statistics.InternalFails.Add(e) + statistics.InternalFails.Add (e) Application.terminateState state reporter.ReportInternalFail state.EntryMethod e - let reportInternalFail (method : Method) (e : Exception) = + let reportInternalFail (method: Method) (e: Exception) = match e with - | :? InsufficientInformationException as e -> - reportIncomplete e + | :? InsufficientInformationException as e -> reportIncomplete e | _ -> - statistics.InternalFails.Add(e) + statistics.InternalFails.Add (e) reporter.ReportInternalFail method e - let reportFinished (state : cilState) = + let reportFinished (state: cilState) = let result = Memory.StateResult state.state Logger.info $"Result of method {state.EntryMethod.FullName} is {result}" Application.terminateState state reportState Test state - let wrapOnError isFatal (state : cilState) errorMessage = + let wrapOnError isFatal (state: cilState) errorMessage = if not <| String.IsNullOrWhiteSpace errorMessage then Logger.info $"Error in {state.EntryMethod.FullName}: {errorMessage}" Application.terminateState state - let testSuite = Error(errorMessage, isFatal) + let testSuite = + Error (errorMessage, isFatal) reportState testSuite state let reportError = wrapOnError false let reportFatalError = wrapOnError true let reportCrash = reporter.ReportCrash - let isTimeoutReached() = hasTimeout && statistics.CurrentExplorationTime.TotalMilliseconds >= explorationOptions.timeout.TotalMilliseconds - let shouldReleaseBranches() = options.releaseBranches && statistics.CurrentExplorationTime.TotalMilliseconds >= branchReleaseTimeout - let isStepsLimitReached() = hasStepsLimit && statistics.StepsCount >= options.stepsLimit - - static member private AllocateByRefParameters initialState (method : Method) = - let allocateIfByRef (pi : ParameterInfo) = + let isTimeoutReached () = + hasTimeout + && statistics.CurrentExplorationTime.TotalMilliseconds + >= explorationOptions.timeout.TotalMilliseconds + let shouldReleaseBranches () = + options.releaseBranches + && statistics.CurrentExplorationTime.TotalMilliseconds >= branchReleaseTimeout + let isStepsLimitReached () = + hasStepsLimit && statistics.StepsCount >= options.stepsLimit + + static member private AllocateByRefParameters initialState (method: Method) = + let allocateIfByRef (pi: ParameterInfo) = let parameterType = pi.ParameterType if parameterType.IsByRef then if Memory.CallStackSize initialState = 0 then Memory.NewStackFrame initialState None [] - let typ = parameterType.GetElementType() + let typ = parameterType.GetElementType () let position = pi.Position + 1 - let stackRef = Memory.AllocateTemporaryLocalVariableOfType initialState pi.Name position typ + let stackRef = + Memory.AllocateTemporaryLocalVariableOfType initialState pi.Name position typ Some stackRef else None method.Parameters |> Array.map allocateIfByRef |> Array.toList - member private x.FormIsolatedInitialStates (method : Method, initialState : state) = + member private x.FormIsolatedInitialStates (method: Method, initialState: state) = try initialState.model <- Memory.EmptyModel method let declaringType = method.DeclaringType - let cilState = cilState.CreateInitial method initialState + let cilState = + cilState.CreateInitial method initialState let this = if method.HasThis then if Types.IsValueType declaringType then Memory.NewStackFrame initialState None [] - Memory.AllocateTemporaryLocalVariableOfType initialState "this" 0 declaringType |> Some + Memory.AllocateTemporaryLocalVariableOfType initialState "this" 0 declaringType + |> Some else - let this = Memory.MakeSymbolicThis initialState method + let this = + Memory.MakeSymbolicThis initialState method !!(IsNullReference this) |> AddConstraint initialState Some this - else None - let parameters = SVMExplorer.AllocateByRefParameters initialState method + else + None + let parameters = + SVMExplorer.AllocateByRefParameters initialState method Memory.InitFunctionFrame initialState method this (Some parameters) match this with | Some this -> SolveThisType initialState this | _ -> () - let cilStates = ILInterpreter.CheckDisallowNullAttribute method None cilState false id - assert(List.length cilStates = 1) + let cilStates = + ILInterpreter.CheckDisallowNullAttribute method None cilState false id + assert (List.length cilStates = 1) if not method.IsStaticConstructor then let cilState = List.head cilStates interpreter.InitializeStatics cilState declaringType List.singleton else Memory.MarkTypeInitialized initialState declaringType cilStates - with - | e -> + with e -> reportInternalFail method e [] - member private x.FormEntryPointInitialStates (method : Method, config : EntryPointConfiguration, initialState : state) : cilState list = + member private x.FormEntryPointInitialStates + (method: Method, config: EntryPointConfiguration, initialState: state) + : cilState list = try assert method.IsStatic let optionArgs = config.Args let parameters = method.Parameters - let hasParameters = Array.length parameters > 0 - let state = { initialState with complete = not hasParameters || Option.isSome optionArgs } + let hasParameters = + Array.length parameters > 0 + let state = + { initialState with + complete = not hasParameters || Option.isSome optionArgs + } state.model <- Memory.EmptyModel method match optionArgs with | Some args -> let stringType = typeof let argsNumber = MakeNumber args.Length - let argsRef = Memory.AllocateConcreteVectorArray state argsNumber stringType args - let args = Some (List.singleton (Some argsRef)) + let argsRef = + Memory.AllocateConcreteVectorArray state argsNumber stringType args + let args = + Some (List.singleton (Some argsRef)) Memory.InitFunctionFrame state method None args | None when hasParameters -> Memory.InitFunctionFrame state method None None // NOTE: if args are symbolic, constraint 'args != null' is added - assert(Array.length parameters = 1) + assert (Array.length parameters = 1) let argsParameter = Array.head parameters - let argsParameterTerm = Memory.ReadArgument state argsParameter + let argsParameterTerm = + Memory.ReadArgument state argsParameter AddConstraint state (!!(IsNullReference argsParameterTerm)) // Filling model with default args to match PC let modelState = match state.model with | StateModel modelState -> modelState - | _ -> __unreachable__() - let argsForModel = Memory.AllocateVectorArray modelState (MakeNumber 0) typeof + | _ -> __unreachable__ () + let argsForModel = + Memory.AllocateVectorArray modelState (MakeNumber 0) typeof Memory.WriteStackLocation modelState (ParameterKey argsParameter) argsForModel | None -> let args = Some List.empty @@ -303,45 +364,47 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM let initialState = match config with | :? WebConfiguration as config -> - let webConfig = config.ToWebConfig() + let webConfig = config.ToWebConfig () cilState.CreateWebInitial method state webConfig | _ -> cilState.CreateInitial method state - [initialState] - with - | e -> + [ initialState ] + with e -> reportInternalFail method e [] - member private x.Forward (s : cilState) = + member private x.Forward (s: cilState) = let loc = s.approximateLoc let ip = s.CurrentIp let stackSize = s.StackSize // TODO: update pobs when visiting new methods; use coverageZone - let goodStates, iieStates, errors = interpreter.ExecuteOneInstruction s + let goodStates, iieStates, errors = + interpreter.ExecuteOneInstruction s for s in goodStates @ iieStates @ errors do if not s.HasRuntimeExceptionOrError then statistics.TrackStepForward s ip stackSize - let goodStates, toReportFinished = goodStates |> List.partition (fun s -> s.IsExecutable || s.IsIsolated) + let goodStates, toReportFinished = + goodStates |> List.partition (fun s -> s.IsExecutable || s.IsIsolated) toReportFinished |> List.iter reportFinished - let errors, _ = errors |> List.partition (fun s -> not s.HasReportedError) - let errors, toReportExceptions = errors |> List.partition (fun s -> s.IsIsolated || not s.IsStoppedByException) - let runtimeExceptions, userExceptions = toReportExceptions |> List.partition (fun s -> s.HasRuntimeExceptionOrError) + let errors, _ = + errors |> List.partition (fun s -> not s.HasReportedError) + let errors, toReportExceptions = + errors |> List.partition (fun s -> s.IsIsolated || not s.IsStoppedByException) + let runtimeExceptions, userExceptions = + toReportExceptions |> List.partition (fun s -> s.HasRuntimeExceptionOrError) runtimeExceptions |> List.iter (fun state -> reportError state null) userExceptions |> List.iter reportFinished - let iieStates, toReportIIE = iieStates |> List.partition (fun s -> s.IsIsolated) + let iieStates, toReportIIE = + iieStates |> List.partition (fun s -> s.IsIsolated) toReportIIE |> List.iter reportStateIncomplete let mutable sIsStopped = false let newStates = match goodStates, iieStates, errors with - | s'::goodStates, _, _ when LanguagePrimitives.PhysicalEquality s s' -> - goodStates @ iieStates @ errors - | _, s'::iieStates, _ when LanguagePrimitives.PhysicalEquality s s' -> - goodStates @ iieStates @ errors - | _, _, s'::errors when LanguagePrimitives.PhysicalEquality s s' -> - goodStates @ iieStates @ errors + | s' :: goodStates, _, _ when LanguagePrimitives.PhysicalEquality s s' -> goodStates @ iieStates @ errors + | _, s' :: iieStates, _ when LanguagePrimitives.PhysicalEquality s s' -> goodStates @ iieStates @ errors + | _, _, s' :: errors when LanguagePrimitives.PhysicalEquality s s' -> goodStates @ iieStates @ errors | _ -> sIsStopped <- true - goodStates @ iieStates @ errors + goodStates @ iieStates @ errors s.children <- s.children @ newStates Application.moveState loc s (Seq.cast<_> newStates) @@ -350,8 +413,8 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM if sIsStopped then searcher.Remove s - member private x.Backward p' (s' : cilState) = - assert(s'.CurrentLoc = p'.loc) + member private x.Backward p' (s': cilState) = + assert (s'.CurrentLoc = p'.loc) let sLvl = s'.Level if p'.lvl >= sLvl then let lvl = p'.lvl - sLvl @@ -360,105 +423,126 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM | true when not s'.IsIsolated -> searcher.Answer p' (Witnessed s') | true -> statistics.TrackStepBackward p' s' - let p = {loc = s'.StartingLoc; lvl = lvl; pc = pc} + let p = + { + loc = s'.StartingLoc + lvl = lvl + pc = pc + } Logger.trace $"Backward:\nWas: {p'}\nNow: {p}\n\n" Application.addGoal p.loc searcher.UpdatePobs p' p - | false -> - Logger.trace "UNSAT for pob = %O and s'.PC = %s" p' (API.Print.PrintPC s'.state.pc) + | false -> Logger.trace "UNSAT for pob = %O and s'.PC = %s" p' (API.Print.PrintPC s'.state.pc) + + member private x.BidirectionalSymbolicExecution () = - member private x.BidirectionalSymbolicExecution() = - let mutable action = Stop - - let pick() = - match searcher.Pick() with + + let pick () = + match searcher.Pick () with | Stop -> false - | a -> action <- a; true + | a -> + action <- a + true (* TODO: checking for timeout here is not fine-grained enough (that is, we can work significantly beyond the timeout, but we'll live with it for now. *) - while not isStopped && not <| isStepsLimitReached() && not <| isTimeoutReached() && pick() do - if shouldReleaseBranches() then - releaseBranches() + while not isStopped + && not <| isStepsLimitReached () + && not <| isTimeoutReached () + && pick () do + if shouldReleaseBranches () then + releaseBranches () match action with | GoFront s -> try - if options.aiAgentTrainingOptions.IsSome && options.aiAgentTrainingOptions.Value.serializeSteps + if + options.aiAgentTrainingOptions.IsSome + && options.aiAgentTrainingOptions.Value.serializeSteps then - dumpGameState (Path.Combine(folderToStoreSerializationResult, string firstFreeEpisodeNumber)) s.internalId + dumpGameState + (Path.Combine (folderToStoreSerializationResult, string firstFreeEpisodeNumber)) + s.internalId firstFreeEpisodeNumber <- firstFreeEpisodeNumber + 1 - x.Forward(s) - with - | e -> reportStateInternalFail s e - | GoBack(s, p) -> + x.Forward (s) + with e -> + reportStateInternalFail s e + | GoBack (s, p) -> try x.Backward p s - with - | e -> reportStateInternalFail s e - | Stop -> __unreachable__() + with e -> + reportStateInternalFail s e + | Stop -> __unreachable__ () member private x.AnswerPobs initialStates = - statistics.ExplorationStarted() + statistics.ExplorationStarted () // For backward compatibility. TODO: remove main pobs at all let mainPobs = [] Application.spawnStates (Seq.cast<_> initialStates) mainPobs |> Seq.map (fun pob -> pob.loc) |> Seq.toArray |> Application.addGoals searcher.Init initialStates mainPobs - initialStates |> Seq.filter (fun s -> s.IsIIEState) |> Seq.iter reportStateIncomplete - x.BidirectionalSymbolicExecution() - searcher.Statuses() |> Seq.iter (fun (pob, status) -> + initialStates + |> Seq.filter (fun s -> s.IsIIEState) + |> Seq.iter reportStateIncomplete + x.BidirectionalSymbolicExecution () + searcher.Statuses () + |> Seq.iter (fun (pob, status) -> match status with - | pobStatus.Unknown -> - Logger.warning $"Unknown status for pob at {pob.loc}" - | _ -> ()) + | pobStatus.Unknown -> Logger.warning $"Unknown status for pob at {pob.loc}" + | _ -> () + ) interface IExplorer with member x.Reset entryMethods = - HashMap.clear() - API.Reset() - SolverPool.reset() - searcher.Reset() + HashMap.clear () + API.Reset () + SolverPool.reset () + searcher.Reset () isStopped <- false branchesReleased <- false SolverInteraction.setOnSolverStarted statistics.SolverStarted SolverInteraction.setOnSolverStopped statistics.SolverStopped - AcquireBranches() + AcquireBranches () isCoverageAchieved <- always false match options.explorationMode with | TestCoverageMode _ -> if options.stopOnCoverageAchieved > 0 then - let checkCoverage() = - let cov = statistics.GetCurrentCoverage entryMethods + let checkCoverage () = + let cov = + statistics.GetCurrentCoverage entryMethods cov >= options.stopOnCoverageAchieved isCoverageAchieved <- checkCoverage - | StackTraceReproductionMode _ -> __notImplemented__() + | StackTraceReproductionMode _ -> __notImplemented__ () member x.StartExploration isolated entryPoints = task { try try interpreter.ConfigureErrorReporter reportError reportFatalError - let isolatedInitialStates = isolated |> List.collect x.FormIsolatedInitialStates - let entryPointsInitialStates = entryPoints |> List.collect x.FormEntryPointInitialStates + let isolatedInitialStates = + isolated |> List.collect x.FormIsolatedInitialStates + let entryPointsInitialStates = + entryPoints |> List.collect x.FormEntryPointInitialStates let iieStates, initialStates = isolatedInitialStates @ entryPointsInitialStates |> List.partition (fun state -> state.IsIIEState) iieStates |> List.iter reportStateIncomplete - statistics.SetStatesGetter(fun () -> searcher.States()) - statistics.SetStatesCountGetter(fun () -> searcher.StatesCount) + statistics.SetStatesGetter (fun () -> searcher.States ()) + statistics.SetStatesCountGetter (fun () -> searcher.StatesCount) if not initialStates.IsEmpty then x.AnswerPobs initialStates - with e -> reportCrash e + with e -> + reportCrash e finally try - statistics.ExplorationFinished() - API.Restore() - searcher.Reset() - with e -> reportCrash e + statistics.ExplorationFinished () + API.Restore () + searcher.Reset () + with e -> + reportCrash e } - member private x.Stop() = isStopped <- true + member private x.Stop () = isStopped <- true type private FuzzerExplorer(explorationOptions: ExplorationOptions, statistics: SVMStatistics) = @@ -467,84 +551,99 @@ type private FuzzerExplorer(explorationOptions: ExplorationOptions, statistics: member x.Reset _ = () member x.StartExploration isolated _ = - let saveStatistic = statistics.SetBasicBlocksAsCoveredByTest - let outputDir = explorationOptions.outputDirectory.FullName - let cancellationTokenSource = new CancellationTokenSource() - cancellationTokenSource.CancelAfter(explorationOptions.timeout) - let methodsBase = isolated |> List.map (fun (m, _) -> (m :> IMethod).MethodBase) + let saveStatistic = + statistics.SetBasicBlocksAsCoveredByTest + let outputDir = + explorationOptions.outputDirectory.FullName + let cancellationTokenSource = + new CancellationTokenSource () + cancellationTokenSource.CancelAfter (explorationOptions.timeout) + let methodsBase = + isolated |> List.map (fun (m, _) -> (m :> IMethod).MethodBase) task { try - let targetAssemblyPath = (Seq.head methodsBase).Module.Assembly.Location + let targetAssemblyPath = + (Seq.head methodsBase).Module.Assembly.Location let onCancelled () = Logger.warning "Fuzzer canceled" - let interactor = Fuzzer.Interactor( - targetAssemblyPath, - methodsBase, - cancellationTokenSource.Token, - outputDir, - saveStatistic, - onCancelled - ) + let interactor = + Fuzzer.Interactor ( + targetAssemblyPath, + methodsBase, + cancellationTokenSource.Token, + outputDir, + saveStatistic, + onCancelled + ) do! interactor.StartFuzzing () with e -> Logger.error $"Fuzzer unhandled exception: {e}" raise e } -type public Explorer(options : ExplorationOptions, reporter: IReporter) = +type public Explorer(options: ExplorationOptions, reporter: IReporter) = - let statistics = new SVMStatistics(Seq.empty, true) + let statistics = + new SVMStatistics (Seq.empty, true) let explorers = let createFuzzer () = - FuzzerExplorer(options, statistics) :> IExplorer + FuzzerExplorer (options, statistics) :> IExplorer let createSVM () = - SVMExplorer(options, statistics, reporter) :> IExplorer + SVMExplorer (options, statistics, reporter) :> IExplorer match options.explorationModeOptions with - | Fuzzing _ -> createFuzzer() |> Array.singleton - | SVM _ -> createSVM() |> Array.singleton - | Combined _ -> - [| - createFuzzer() - createSVM() - |] - - let inCoverageZone coverageZone (entryMethods : Method list) = + | Fuzzing _ -> createFuzzer () |> Array.singleton + | SVM _ -> createSVM () |> Array.singleton + | Combined _ -> [| createFuzzer () ; createSVM () |] + + let inCoverageZone coverageZone (entryMethods: Method list) = match coverageZone with | MethodZone -> fun method -> entryMethods |> List.contains method - | ClassZone -> fun method -> entryMethods |> List.exists (fun m -> method.DeclaringType.TypeHandle = m.DeclaringType.TypeHandle) - | ModuleZone -> fun method -> entryMethods |> List.exists (fun m -> method.Module.ModuleHandle = m.Module.ModuleHandle) - - member private x.TrySubstituteTypeParameters (state : state) (methodBase : MethodBase) = - let method = Application.getMethod methodBase - let getConcreteType = function - | ConcreteType t -> Some t - | _ -> None + | ClassZone -> + fun method -> + entryMethods + |> List.exists (fun m -> method.DeclaringType.TypeHandle = m.DeclaringType.TypeHandle) + | ModuleZone -> + fun method -> + entryMethods + |> List.exists (fun m -> method.Module.ModuleHandle = m.Module.ModuleHandle) + + member private x.TrySubstituteTypeParameters (state: state) (methodBase: MethodBase) = + let method = + Application.getMethod methodBase + let getConcreteType = + function + | ConcreteType t -> Some t + | _ -> None try if method.ContainsGenericParameters then match SolveGenericMethodParameters state.typeStorage method with - | Some(classParams, methodParams) -> - let classParams = classParams |> Array.choose getConcreteType - let methodParams = methodParams |> Array.choose getConcreteType + | Some (classParams, methodParams) -> + let classParams = + classParams |> Array.choose getConcreteType + let methodParams = + methodParams |> Array.choose getConcreteType let declaringType = methodBase.DeclaringType - let isSuitableType() = + let isSuitableType () = not declaringType.IsGenericType || classParams.Length = declaringType.GetGenericArguments().Length - let isSuitableMethod() = + let isSuitableMethod () = methodBase.IsConstructor || not methodBase.IsGenericMethod || methodParams.Length = methodBase.GetGenericArguments().Length - if isSuitableType() && isSuitableMethod() then - let reflectedType = Reflection.concretizeTypeParameters methodBase.ReflectedType classParams - let method = Reflection.concretizeMethodParameters reflectedType methodBase methodParams + if isSuitableType () && isSuitableMethod () then + let reflectedType = + Reflection.concretizeTypeParameters methodBase.ReflectedType classParams + let method = + Reflection.concretizeMethodParameters reflectedType methodBase methodParams Some method else None | _ -> None - else Some methodBase - with - | e -> + else + Some methodBase + with e -> reporter.ReportInternalFail method e None @@ -554,45 +653,50 @@ type public Explorer(options : ExplorationOptions, reporter: IReporter) = for explorer in explorers do explorer.Reset entryMethods - member x.StartExploration (isolated : MethodBase seq) (entryPoints : (MethodBase * EntryPointConfiguration) seq) : unit = + member x.StartExploration + (isolated: MethodBase seq) + (entryPoints: (MethodBase * EntryPointConfiguration) seq) + : unit = try let trySubstituteTypeParameters method = - let emptyState = Memory.EmptyIsolatedState() + let emptyState = + Memory.EmptyIsolatedState () (Option.defaultValue method (x.TrySubstituteTypeParameters emptyState method), emptyState) let isolated = isolated |> Seq.map trySubstituteTypeParameters - |> Seq.map (fun (m, s) -> Application.getMethod m, s) |> Seq.toList + |> Seq.map (fun (m, s) -> Application.getMethod m, s) + |> Seq.toList let entryPoints = entryPoints |> Seq.map (fun (m, a) -> let m, s = trySubstituteTypeParameters m - (Application.getMethod m, a, s)) + (Application.getMethod m, a, s) + ) |> Seq.toList - (List.map fst isolated) - @ (List.map (fun (x, _, _) -> x) entryPoints) - |> x.Reset + (List.map fst isolated) @ (List.map (fun (x, _, _) -> x) entryPoints) |> x.Reset let explorationTasks = - explorers - |> Array.map (fun e -> e.StartExploration isolated entryPoints) + explorers |> Array.map (fun e -> e.StartExploration isolated entryPoints) - let finished = Task.WaitAll(explorationTasks, options.timeout) + let finished = + Task.WaitAll (explorationTasks, options.timeout) - if not finished then Logger.warning "Exploration cancelled" + if not finished then + Logger.warning "Exploration cancelled" for explorationTask in explorationTasks do if explorationTask.IsFaulted then for ex in explorationTask.Exception.InnerExceptions do - reporter.ReportCrash ex + reporter.ReportCrash ex with | :? AggregateException as e -> reporter.ReportCrash e.InnerException | e -> reporter.ReportCrash e - member x.Statistics with get() = statistics + member x.Statistics = statistics interface IDisposable with - member x.Dispose() = (statistics :> IDisposable).Dispose() + member x.Dispose () = (statistics :> IDisposable).Dispose () diff --git a/VSharp.Explorer/Options.fs b/VSharp.Explorer/Options.fs index e78bac048..4b29327d8 100644 --- a/VSharp.Explorer/Options.fs +++ b/VSharp.Explorer/Options.fs @@ -24,19 +24,23 @@ type explorationMode = | TestCoverageMode of coverageZone * searchMode | StackTraceReproductionMode of StackTrace -type fuzzerIsolation = - | Process +type fuzzerIsolation = | Process -type FuzzerOptions = { - isolation: fuzzerIsolation - coverageZone: coverageZone -} +type FuzzerOptions = + { + isolation: fuzzerIsolation + coverageZone: coverageZone + } [] type Oracle = val Predict: GameState -> uint val Feedback: Feedback -> unit - new (predict, feedback) = {Predict=predict; Feedback = feedback} + new(predict, feedback) = + { + Predict = predict + Feedback = feedback + } /// /// Options used in AI agent training. @@ -56,34 +60,38 @@ type AIAgentTrainingOptions = mapName: string oracle: Option } - -type SVMOptions = { - explorationMode : explorationMode - recThreshold : uint - solverTimeout : int - visualize : bool - releaseBranches : bool - maxBufferSize : int - prettyChars : bool // If true, 33 <= char <= 126, otherwise any char - checkAttributes : bool - stopOnCoverageAchieved : int - randomSeed : int - stepsLimit : uint - aiAgentTrainingOptions: Option - pathToModel: Option -} + +type SVMOptions = + { + explorationMode: explorationMode + recThreshold: uint + solverTimeout: int + visualize: bool + releaseBranches: bool + maxBufferSize: int + prettyChars: bool // If true, 33 <= char <= 126, otherwise any char + checkAttributes: bool + stopOnCoverageAchieved: int + randomSeed: int + stepsLimit: uint + aiAgentTrainingOptions: Option + pathToModel: Option + useGPU: Option + optimize: Option + } type explorationModeOptions = | Fuzzing of FuzzerOptions | SVM of SVMOptions | Combined of SVMOptions * FuzzerOptions -type ExplorationOptions = { - timeout : System.TimeSpan - outputDirectory : DirectoryInfo - explorationModeOptions : explorationModeOptions -} -with +type ExplorationOptions = + { + timeout: System.TimeSpan + outputDirectory: DirectoryInfo + explorationModeOptions: explorationModeOptions + } + member this.fuzzerOptions = match this.explorationModeOptions with | Fuzzing x -> x @@ -100,7 +108,7 @@ with match this.explorationModeOptions with | SVM x -> match x.explorationMode with - | TestCoverageMode (coverageZone, _) -> coverageZone + | TestCoverageMode (coverageZone, _) -> coverageZone | StackTraceReproductionMode _ -> failwith "" | Combined (_, x) -> x.coverageZone | Fuzzing x -> x.coverageZone diff --git a/VSharp.Explorer/VSharp.Explorer.fsproj b/VSharp.Explorer/VSharp.Explorer.fsproj index 234fdb506..708537b7d 100644 --- a/VSharp.Explorer/VSharp.Explorer.fsproj +++ b/VSharp.Explorer/VSharp.Explorer.fsproj @@ -49,7 +49,7 @@ - + diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs index d6414fb81..1ae0927da 100644 --- a/VSharp.ML.GameServer.Runner/Main.fs +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -23,14 +23,14 @@ type ExplorationResult = val TestsCount: uint val ErrorsCount: uint val StepsCount: uint - new (actualCoverage, testsCount, errorsCount, stepsCount) = + new(actualCoverage, testsCount, errorsCount, stepsCount) = { ActualCoverage = actualCoverage TestsCount = testsCount ErrorsCount = errorsCount StepsCount = stepsCount } - + type Mode = | Server = 0 | Generator = 1 @@ -38,156 +38,201 @@ type CliArguments = | [] Port of int | [] DatasetBasePath of string | [] DatasetDescription of string - | [] Mode of Mode + | [] Mode of Mode | [] OutFolder of string | [] StepsToSerialize of uint + | [] UseGPU + | [] Optimize + interface IArgParserTemplate with member s.Usage = match s with | Port _ -> "Port to communicate with game client." - | DatasetBasePath _ -> "Full path to dataset root directory. Dll location is /" + | DatasetBasePath _ -> + "Full path to dataset root directory. Dll location is /" | DatasetDescription _ -> "Full paths to JSON-file with dataset description." - | Mode _ -> "Mode to run application. Server --- to train network, Generator --- to generate data for training." + | Mode _ -> + "Mode to run application. Server --- to train network, Generator --- to generate data for training." | OutFolder _ -> "Folder to store generated data." | StepsToSerialize _ -> "Maximal number of steps for each method to serialize." - + | UseGPU -> "Specifies whether the ONNX execution session should use a CUDA-enabled GPU." + | Optimize -> + "Enabling options like parallel execution and various graph transformations to enhance performance of ONNX." + let mutable inTrainMode = true -let explore (gameMap:GameMap) options = - let assembly = RunnerProgram.TryLoadAssembly <| FileInfo gameMap.AssemblyFullName - let method = RunnerProgram.ResolveMethod(assembly, gameMap.NameOfObjectToCover) - let statistics = TestGenerator.Cover(method, options) - let actualCoverage = - try +let explore (gameMap: GameMap) options = + let assembly = + RunnerProgram.TryLoadAssembly <| FileInfo gameMap.AssemblyFullName + let method = + RunnerProgram.ResolveMethod (assembly, gameMap.NameOfObjectToCover) + let statistics = + TestGenerator.Cover (method, options) + let actualCoverage = + try let testsDir = statistics.OutputDir let _expectedCoverage = 100 - let exploredMethodInfo = AssemblyManager.NormalizeMethod method - let status,actualCoverage,message = VSharp.Test.TestResultChecker.Check(testsDir, exploredMethodInfo :?> MethodInfo, _expectedCoverage) + let exploredMethodInfo = + AssemblyManager.NormalizeMethod method + let status, actualCoverage, message = + VSharp.Test.TestResultChecker.Check (testsDir, exploredMethodInfo :?> MethodInfo, _expectedCoverage) printfn $"Actual coverage for {gameMap.MapName}: {actualCoverage}" - if actualCoverage < 0 then 0u else uint actualCoverage * 1u - with - e -> + if actualCoverage < 0 then + 0u + else + uint actualCoverage * 1u + with e -> printfn $"Coverage checking problem:{e.Message} \n {e.StackTrace}" 0u - ExplorationResult(actualCoverage, statistics.TestsCount * 1u, statistics.ErrorsCount *1u, statistics.StepsCount * 1u) + ExplorationResult ( + actualCoverage, + statistics.TestsCount * 1u, + statistics.ErrorsCount * 1u, + statistics.StepsCount * 1u + ) -let loadGameMaps (datasetDescriptionFilePath:string) = - let jsonString = File.ReadAllText datasetDescriptionFilePath - let maps = ResizeArray() - for map in System.Text.Json.JsonSerializer.Deserialize jsonString do - maps.Add map +let loadGameMaps (datasetDescriptionFilePath: string) = + let jsonString = + File.ReadAllText datasetDescriptionFilePath + let maps = ResizeArray () + for map in System.Text.Json.JsonSerializer.Deserialize jsonString do + maps.Add map maps -let ws port outputDirectory (webSocket : WebSocket) (context: HttpContext) = - let mutable loop = true - - socket { - - let sendResponse (message:OutgoingMessage) = - let byteResponse = - serializeOutgoingMessage message - |> System.Text.Encoding.UTF8.GetBytes - |> ByteSegment - webSocket.send Text byteResponse true - - let oracle = - let feedback = - fun (feedback: Feedback) -> - let res = - socket { - let message = - match feedback with - | Feedback.ServerError s -> OutgoingMessage.ServerError s - | Feedback.MoveReward reward -> OutgoingMessage.MoveReward reward - | Feedback.IncorrectPredictedStateId i -> OutgoingMessage.IncorrectPredictedStateId i - do! sendResponse message - } - match Async.RunSynchronously res with - | Choice1Of2 () -> () - | Choice2Of2 error -> failwithf $"Error: %A{error}" - - let predict = - let mutable cnt = 0u - fun (gameState:GameState) -> - let toDot drawHistory = - let file = Path.Join ("dot",$"{cnt}.dot") - gameState.ToDot file drawHistory - cnt <- cnt + 1u - //toDot false - let res = - socket { - do! sendResponse (ReadyForNextStep gameState) - let! msg = webSocket.read() - let res = - match msg with - | (Text, data, true) -> - let msg = deserializeInputMessage data +let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = + let mutable loop = true + + socket { + + let sendResponse (message: OutgoingMessage) = + let byteResponse = + serializeOutgoingMessage message + |> System.Text.Encoding.UTF8.GetBytes + |> ByteSegment + webSocket.send Text byteResponse true + + let oracle = + let feedback = + fun (feedback: Feedback) -> + let res = + socket { + let message = + match feedback with + | Feedback.ServerError s -> OutgoingMessage.ServerError s + | Feedback.MoveReward reward -> OutgoingMessage.MoveReward reward + | Feedback.IncorrectPredictedStateId i -> OutgoingMessage.IncorrectPredictedStateId i + do! sendResponse message + } + match Async.RunSynchronously res with + | Choice1Of2 () -> () + | Choice2Of2 error -> failwithf $"Error: %A{error}" + + let predict = + let mutable cnt = 0u + fun (gameState: GameState) -> + let toDot drawHistory = + let file = Path.Join ("dot", $"{cnt}.dot") + gameState.ToDot file drawHistory + cnt <- cnt + 1u + //toDot false + let res = + socket { + do! sendResponse (ReadyForNextStep gameState) + let! msg = webSocket.read () + let res = match msg with - | Step stepParams -> (stepParams.StateId) + | (Text, data, true) -> + let msg = deserializeInputMessage data + match msg with + | Step stepParams -> (stepParams.StateId) + | _ -> failwithf $"Unexpected message: %A{msg}" | _ -> failwithf $"Unexpected message: %A{msg}" - | _ -> failwithf $"Unexpected message: %A{msg}" - return res - } - match Async.RunSynchronously res with - | Choice1Of2 i -> i - | Choice2Of2 error -> failwithf $"Error: %A{error}" - - Oracle(predict,feedback) - - while loop do - let! msg = webSocket.read() - match msg with - | (Text, data, true) -> + return res + } + match Async.RunSynchronously res with + | Choice1Of2 i -> i + | Choice2Of2 error -> failwithf $"Error: %A{error}" + + Oracle (predict, feedback) + + while loop do + let! msg = webSocket.read () + match msg with + | (Text, data, true) -> let message = deserializeInputMessage data match message with | ServerStop -> loop <- false | Start gameMap -> printfn $"Start map {gameMap.MapName}, port {port}" - let stepsToStart= gameMap.StepsToStart + let stepsToStart = gameMap.StepsToStart let stepsToPlay = gameMap.StepsToPlay let aiTrainingOptions = - { - stepsToSwitchToAI = stepsToStart - stepsToPlay = stepsToPlay - defaultSearchStrategy = - match gameMap.DefaultSearcher with - | searcher.BFS -> BFSMode - | searcher.DFS -> DFSMode - | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." - serializeSteps = false - mapName = gameMap.MapName - oracle = Some oracle - } - let options = VSharpOptions(timeout = 15 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.AI, aiAgentTrainingOptions = aiTrainingOptions, stepsLimit = uint(stepsToPlay + stepsToStart), solverTimeout=2) - let explorationResult = explore gameMap options - - Application.reset() - API.Reset() - HashMap.hashMap.Clear() - do! sendResponse (GameOver (explorationResult.ActualCoverage, explorationResult.TestsCount, explorationResult.ErrorsCount)) + { + stepsToSwitchToAI = stepsToStart + stepsToPlay = stepsToPlay + defaultSearchStrategy = + match gameMap.DefaultSearcher with + | searcher.BFS -> BFSMode + | searcher.DFS -> DFSMode + | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." + serializeSteps = false + mapName = gameMap.MapName + oracle = Some oracle + } + let options = + VSharpOptions ( + timeout = 15 * 60, + outputDirectory = outputDirectory, + searchStrategy = SearchStrategy.AI, + aiAgentTrainingOptions = aiTrainingOptions, + stepsLimit = uint (stepsToPlay + stepsToStart), + solverTimeout = 2 + ) + let explorationResult = + explore gameMap options + + Application.reset () + API.Reset () + HashMap.hashMap.Clear () + do! + sendResponse ( + GameOver ( + explorationResult.ActualCoverage, + explorationResult.TestsCount, + explorationResult.ErrorsCount + ) + ) printfn $"Finish map {gameMap.MapName}, port {port}" | x -> failwithf $"Unexpected message: %A{x}" - - | (Close, _, _) -> + + | (Close, _, _) -> let emptyResponse = [||] |> ByteSegment do! webSocket.send Close emptyResponse true loop <- false - | _ -> () + | _ -> () } - + let app port outputDirectory : WebPart = - choose [ - path "/gameServer" >=> handShake (ws port outputDirectory) - ] + choose + [ + path "/gameServer" >=> handShake (ws port outputDirectory) + ] -let generateDataForPretraining outputDirectory datasetBasePath (maps:ResizeArray) stepsToSerialize = +let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArray) stepsToSerialize = for map in maps do - if map.StepsToStart = 0u - then + if map.StepsToStart = 0u then printfn $"Generation for {map.MapName} started." - let map = GameMap(map.StepsToPlay, map.StepsToStart, Path.Combine (datasetBasePath, map.AssemblyFullName), map.DefaultSearcher, map.NameOfObjectToCover, map.MapName) + let map = + GameMap ( + map.StepsToPlay, + map.StepsToStart, + Path.Combine (datasetBasePath, map.AssemblyFullName), + map.DefaultSearcher, + map.NameOfObjectToCover, + map.MapName + ) let aiTrainingOptions = { stepsToSwitchToAI = 0u @@ -197,68 +242,95 @@ let generateDataForPretraining outputDirectory datasetBasePath (maps:ResizeArray mapName = map.MapName oracle = None } - - let options = VSharpOptions(timeout = 5 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.ExecutionTreeContributedCoverage, stepsLimit = stepsToSerialize, solverTimeout=2, aiAgentTrainingOptions = aiTrainingOptions) - let folderForResults = Serializer.getFolderToStoreSerializationResult outputDirectory map.MapName - if Directory.Exists folderForResults - then Directory.Delete(folderForResults, true) - let _ = Directory.CreateDirectory folderForResults - + + let options = + VSharpOptions ( + timeout = 5 * 60, + outputDirectory = outputDirectory, + searchStrategy = SearchStrategy.ExecutionTreeContributedCoverage, + stepsLimit = stepsToSerialize, + solverTimeout = 2, + aiAgentTrainingOptions = aiTrainingOptions + ) + let folderForResults = + Serializer.getFolderToStoreSerializationResult outputDirectory map.MapName + if Directory.Exists folderForResults then + Directory.Delete (folderForResults, true) + let _ = + Directory.CreateDirectory folderForResults + let explorationResult = explore map options - File.WriteAllText(Path.Join(folderForResults, "result"), $"{explorationResult.ActualCoverage} {explorationResult.TestsCount} {explorationResult.StepsCount} {explorationResult.ErrorsCount}") - printfn $"Generation for {map.MapName} finished with coverage {explorationResult.ActualCoverage}, tests {explorationResult.TestsCount}, steps {explorationResult.StepsCount},errors {explorationResult.ErrorsCount}." - Application.reset() - API.Reset() - HashMap.hashMap.Clear() + File.WriteAllText ( + Path.Join (folderForResults, "result"), + $"{explorationResult.ActualCoverage} {explorationResult.TestsCount} {explorationResult.StepsCount} {explorationResult.ErrorsCount}" + ) + printfn + $"Generation for {map.MapName} finished with coverage {explorationResult.ActualCoverage}, tests {explorationResult.TestsCount}, steps {explorationResult.StepsCount},errors {explorationResult.ErrorsCount}." + Application.reset () + API.Reset () + HashMap.hashMap.Clear () [] let main args = - let parser = ArgumentParser.Create(programName = "VSharp.ML.GameServer.Runner.exe") + let parser = + ArgumentParser.Create (programName = "VSharp.ML.GameServer.Runner.exe") let args = parser.Parse args - - let mode = args.GetResult <@Mode@> - + + let mode = args.GetResult <@ Mode @> + let port = - match args.TryGetResult <@Port@> with + match args.TryGetResult <@ Port @> with | Some port -> port | None -> 8100 - - let datasetBasePath = - match args.TryGetResult <@DatasetBasePath@> with + + let datasetBasePath = + match args.TryGetResult <@ DatasetBasePath @> with | Some path -> path | None -> "" - - let datasetDescription = - match args.TryGetResult <@DatasetDescription@> with + + let datasetDescription = + match args.TryGetResult <@ DatasetDescription @> with | Some path -> path | None -> "" let stepsToSerialize = - match args.TryGetResult <@StepsToSerialize@> with + match args.TryGetResult <@ StepsToSerialize @> with | Some steps -> steps | None -> 500u - + + let useGPU = + (args.TryGetResult <@ UseGPU @>).IsSome + + let optimize = + (args.TryGetResult <@ Optimize @>).IsSome + let outputDirectory = - Path.Combine(Directory.GetCurrentDirectory(), string port) - - if Directory.Exists outputDirectory - then Directory.Delete(outputDirectory,true) - let testsDirInfo = Directory.CreateDirectory outputDirectory - printfn $"outputDir: {outputDirectory}" - + Path.Combine (Directory.GetCurrentDirectory (), string port) + + if Directory.Exists outputDirectory then + Directory.Delete (outputDirectory, true) + let testsDirInfo = + Directory.CreateDirectory outputDirectory + printfn $"outputDir: {outputDirectory}" + match mode with | Mode.Server -> - try - startWebServer {defaultConfig with - logger = Targets.create Verbose [||] - bindings = [HttpBinding.createSimple HTTP "127.0.0.1" port]} (app port outputDirectory) - with - | e -> + try + startWebServer + { defaultConfig with + logger = Targets.create Verbose [||] + bindings = + [ + HttpBinding.createSimple HTTP "127.0.0.1" port + ] + } + (app port outputDirectory) + with e -> printfn $"Failed on port {port}" printfn $"{e.Message}" | Mode.Generator -> let maps = loadGameMaps datasetDescription generateDataForPretraining outputDirectory datasetBasePath maps stepsToSerialize | x -> failwithf $"Unexpected mode {x}." - - 0 \ No newline at end of file + + 0 diff --git a/VSharp.Runner/RunnerProgram.cs b/VSharp.Runner/RunnerProgram.cs index a3b841c3c..096c04d18 100644 --- a/VSharp.Runner/RunnerProgram.cs +++ b/VSharp.Runner/RunnerProgram.cs @@ -90,12 +90,12 @@ public static class RunnerProgram { var t = methodArgumentValue.Split('.'); var className = t.Length == 1 ? "" : t[t.Length - 2]; - var methodName = t.Last(); + var methodName = t.Last(); var x = type.GetMethods(Reflection.allBindingFlags); method ??= x .Where(m => type.FullName.Split('.').Last().Contains(className) && m.Name.Contains(methodName)) .MinBy(m => m.Name.Length); - if (method != null) + if (method != null) break; } catch (Exception) @@ -132,7 +132,10 @@ private static void EntryPointHandler( Verbosity verbosity, uint recursionThreshold, ExplorationMode explorationMode, - string pathToModel) + string pathToModel, + bool useGPU, + bool optimize + ) { var assembly = TryLoadAssembly(assemblyPath); var options = @@ -145,7 +148,9 @@ private static void EntryPointHandler( verbosity: verbosity, recursionThreshold: recursionThreshold, explorationMode: explorationMode, - pathToModel: pathToModel); + pathToModel: pathToModel, + useGPU: useGPU, + optimize: optimize); if (assembly == null) return; @@ -239,7 +244,9 @@ private static void AllPublicMethodsHandler( Verbosity verbosity, uint recursionThreshold, ExplorationMode explorationMode, - string pathToModel) + string pathToModel, + bool useGPU, + bool optimize) { var assembly = TryLoadAssembly(assemblyPath); var options = @@ -252,7 +259,9 @@ private static void AllPublicMethodsHandler( verbosity: verbosity, recursionThreshold: recursionThreshold, explorationMode: explorationMode, - pathToModel: pathToModel); + pathToModel: pathToModel, + useGPU: useGPU, + optimize: optimize); if (assembly == null) return; @@ -276,7 +285,9 @@ private static void PublicMethodsOfTypeHandler( Verbosity verbosity, uint recursionThreshold, ExplorationMode explorationMode, - string pathToModel) + string pathToModel, + bool useGPU, + bool optimize) { var assembly = TryLoadAssembly(assemblyPath); if (assembly == null) return; @@ -298,7 +309,9 @@ private static void PublicMethodsOfTypeHandler( verbosity: verbosity, recursionThreshold: recursionThreshold, explorationMode: explorationMode, - pathToModel: pathToModel); + pathToModel: pathToModel, + useGPU: useGPU, + optimize: optimize); Statistics statistics; if (runTests) @@ -321,7 +334,9 @@ private static void SpecificMethodHandler( Verbosity verbosity, uint recursionThreshold, ExplorationMode explorationMode, - string pathToModel) + string pathToModel, + bool useGPU, + bool optimize) { var assembly = TryLoadAssembly(assemblyPath); if (assembly == null) return; @@ -359,7 +374,9 @@ private static void SpecificMethodHandler( verbosity: verbosity, recursionThreshold: recursionThreshold, explorationMode: explorationMode, - pathToModel: pathToModel); + pathToModel: pathToModel, + useGPU: useGPU, + optimize: optimize); Statistics statistics; if (runTests || checkCoverage) @@ -381,7 +398,9 @@ private static void NamespaceHandler( Verbosity verbosity, uint recursionThreshold, ExplorationMode explorationMode, - string pathToModel) + string pathToModel, + bool useGPU, + bool optimize) { var assembly = TryLoadAssembly(assemblyPath); if (assembly == null) return; @@ -403,7 +422,9 @@ private static void NamespaceHandler( verbosity: verbosity, recursionThreshold: recursionThreshold, explorationMode: explorationMode, - pathToModel: pathToModel); + pathToModel: pathToModel, + useGPU: useGPU, + optimize: optimize); Statistics statistics; if (runTests) @@ -427,6 +448,14 @@ public static int Main(string[] args) aliases: new[] { "--model", "-m" }, () => defaultOptions.GetDefaultPathToModel(), "Path to ONNX file with model for AI searcher."); + var useGPUOption = new Option( + aliases: new[] { "--gpu" }, + () => false, + "Enables GPU processing."); + var optimizeOption = new Option( + aliases: new[] { "--optimize" }, + () => false, + "Optimize option."); var solverTimeoutOption = new Option( aliases: new[] { "--solver-timeout", "-st" }, () => -1, @@ -473,6 +502,8 @@ public static int Main(string[] args) rootCommand.AddGlobalOption(recursionThresholdOption); rootCommand.AddGlobalOption(explorationModeOption); rootCommand.AddGlobalOption(pathToModelOption); + rootCommand.AddGlobalOption(useGPUOption); + rootCommand.AddGlobalOption(optimizeOption); var entryPointCommand = new Command("--entry-point", "Generate test coverage from the entry point of assembly (assembly must contain Main method)"); @@ -499,7 +530,9 @@ public static int Main(string[] args) parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), parseResult.GetValueForOption(explorationModeOption), - parseResult.GetValueForOption(pathToModelOption) + parseResult.GetValueForOption(pathToModelOption), + parseResult.GetValueForOption(useGPUOption), + parseResult.GetValueForOption(optimizeOption) ); }); @@ -561,7 +594,9 @@ public static int Main(string[] args) parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), parseResult.GetValueForOption(explorationModeOption), - parseResult.GetValueForOption(pathToModelOption) + parseResult.GetValueForOption(pathToModelOption), + parseResult.GetValueForOption(useGPUOption), + parseResult.GetValueForOption(optimizeOption) ); }); @@ -589,7 +624,9 @@ public static int Main(string[] args) parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), parseResult.GetValueForOption(explorationModeOption), - parseResult.GetValueForOption(pathToModelOption) + parseResult.GetValueForOption(pathToModelOption), + parseResult.GetValueForOption(useGPUOption), + parseResult.GetValueForOption(optimizeOption) ); }); @@ -620,7 +657,9 @@ public static int Main(string[] args) parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), parseResult.GetValueForOption(explorationModeOption), - pathToModel + pathToModel, + parseResult.GetValueForOption(useGPUOption), + parseResult.GetValueForOption(optimizeOption) ); }); @@ -649,7 +688,9 @@ public static int Main(string[] args) parseResult.GetValueForOption(verbosityOption), parseResult.GetValueForOption(recursionThresholdOption), parseResult.GetValueForOption(explorationModeOption), - pathToModel + pathToModel, + parseResult.GetValueForOption(useGPUOption), + parseResult.GetValueForOption(optimizeOption) ); }); diff --git a/VSharp.Test/Benchmarks/Benchmarks.cs b/VSharp.Test/Benchmarks/Benchmarks.cs index 677e8ed8e..12e022ffd 100644 --- a/VSharp.Test/Benchmarks/Benchmarks.cs +++ b/VSharp.Test/Benchmarks/Benchmarks.cs @@ -35,7 +35,7 @@ private static bool TryBuildGeneratedTests() return process.IsSuccess(); } - private class Reporter: IReporter + private class Reporter : IReporter { private readonly UnitTests _unitTests; @@ -94,7 +94,9 @@ public static BenchmarkResult Run( randomSeed: randomSeed, stepsLimit: stepsLimit, aiAgentTrainingOptions: null, - pathToModel: null + pathToModel: null, + useGPU: null, + optimize: null ); var fuzzerOptions = new FuzzerOptions( @@ -113,7 +115,7 @@ public static BenchmarkResult Run( using var explorer = new Explorer.Explorer(explorationOptions, new Reporter(unitTests)); explorer.StartExploration( - new[] {exploredMethodInfo}, + new[] { exploredMethodInfo }, global::System.Array.Empty>() ); diff --git a/VSharp.Test/IntegrationTests.cs b/VSharp.Test/IntegrationTests.cs index 13ce410fc..00ed34653 100644 --- a/VSharp.Test/IntegrationTests.cs +++ b/VSharp.Test/IntegrationTests.cs @@ -75,7 +75,7 @@ public IEnumerable BuildFrom(ITypeInfo typeInfo) } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method, AllowMultiple = true)] - public class IgnoreFuzzerAttribute: Attribute + public class IgnoreFuzzerAttribute : Attribute { public string Reason { get; init; } @@ -133,6 +133,9 @@ static TestSvmAttribute() private readonly int _randomSeed; private readonly uint _stepsLimit; private readonly string _pathToModel; + private readonly bool _useGPU; + private readonly bool _optimize; + public TestSvmAttribute( int expectedCoverage = -1, @@ -150,7 +153,9 @@ public TestSvmAttribute( ExplorationMode explorationMode = ExplorationMode.Sili, int randomSeed = 0, uint stepsLimit = 0, - string pathToModel = "models/model.onnx") + string pathToModel = "models/model.onnx", + bool useGPU = false, + bool optimize = false) { if (expectedCoverage < 0) _expectedCoverage = null; @@ -172,6 +177,8 @@ public TestSvmAttribute( _explorationMode = explorationMode; _randomSeed = randomSeed; _pathToModel = pathToModel; + _useGPU = useGPU; + _optimize = optimize; _stepsLimit = stepsLimit; } @@ -194,6 +201,8 @@ public TestCommand Wrap(TestCommand command) _randomSeed, _stepsLimit, _pathToModel, + _useGPU, + _optimize, _hasExternMocking ); } @@ -219,8 +228,10 @@ private class TestSvmCommand : DelegatingTestCommand private readonly int _randomSeed; private readonly uint _stepsLimit; private readonly string _pathToModel; + private readonly bool _useGPU; + private readonly bool _optimize; - private class Reporter: IReporter + private class Reporter : IReporter { private readonly UnitTests _unitTests; @@ -231,7 +242,7 @@ public Reporter(UnitTests unitTests) public void ReportFinished(UnitTest unitTest) => _unitTests.GenerateTest(unitTest); public void ReportException(UnitTest unitTest) => _unitTests.GenerateError(unitTest); - public void ReportIIE(InsufficientInformationException iie) {} + public void ReportIIE(InsufficientInformationException iie) { } public void ReportInternalFail(Method method, Exception exn) => ExceptionDispatchInfo.Capture(exn).Throw(); public void ReportCrash(Exception exn) => ExceptionDispatchInfo.Capture(exn).Throw(); } @@ -253,6 +264,8 @@ public TestSvmCommand( int randomSeed, uint stepsLimit, string pathToModel, + bool useGPU, + bool optimize, bool hasExternMocking) : base(innerCommand) { _baseCoverageZone = coverageZone; @@ -307,6 +320,8 @@ public TestSvmCommand( _randomSeed = randomSeed; _stepsLimit = stepsLimit; _pathToModel = pathToModel; + _useGPU = useGPU; + _optimize = optimize; } private TestResult IgnoreTest(TestExecutionContext context) @@ -366,7 +381,7 @@ DirectoryInfo testsDir bool success; var resultMessage = string.Empty; uint? actualCoverage; - if (_expectedCoverage is {} expectedCoverage) + if (_expectedCoverage is { } expectedCoverage) { TestContext.Out.WriteLine("Starting coverage tool..."); success = @@ -438,7 +453,7 @@ private TestResult Explore(TestExecutionContext context) }; var originMethodInfo = innerCommand.Test.Method.MethodInfo; - var exploredMethodInfo = (MethodInfo) AssemblyManager.NormalizeMethod(originMethodInfo); + var exploredMethodInfo = (MethodInfo)AssemblyManager.NormalizeMethod(originMethodInfo); var stats = new TestStatistics( exploredMethodInfo, _releaseBranches, @@ -463,8 +478,10 @@ private TestResult Explore(TestExecutionContext context) stopOnCoverageAchieved: _expectedCoverage ?? -1, randomSeed: _randomSeed, stepsLimit: _stepsLimit, - aiAgentTrainingOptions:null, - pathToModel: _pathToModel + aiAgentTrainingOptions: null, + pathToModel: _pathToModel, + useGPU: _useGPU, + optimize: _optimize ); var fuzzerOptions = new FuzzerOptions( @@ -489,7 +506,7 @@ private TestResult Explore(TestExecutionContext context) using var explorer = new Explorer.Explorer(explorationOptions, new Reporter(unitTests)); Application.reset(); explorer.StartExploration( - new [] { exploredMethodInfo }, + new[] { exploredMethodInfo }, global::System.Array.Empty>() ); From 02cf58dbc9abebeba6899e3b345353632f9b54c2 Mon Sep 17 00:00:00 2001 From: Anya Chistyakova <57658770+Anya497@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:07:05 +0300 Subject: [PATCH 05/12] CI (#92) * Run VSharp building on pull request and push. * Refresh versions. * Setup dotnet7. * Build in Release mode. --- .github/workflows/build_vsharp.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_vsharp.yml b/.github/workflows/build_vsharp.yml index 3ad121329..edc53591f 100644 --- a/.github/workflows/build_vsharp.yml +++ b/.github/workflows/build_vsharp.yml @@ -1,31 +1,39 @@ name: 'Build VSharp' on: - workflow_call + [workflow_call, pull_request, push] jobs: build: runs-on: ubuntu-22.04 steps: + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '7.0.x' + - name: Checkout VSharp - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: false - - uses: actions/cache@v3 + + - uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.fsproj') }} restore-keys: | ${{ runner.os }}-nuget- + - name: Build VSharp run: - dotnet build -c DebugTailRec - - uses: actions/upload-artifact@v3 + dotnet build -c Release + + - uses: actions/upload-artifact@v4 with: name: runner path: ./VSharp.Runner/bin/DebugTailRec/net7.0 - - uses: actions/upload-artifact@v3 + + - uses: actions/upload-artifact@v4 with: name: test_runner path: ./VSharp.TestRunner/bin/DebugTailRec/net7.0 From 7d9becb0362f464a56ab5f1a49aa43f022598061 Mon Sep 17 00:00:00 2001 From: Anya Chistyakova <57658770+Anya497@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:24:12 +0300 Subject: [PATCH 06/12] Path condition (#91) * Run VSharp building on pull request and push. * Refresh versions. * Setup dotnet7. * Build in Release mode. * Add path condition vertex type. Support path condition processing. Refactor. * Reset dictionary with path condition vertices. Refactor. * Update path condition. Refactor. * Pass path condition instead its size. Make PC not internal to use its methods. --- VSharp.Explorer/AISearcher.fs | 297 +++++++------ VSharp.IL/CFG.fs | 3 +- VSharp.IL/Serializer.fs | 630 +++++++++++++++++++--------- VSharp.ML.GameServer.Runner/Main.fs | 166 ++++---- VSharp.ML.GameServer/Messages.fs | 422 ++++++++++++------- VSharp.SILI.Core/PathCondition.fs | 2 +- VSharp.SILI/CILState.fs | 2 +- 7 files changed, 940 insertions(+), 582 deletions(-) diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs index e1a31ab0e..b02f9a423 100644 --- a/VSharp.Explorer/AISearcher.fs +++ b/VSharp.Explorer/AISearcher.fs @@ -17,49 +17,51 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option 0u | Some options -> options.stepsToPlay - let mutable lastCollectedStatistics = - Statistics () + let mutable lastCollectedStatistics = Statistics() let mutable defaultSearcherSteps = 0u - let mutable (gameState: Option) = - None - let mutable useDefaultSearcher = - stepsToSwitchToAI > 0u + let mutable (gameState: Option) = None + let mutable useDefaultSearcher = stepsToSwitchToAI > 0u let mutable afterFirstAIPeek = false - let mutable incorrectPredictedStateId = - false + let mutable incorrectPredictedStateId = false + let defaultSearcher = match aiAgentTrainingOptions with - | None -> BFSSearcher () :> IForwardSearcher + | None -> BFSSearcher() :> IForwardSearcher | Some options -> match options.defaultSearchStrategy with - | BFSMode -> BFSSearcher () :> IForwardSearcher - | DFSMode -> DFSSearcher () :> IForwardSearcher + | BFSMode -> BFSSearcher() :> IForwardSearcher + | DFSMode -> DFSSearcher() :> IForwardSearcher | x -> failwithf $"Unexpected default searcher {x}. DFS and BFS supported for now." + let mutable stepsPlayed = 0u + let isInAIMode () = (not useDefaultSearcher) && afterFirstAIPeek - let q = ResizeArray<_> () - let availableStates = HashSet<_> () + + let q = ResizeArray<_>() + let availableStates = HashSet<_>() + let updateGameState (delta: GameState) = match gameState with | None -> gameState <- Some delta | Some s -> - let updatedBasicBlocks = - delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet - let updatedStates = - delta.States |> Array.map (fun s -> s.Id) |> HashSet + let updatedBasicBlocks = delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet + let updatedStates = delta.States |> Array.map (fun s -> s.Id) |> HashSet + let vertices = s.GraphVertices |> Array.filter (fun v -> updatedBasicBlocks.Contains v.Id |> not) |> ResizeArray<_> + vertices.AddRange delta.GraphVertices + let edges = s.Map |> Array.filter (fun e -> updatedBasicBlocks.Contains e.VertexFrom |> not) |> ResizeArray<_> + edges.AddRange delta.Map - let activeStates = - vertices |> Seq.collect (fun v -> v.States) |> HashSet + let activeStates = vertices |> Seq.collect (fun v -> v.States) |> HashSet let states = let part1 = @@ -69,12 +71,12 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option Array.map (fun s -> - State ( + State( s.Id, s.Position, - s.PathConditionSize, + s.PathCondition, s.VisitedAgainVertices, s.VisitedNotCoveredVerticesInZone, s.VisitedNotCoveredVerticesOutOfZone, @@ -82,69 +84,80 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option Array.filter activeStates.Contains - ) - ) + )) + + let pathConditionVertices = + ResizeArray s.PathConditionVertices + + pathConditionVertices.AddRange delta.PathConditionVertices - gameState <- Some <| GameState (vertices.ToArray (), states, edges.ToArray ()) + gameState <- + Some + <| GameState(vertices.ToArray(), states, pathConditionVertices.ToArray(), edges.ToArray()) let init states = q.AddRange states defaultSearcher.Init q states |> Seq.iter (availableStates.Add >> ignore) + let reset () = - defaultSearcher.Reset () + defaultSearcher.Reset() defaultSearcherSteps <- 0u - lastCollectedStatistics <- Statistics () + lastCollectedStatistics <- Statistics() gameState <- None afterFirstAIPeek <- false incorrectPredictedStateId <- false useDefaultSearcher <- stepsToSwitchToAI > 0u - q.Clear () - availableStates.Clear () - let update (parent, newSates) = + q.Clear() + availableStates.Clear() + + let update (parent, newStates) = if useDefaultSearcher then - defaultSearcher.Update (parent, newSates) - newSates |> Seq.iter (availableStates.Add >> ignore) + defaultSearcher.Update(parent, newStates) + + newStates |> Seq.iter (availableStates.Add >> ignore) + let remove state = if useDefaultSearcher then defaultSearcher.Remove state + let removed = availableStates.Remove state assert removed + for bb in state._history do bb.Key.AssociatedStates.Remove state |> ignore - let inTrainMode = - aiAgentTrainingOptions.IsSome + let inTrainMode = aiAgentTrainingOptions.IsSome let pick selector = if useDefaultSearcher then defaultSearcherSteps <- defaultSearcherSteps + 1u + if Seq.length availableStates > 0 then - let gameStateDelta = - collectGameStateDelta () + let gameStateDelta = collectGameStateDelta () updateGameState gameStateDelta - let statistics = - computeStatistics gameState.Value - Application.applicationGraphDelta.Clear () + let statistics = computeStatistics gameState.Value + Application.applicationGraphDelta.Clear() lastCollectedStatistics <- statistics useDefaultSearcher <- defaultSearcherSteps < stepsToSwitchToAI - defaultSearcher.Pick () + + defaultSearcher.Pick() elif Seq.length availableStates = 0 then None elif Seq.length availableStates = 1 then - Some (Seq.head availableStates) + Some(Seq.head availableStates) else - let gameStateDelta = - collectGameStateDelta () + let gameStateDelta = collectGameStateDelta () updateGameState gameStateDelta - let statistics = - computeStatistics gameState.Value + let statistics = computeStatistics gameState.Value + if isInAIMode () then - let reward = - computeReward lastCollectedStatistics statistics - oracle.Feedback (Feedback.MoveReward reward) - Application.applicationGraphDelta.Clear () + let reward = computeReward lastCollectedStatistics statistics + oracle.Feedback(Feedback.MoveReward reward) + + Application.applicationGraphDelta.Clear() + if inTrainMode && stepsToPlay = stepsPlayed then None else @@ -153,17 +166,18 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option Seq.tryFind (fun s -> s.internalId = stateId) + let state = availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) lastCollectedStatistics <- statistics stepsPlayed <- stepsPlayed + 1u + match state with | Some state -> Some state | None -> incorrectPredictedStateId <- true - oracle.Feedback (Feedback.IncorrectPredictedStateId stateId) + oracle.Feedback(Feedback.IncorrectPredictedStateId stateId) None new(pathToONNX: string, useGPU: bool, optimize: bool) = @@ -174,9 +188,9 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option, int> () - let verticesIds = - Dictionary, int> () + let stateIds = Dictionary, int>() + let verticesIds = Dictionary, int>() let networkInput = - let res = Dictionary<_, _> () + let res = Dictionary<_, _>() + let gameVertices = - let shape = - [| - int64 gameState.GraphVertices.Length - numOfVertexAttributes - |] + let shape = [| int64 gameState.GraphVertices.Length; numOfVertexAttributes |] + let attributes = Array.zeroCreate (gameState.GraphVertices.Length * numOfVertexAttributes) + for i in 0 .. gameState.GraphVertices.Length - 1 do let v = gameState.GraphVertices.[i] - verticesIds.Add (v.Id, i) + verticesIds.Add(v.Id, i) let j = i * numOfVertexAttributes attributes.[j] <- float32 <| if v.InCoverageZone then 1u else 0u attributes.[j + 1] <- float32 <| v.BasicBlockSize @@ -216,111 +226,97 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option Array.iteri (fun i e -> index[i] <- int64 verticesIds[e.VertexFrom] index[gameState.Map.Length + i] <- int64 verticesIds[e.VertexTo] - attributes[i] <- int64 e.Label.Token - ) + attributes[i] <- int64 e.Label.Token) - OrtValue.CreateTensorValueFromMemory (index, shapeOfIndex), - OrtValue.CreateTensorValueFromMemory (attributes, shapeOfAttributes) + OrtValue.CreateTensorValueFromMemory(index, shapeOfIndex), + OrtValue.CreateTensorValueFromMemory(attributes, shapeOfAttributes) let historyEdgesIndex_vertexToState, historyEdgesAttributes, parentOfEdges = - let shapeOfParentOf = - [| 2L ; numOfParentOfEdges |] - let parentOf = - Array.zeroCreate (2 * numOfParentOfEdges) - let shapeOfHistory = - [| 2L ; numOfHistoryEdges |] - let historyIndex_vertexToState = - Array.zeroCreate (2 * numOfHistoryEdges) + let shapeOfParentOf = [| 2L; numOfParentOfEdges |] + let parentOf = Array.zeroCreate (2 * numOfParentOfEdges) + let shapeOfHistory = [| 2L; numOfHistoryEdges |] + let historyIndex_vertexToState = Array.zeroCreate (2 * numOfHistoryEdges) + let shapeOfHistoryAttributes = - [| - int64 numOfHistoryEdges - int64 numOfHistoryEdgeAttributes - |] - let historyAttributes = - Array.zeroCreate (2 * numOfHistoryEdges) + [| int64 numOfHistoryEdges; int64 numOfHistoryEdgeAttributes |] + + let historyAttributes = Array.zeroCreate (2 * numOfHistoryEdges) let mutable firstFreePositionInParentsOf = 0 - let mutable firstFreePositionInHistoryIndex = - 0 - let mutable firstFreePositionInHistoryAttributes = - 0 + let mutable firstFreePositionInHistoryIndex = 0 + let mutable firstFreePositionInHistoryAttributes = 0 + gameState.States |> Array.iter (fun state -> state.Children |> Array.iteri (fun i children -> let j = firstFreePositionInParentsOf + i parentOf[j] <- int64 stateIds[state.Id] - parentOf[numOfParentOfEdges + j] <- int64 stateIds[children] - ) + parentOf[numOfParentOfEdges + j] <- int64 stateIds[children]) + firstFreePositionInParentsOf <- firstFreePositionInParentsOf + state.Children.Length + state.History |> Array.iteri (fun i historyElem -> let j = firstFreePositionInHistoryIndex + i historyIndex_vertexToState[j] <- int64 verticesIds[historyElem.GraphVertexId] historyIndex_vertexToState[numOfHistoryEdges + j] <- int64 stateIds[state.Id] - let j = - firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * i + let j = firstFreePositionInHistoryAttributes + numOfHistoryEdgeAttributes * i historyAttributes[j] <- int64 historyElem.NumOfVisits - historyAttributes[j + 1] <- int64 historyElem.StepWhenVisitedLastTime - ) + historyAttributes[j + 1] <- int64 historyElem.StepWhenVisitedLastTime) + firstFreePositionInHistoryIndex <- firstFreePositionInHistoryIndex + state.History.Length + firstFreePositionInHistoryAttributes <- firstFreePositionInHistoryAttributes - + numOfHistoryEdgeAttributes * state.History.Length - ) + + numOfHistoryEdgeAttributes * state.History.Length) - OrtValue.CreateTensorValueFromMemory (historyIndex_vertexToState, shapeOfHistory), - OrtValue.CreateTensorValueFromMemory (historyAttributes, shapeOfHistoryAttributes), - OrtValue.CreateTensorValueFromMemory (parentOf, shapeOfParentOf) + OrtValue.CreateTensorValueFromMemory(historyIndex_vertexToState, shapeOfHistory), + OrtValue.CreateTensorValueFromMemory(historyAttributes, shapeOfHistoryAttributes), + OrtValue.CreateTensorValueFromMemory(parentOf, shapeOfParentOf) let statePosition_stateToVertex, statePosition_vertexToState = - let data_stateToVertex = - Array.zeroCreate (2 * gameState.States.Length) - let data_vertexToState = - Array.zeroCreate (2 * gameState.States.Length) - let shape = - [| 2L ; gameState.States.Length |] + let data_stateToVertex = Array.zeroCreate (2 * gameState.States.Length) + let data_vertexToState = Array.zeroCreate (2 * gameState.States.Length) + let shape = [| 2L; gameState.States.Length |] let mutable firstFreePosition = 0 + gameState.GraphVertices |> Array.iter (fun v -> v.States @@ -332,46 +328,43 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option().ToArray () + let output = session.Run(runOptions, networkInput, session.OutputNames) + let weighedStates = output[0].GetTensorDataAsSpan().ToArray() - let id = - weighedStates |> Array.mapi (fun i v -> i, v) |> Array.maxBy snd |> fst + let id = weighedStates |> Array.mapi (fun i v -> i, v) |> Array.maxBy snd |> fst stateIds |> Seq.find (fun kvp -> kvp.Value = id) |> (fun x -> x.Key) - Oracle (predict, feedback) + Oracle(predict, feedback) - AISearcher (createOracle pathToONNX, None) + AISearcher(createOracle pathToONNX, None) interface IForwardSearcher with override x.Init states = init states - override x.Pick () = pick (always true) + override x.Pick() = pick (always true) override x.Pick selector = pick selector - override x.Update (parent, newStates) = update (parent, newStates) - override x.States () = availableStates - override x.Reset () = reset () + override x.Update(parent, newStates) = update (parent, newStates) + override x.States() = availableStates + override x.Reset() = reset () override x.Remove cilState = remove cilState override x.StatesCount = availableStates.Count diff --git a/VSharp.IL/CFG.fs b/VSharp.IL/CFG.fs index b5b465670..6de1eb186 100644 --- a/VSharp.IL/CFG.fs +++ b/VSharp.IL/CFG.fs @@ -8,6 +8,7 @@ open System.Reflection open System.Collections.Generic open Microsoft.FSharp.Collections open VSharp +open VSharp.Core type ICallGraphNode = inherit IGraphNode @@ -519,7 +520,7 @@ and IGraphTrackableState = abstract member CodeLocation: codeLocation abstract member CallStack: list abstract member Id: uint - abstract member PathConditionSize: uint + abstract member PathCondition: pathCondition abstract member VisitedNotCoveredVerticesInZone: uint with get abstract member VisitedNotCoveredVerticesOutOfZone: uint with get abstract member VisitedAgainVertices: uint with get diff --git a/VSharp.IL/Serializer.fs b/VSharp.IL/Serializer.fs index dc25cfd75..f42e9d46f 100644 --- a/VSharp.IL/Serializer.fs +++ b/VSharp.IL/Serializer.fs @@ -10,6 +10,9 @@ open VSharp open VSharp.GraphUtils open VSharp.ML.GameServer.Messages open FSharpx.Collections +open type VSharp.Core.termNode +open VSharp.Core + [] @@ -21,20 +24,28 @@ type Statistics = val VisitedInstructionsInZone: uint val TouchedVerticesInZone: uint val TouchedVerticesOutOfZone: uint - val TotalVisibleVerticesInZone: uint - new (coveredVerticesInZone, coveredVerticesOutOfZone, visitedVerticesInZone, visitedVerticesOutOfZone - , visitedInstructionsInZone, touchedVerticesInZone, touchedVerticesOutOfZone, totalVisibleVerticesInZone) = - { - CoveredVerticesInZone = coveredVerticesInZone - CoveredVerticesOutOfZone = coveredVerticesOutOfZone - VisitedVerticesInZone = visitedVerticesInZone - VisitedVerticesOutOfZone = visitedVerticesOutOfZone - VisitedInstructionsInZone = visitedInstructionsInZone - TouchedVerticesInZone = touchedVerticesInZone - TouchedVerticesOutOfZone = touchedVerticesOutOfZone - TotalVisibleVerticesInZone = totalVisibleVerticesInZone - } - + val TotalVisibleVerticesInZone: uint + + new + ( + coveredVerticesInZone, + coveredVerticesOutOfZone, + visitedVerticesInZone, + visitedVerticesOutOfZone, + visitedInstructionsInZone, + touchedVerticesInZone, + touchedVerticesOutOfZone, + totalVisibleVerticesInZone + ) = + { CoveredVerticesInZone = coveredVerticesInZone + CoveredVerticesOutOfZone = coveredVerticesOutOfZone + VisitedVerticesInZone = visitedVerticesInZone + VisitedVerticesOutOfZone = visitedVerticesOutOfZone + VisitedInstructionsInZone = visitedInstructionsInZone + TouchedVerticesInZone = touchedVerticesInZone + TouchedVerticesOutOfZone = touchedVerticesOutOfZone + TotalVisibleVerticesInZone = totalVisibleVerticesInZone } + [] type StateMetrics = val StateId: uint @@ -45,6 +56,7 @@ type StateMetrics = val DistanceToNearestUncovered: uint val DistanceToNearestNotVisited: uint val DistanceToNearestReturn: uint + new ( stateId, @@ -56,16 +68,14 @@ type StateMetrics = distanceToNearestNotVisited, distanceTuNearestReturn ) = - { - StateId = stateId - NextInstructionIsUncoveredInZone = nextInstructionIsUncoveredInZone - ChildNumber = childNumber - VisitedVerticesInZone = visitedVerticesInZone - HistoryLength = historyLength - DistanceToNearestUncovered = distanceToNearestUncovered - DistanceToNearestNotVisited = distanceToNearestNotVisited - DistanceToNearestReturn = distanceTuNearestReturn - } + { StateId = stateId + NextInstructionIsUncoveredInZone = nextInstructionIsUncoveredInZone + ChildNumber = childNumber + VisitedVerticesInZone = visitedVerticesInZone + HistoryLength = historyLength + DistanceToNearestUncovered = distanceToNearestUncovered + DistanceToNearestNotVisited = distanceToNearestNotVisited + DistanceToNearestReturn = distanceTuNearestReturn } [] type StateInfoToDump = @@ -78,6 +88,7 @@ type StateInfoToDump = val DistanceToUncoveredNormalized: float val DistanceToNotVisitedNormalized: float val ExpectedWeight: float + new ( stateId, @@ -87,98 +98,147 @@ type StateInfoToDump = productivity, distanceToReturn, distanceToUncovered, - distanceToNotVisited + distanceToNotVisited ) = - { - StateId = stateId - NextInstructionIsUncoveredInZone = nextInstructionIsUncoveredInZone - ChildNumberNormalized = childNumber - VisitedVerticesInZoneNormalized = visitedVerticesInZone - Productivity = productivity - DistanceToReturnNormalized = distanceToReturn - DistanceToUncoveredNormalized = distanceToUncovered - DistanceToNotVisitedNormalized = distanceToNotVisited - ExpectedWeight = nextInstructionIsUncoveredInZone + childNumber + visitedVerticesInZone + distanceToReturn + distanceToUncovered + distanceToNotVisited + productivity - } + { StateId = stateId + NextInstructionIsUncoveredInZone = nextInstructionIsUncoveredInZone + ChildNumberNormalized = childNumber + VisitedVerticesInZoneNormalized = visitedVerticesInZone + Productivity = productivity + DistanceToReturnNormalized = distanceToReturn + DistanceToUncoveredNormalized = distanceToUncovered + DistanceToNotVisitedNormalized = distanceToNotVisited + ExpectedWeight = + nextInstructionIsUncoveredInZone + + childNumber + + visitedVerticesInZone + + distanceToReturn + + distanceToUncovered + + distanceToNotVisited + + productivity } let mutable firstFreeEpisodeNumber = 0 -let calculateStateMetrics interproceduralGraphDistanceFrom (state:IGraphTrackableState) = +let calculateStateMetrics interproceduralGraphDistanceFrom (state: IGraphTrackableState) = let currentBasicBlock = state.CodeLocation.BasicBlock - let distances = + + let distances = let assembly = currentBasicBlock.Method.Module.Assembly - let callGraphDist = Dict.getValueOrUpdate interproceduralGraphDistanceFrom assembly (fun () -> Dictionary<_, _>()) - Dict.getValueOrUpdate callGraphDist (currentBasicBlock :> IInterproceduralCfgNode) (fun () -> - let dist = incrementalSourcedShortestDistanceBfs (currentBasicBlock :> IInterproceduralCfgNode) callGraphDist - let distFromNode = Dictionary() - for i in dist do - if i.Value <> infinity then - distFromNode.Add(i.Key, i.Value) - distFromNode) - - let childCountStore = Dictionary<_,HashSet<_>>() - let rec childCount (state:IGraphTrackableState) = - if childCountStore.ContainsKey state - then childCountStore[state] - else - let cnt = Array.fold (fun (cnt:HashSet<_>) n -> cnt.UnionWith (childCount n); cnt) (HashSet<_>(state.Children)) state.Children - childCountStore.Add (state,cnt) - cnt - let childNumber = uint (childCount state).Count - let visitedVerticesInZone = state.History |> Seq.fold (fun cnt kvp -> if kvp.Key.IsGoal && not kvp.Key.IsCovered then cnt + 1u else cnt) 0u - let nextInstructionIsUncoveredInZone = + + let callGraphDist = + Dict.getValueOrUpdate interproceduralGraphDistanceFrom assembly (fun () -> Dictionary<_, _>()) + + Dict.getValueOrUpdate callGraphDist (currentBasicBlock :> IInterproceduralCfgNode) (fun () -> + let dist = + incrementalSourcedShortestDistanceBfs (currentBasicBlock :> IInterproceduralCfgNode) callGraphDist + + let distFromNode = Dictionary() + + for i in dist do + if i.Value <> infinity then + distFromNode.Add(i.Key, i.Value) + + distFromNode) + + let childCountStore = Dictionary<_, HashSet<_>>() + + let rec childCount (state: IGraphTrackableState) = + if childCountStore.ContainsKey state then + childCountStore[state] + else + let cnt = + Array.fold + (fun (cnt: HashSet<_>) n -> + cnt.UnionWith(childCount n) + cnt) + (HashSet<_>(state.Children)) + state.Children + + childCountStore.Add(state, cnt) + cnt + + let childNumber = uint (childCount state).Count + + let visitedVerticesInZone = + state.History + |> Seq.fold + (fun cnt kvp -> + if kvp.Key.IsGoal && not kvp.Key.IsCovered then + cnt + 1u + else + cnt) + 0u + + let nextInstructionIsUncoveredInZone = let notTouchedFollowingBlocs, notVisitedFollowingBlocs, notCoveredFollowingBlocs = let mutable notCoveredBasicBlocksInZone = 0 let mutable notVisitedBasicBlocksInZone = 0 let mutable notTouchedBasicBlocksInZone = 0 let basicBlocks = HashSet<_>() - currentBasicBlock.OutgoingEdges.Values - |> Seq.iter basicBlocks.UnionWith + currentBasicBlock.OutgoingEdges.Values |> Seq.iter basicBlocks.UnionWith + basicBlocks - |> Seq.iter (fun basicBlock -> if basicBlock.IsGoal - then if not basicBlock.IsTouched - then notTouchedBasicBlocksInZone <- notTouchedBasicBlocksInZone + 1 - elif not basicBlock.IsVisited - then notVisitedBasicBlocksInZone <- notVisitedBasicBlocksInZone + 1 - elif not basicBlock.IsCovered - then notCoveredBasicBlocksInZone <- notCoveredBasicBlocksInZone + 1) + |> Seq.iter (fun basicBlock -> + if basicBlock.IsGoal then + if not basicBlock.IsTouched then + notTouchedBasicBlocksInZone <- notTouchedBasicBlocksInZone + 1 + elif not basicBlock.IsVisited then + notVisitedBasicBlocksInZone <- notVisitedBasicBlocksInZone + 1 + elif not basicBlock.IsCovered then + notCoveredBasicBlocksInZone <- notCoveredBasicBlocksInZone + 1) + notTouchedBasicBlocksInZone, notVisitedBasicBlocksInZone, notCoveredBasicBlocksInZone - if state.CodeLocation.offset <> currentBasicBlock.FinalOffset && currentBasicBlock.IsGoal - then if not currentBasicBlock.IsVisited - then 1.0 - elif not currentBasicBlock.IsCovered - then 0.5 - else 0.0 - elif state.CodeLocation.offset = currentBasicBlock.FinalOffset - then if notTouchedFollowingBlocs > 0 - then 1.0 - elif notVisitedFollowingBlocs > 0 - then 0.5 - elif notCoveredFollowingBlocs > 0 - then 0.3 - else 0.0 - else 0.0 - - let historyLength = state.History |> Seq.fold (fun cnt kvp -> cnt + kvp.Value.NumOfVisits) 0u - + + if + state.CodeLocation.offset <> currentBasicBlock.FinalOffset + && currentBasicBlock.IsGoal + then + if not currentBasicBlock.IsVisited then 1.0 + elif not currentBasicBlock.IsCovered then 0.5 + else 0.0 + elif state.CodeLocation.offset = currentBasicBlock.FinalOffset then + if notTouchedFollowingBlocs > 0 then 1.0 + elif notVisitedFollowingBlocs > 0 then 0.5 + elif notCoveredFollowingBlocs > 0 then 0.3 + else 0.0 + else + 0.0 + + let historyLength = + state.History |> Seq.fold (fun cnt kvp -> cnt + kvp.Value.NumOfVisits) 0u + let getMinBy cond = let s = distances |> Seq.filter cond - if Seq.isEmpty s - then UInt32.MaxValue - else s |> Seq.minBy (fun x -> x.Value) |> fun x -> x.Value - - let distanceToNearestUncovered = getMinBy (fun kvp -> kvp.Key.IsGoal && not kvp.Key.IsCovered) - let distanceToNearestNotVisited = getMinBy (fun kvp -> kvp.Key.IsGoal && not kvp.Key.IsVisited) + + if Seq.isEmpty s then + UInt32.MaxValue + else + s |> Seq.minBy (fun x -> x.Value) |> (fun x -> x.Value) + + let distanceToNearestUncovered = + getMinBy (fun kvp -> kvp.Key.IsGoal && not kvp.Key.IsCovered) + + let distanceToNearestNotVisited = + getMinBy (fun kvp -> kvp.Key.IsGoal && not kvp.Key.IsVisited) + let distanceToNearestReturn = getMinBy (fun kvp -> kvp.Key.IsGoal && kvp.Key.IsSink) - - StateMetrics(state.Id, nextInstructionIsUncoveredInZone, childNumber, visitedVerticesInZone, historyLength - , distanceToNearestUncovered, distanceToNearestNotVisited, distanceToNearestReturn) - -let getFolderToStoreSerializationResult (prefix:string) suffix = + + StateMetrics( + state.Id, + nextInstructionIsUncoveredInZone, + childNumber, + visitedVerticesInZone, + historyLength, + distanceToNearestUncovered, + distanceToNearestNotVisited, + distanceToNearestReturn + ) + +let getFolderToStoreSerializationResult (prefix: string) suffix = let folderName = Path.Combine(Path.Combine(prefix, "SerializedEpisodes"), suffix) folderName -let computeStatistics (gameState:GameState) = +let computeStatistics (gameState: GameState) = let mutable coveredVerticesInZone = 0u let mutable coveredVerticesOutOfZone = 0u let mutable visitedVerticesInZone = 0u @@ -187,164 +247,342 @@ let computeStatistics (gameState:GameState) = let mutable touchedVerticesInZone = 0u let mutable touchedVerticesOutOfZone = 0u let mutable totalVisibleVerticesInZone = 0u + for v in gameState.GraphVertices do - if v.CoveredByTest && v.InCoverageZone - then coveredVerticesInZone <- coveredVerticesInZone + 1u - if v.CoveredByTest && not v.InCoverageZone - then coveredVerticesOutOfZone <- coveredVerticesOutOfZone + 1u - - if v.VisitedByState && v.InCoverageZone - then visitedVerticesInZone <- visitedVerticesInZone + 1u - if v.VisitedByState && not v.InCoverageZone - then visitedVerticesOutOfZone <- visitedVerticesOutOfZone + 1u - - if v.InCoverageZone - then + if v.CoveredByTest && v.InCoverageZone then + coveredVerticesInZone <- coveredVerticesInZone + 1u + + if v.CoveredByTest && not v.InCoverageZone then + coveredVerticesOutOfZone <- coveredVerticesOutOfZone + 1u + + if v.VisitedByState && v.InCoverageZone then + visitedVerticesInZone <- visitedVerticesInZone + 1u + + if v.VisitedByState && not v.InCoverageZone then + visitedVerticesOutOfZone <- visitedVerticesOutOfZone + 1u + + if v.InCoverageZone then totalVisibleVerticesInZone <- totalVisibleVerticesInZone + 1u visitedInstructionsInZone <- visitedInstructionsInZone + v.BasicBlockSize - - if v.TouchedByState && v.InCoverageZone - then touchedVerticesInZone <- touchedVerticesInZone + 1u - if v.TouchedByState && not v.InCoverageZone - then touchedVerticesOutOfZone <- touchedVerticesOutOfZone + 1u - - Statistics(coveredVerticesInZone,coveredVerticesOutOfZone,visitedVerticesInZone,visitedVerticesOutOfZone,visitedInstructionsInZone,touchedVerticesInZone,touchedVerticesOutOfZone, totalVisibleVerticesInZone) - -let collectStatesInfoToDump (basicBlocks:ResizeArray) = + + if v.TouchedByState && v.InCoverageZone then + touchedVerticesInZone <- touchedVerticesInZone + 1u + + if v.TouchedByState && not v.InCoverageZone then + touchedVerticesOutOfZone <- touchedVerticesOutOfZone + 1u + + Statistics( + coveredVerticesInZone, + coveredVerticesOutOfZone, + visitedVerticesInZone, + visitedVerticesOutOfZone, + visitedInstructionsInZone, + touchedVerticesInZone, + touchedVerticesOutOfZone, + totalVisibleVerticesInZone + ) + +let collectStatesInfoToDump (basicBlocks: ResizeArray) = let statesMetrics = ResizeArray<_>() + for currentBasicBlock in basicBlocks do - let interproceduralGraphDistanceFrom = Dictionary>() + let interproceduralGraphDistanceFrom = + Dictionary>() + currentBasicBlock.AssociatedStates - |> Seq.iter (fun s -> statesMetrics.Add (calculateStateMetrics interproceduralGraphDistanceFrom s)) - - let statesInfoToDump = + |> Seq.iter (fun s -> statesMetrics.Add(calculateStateMetrics interproceduralGraphDistanceFrom s)) + + let statesInfoToDump = let mutable maxVisitedVertices = UInt32.MinValue let mutable maxChildNumber = UInt32.MinValue let mutable minDistToUncovered = UInt32.MaxValue let mutable minDistToNotVisited = UInt32.MaxValue let mutable minDistToReturn = UInt32.MaxValue - + statesMetrics |> ResizeArray.iter (fun s -> - if s.VisitedVerticesInZone > maxVisitedVertices - then maxVisitedVertices <- s.VisitedVerticesInZone - if s.ChildNumber > maxChildNumber - then maxChildNumber <- s.ChildNumber - if s.DistanceToNearestUncovered < minDistToUncovered - then minDistToUncovered <- s.DistanceToNearestUncovered - if s.DistanceToNearestNotVisited < minDistToNotVisited - then minDistToNotVisited <- s.DistanceToNearestNotVisited - if s.DistanceToNearestReturn < minDistToReturn - then minDistToReturn <- s.DistanceToNearestReturn - ) - let normalize minV v (sm:StateMetrics) = - if v = minV || (v = UInt32.MaxValue && sm.DistanceToNearestReturn = UInt32.MaxValue) - then 1.0 - elif v = UInt32.MaxValue - then 0.0 - else float (1u + minV) / float (1u + v) - + if s.VisitedVerticesInZone > maxVisitedVertices then + maxVisitedVertices <- s.VisitedVerticesInZone + + if s.ChildNumber > maxChildNumber then + maxChildNumber <- s.ChildNumber + + if s.DistanceToNearestUncovered < minDistToUncovered then + minDistToUncovered <- s.DistanceToNearestUncovered + + if s.DistanceToNearestNotVisited < minDistToNotVisited then + minDistToNotVisited <- s.DistanceToNearestNotVisited + + if s.DistanceToNearestReturn < minDistToReturn then + minDistToReturn <- s.DistanceToNearestReturn) + + let normalize minV v (sm: StateMetrics) = + if + v = minV + || (v = UInt32.MaxValue && sm.DistanceToNearestReturn = UInt32.MaxValue) + then + 1.0 + elif v = UInt32.MaxValue then + 0.0 + else + float (1u + minV) / float (1u + v) + statesMetrics - |> ResizeArray.map (fun m -> StateInfoToDump (m.StateId - , m.NextInstructionIsUncoveredInZone - , if maxChildNumber = 0u then 0.0 else float m.ChildNumber / float maxChildNumber - , if maxVisitedVertices = 0u then 0.0 else float m.VisitedVerticesInZone / float maxVisitedVertices - , float m.VisitedVerticesInZone / float m.HistoryLength - , normalize minDistToReturn m.DistanceToNearestReturn m - , normalize minDistToUncovered m.DistanceToNearestUncovered m - , normalize minDistToUncovered m.DistanceToNearestNotVisited m)) + |> ResizeArray.map (fun m -> + StateInfoToDump( + m.StateId, + m.NextInstructionIsUncoveredInZone, + (if maxChildNumber = 0u then + 0.0 + else + float m.ChildNumber / float maxChildNumber), + (if maxVisitedVertices = 0u then + 0.0 + else + float m.VisitedVerticesInZone / float maxVisitedVertices), + float m.VisitedVerticesInZone / float m.HistoryLength, + normalize minDistToReturn m.DistanceToNearestReturn m, + normalize minDistToUncovered m.DistanceToNearestUncovered m, + normalize minDistToUncovered m.DistanceToNearestNotVisited m + )) + statesInfoToDump - -let collectGameState (basicBlocks:ResizeArray) filterStates = - + +let getFirstFreePathConditionVertexId = + let mutable count = 0u + + fun () -> + let res = count + count <- count + 1u + res + +let pathConditionVertices = Dictionary() + +let collectPathCondition term = // TODO: Support other operations + let termsToVisit = + Stack> [| (term, getFirstFreePathConditionVertexId ()) |] + + let pathConditionDelta = ResizeArray() + + while termsToVisit.Count > 0 do + let currentTerm, currentTermId = termsToVisit.Pop() + + let markAsVisited (vertexType: pathConditionVertexType) children = + let newVertex = PathConditionVertex(currentTermId, vertexType, children) + pathConditionVertices.Add(currentTerm, newVertex) + pathConditionDelta.Add newVertex + + let handleTerm term (children: ResizeArray<_>) = + let termId = + if not <| pathConditionVertices.ContainsKey term then + getFirstFreePathConditionVertexId () + else + pathConditionVertices.[term].Id + + children.Add termId + termsToVisit.Push((term, termId)) + + if not <| pathConditionVertices.ContainsKey currentTerm then + match currentTerm.term with + | Nop -> markAsVisited pathConditionVertexType.Nop [||] + | Concrete(_, _) -> markAsVisited pathConditionVertexType.Constant [||] + | Constant(_, _, _) -> markAsVisited pathConditionVertexType.Constant [||] + | Expression(operation, termList, _) -> + let children = ResizeArray>() + + for t in termList do + handleTerm t children + + let children = children.ToArray() + + match operation with + | Operator operator -> + match operator with + | OperationType.UnaryMinus -> pathConditionVertexType.UnaryMinus + | OperationType.BitwiseNot -> pathConditionVertexType.BitwiseNot + | OperationType.BitwiseAnd -> pathConditionVertexType.BitwiseAnd + | OperationType.BitwiseOr -> pathConditionVertexType.BitwiseOr + | OperationType.BitwiseXor -> pathConditionVertexType.BitwiseXor + | OperationType.LogicalNot -> pathConditionVertexType.LogicalNot + | OperationType.LogicalAnd -> pathConditionVertexType.LogicalAnd + | OperationType.LogicalOr -> pathConditionVertexType.LogicalOr + | OperationType.LogicalXor -> pathConditionVertexType.LogicalXor + | OperationType.Equal -> pathConditionVertexType.Equal + | OperationType.NotEqual -> pathConditionVertexType.NotEqual + | OperationType.Greater -> pathConditionVertexType.Greater + | OperationType.Greater_Un -> pathConditionVertexType.Greater_Un + | OperationType.Less -> pathConditionVertexType.Less + | OperationType.Less_Un -> pathConditionVertexType.Less_Un + | OperationType.GreaterOrEqual -> pathConditionVertexType.GreaterOrEqual + | OperationType.GreaterOrEqual_Un -> pathConditionVertexType.GreaterOrEqual_Un + | OperationType.LessOrEqual -> pathConditionVertexType.LessOrEqual + | OperationType.LessOrEqual_Un -> pathConditionVertexType.LessOrEqual_Un + | OperationType.Add -> pathConditionVertexType.Add + | OperationType.AddNoOvf -> pathConditionVertexType.AddNoOvf + | OperationType.AddNoOvf_Un -> pathConditionVertexType.AddNoOvf_Un + | OperationType.Subtract -> pathConditionVertexType.Subtract + | OperationType.SubNoOvf -> pathConditionVertexType.SubNoOvf + | OperationType.SubNoOvf_Un -> pathConditionVertexType.SubNoOvf_Un + | OperationType.Divide -> pathConditionVertexType.Divide + | OperationType.Divide_Un -> pathConditionVertexType.Divide_Un + | OperationType.Multiply -> pathConditionVertexType.Multiply + | OperationType.MultiplyNoOvf -> pathConditionVertexType.MultiplyNoOvf + | OperationType.MultiplyNoOvf_Un -> pathConditionVertexType.MultiplyNoOvf_Un + | OperationType.Remainder -> pathConditionVertexType.Remainder + | OperationType.Remainder_Un -> pathConditionVertexType.Remainder_Un + | OperationType.ShiftLeft -> pathConditionVertexType.ShiftLeft + | OperationType.ShiftRight -> pathConditionVertexType.ShiftRight + | OperationType.ShiftRight_Un -> pathConditionVertexType.ShiftRight_Un + | unexpectedOperation -> failwith $"Add {unexpectedOperation} support." + |> fun vertexType -> markAsVisited vertexType children + + | Application(_) -> markAsVisited pathConditionVertexType.StandardFunctionApplication children + | Cast(_, _) -> markAsVisited pathConditionVertexType.Cast children + | Combine -> markAsVisited pathConditionVertexType.Combine children + | Struct(fields, _) -> + let children = ResizeArray>() + + for _, t in PersistentDict.toSeq fields do + handleTerm t children + + markAsVisited pathConditionVertexType.Struct (children.ToArray()) + | HeapRef(_, _) -> markAsVisited pathConditionVertexType.HeapRef [||] + | Ref(_) -> markAsVisited pathConditionVertexType.Ref [||] + | Ptr(_, _, t) -> + let children = ResizeArray> [||] + handleTerm t children + markAsVisited pathConditionVertexType.Ptr (children.ToArray()) + | Slice(t, listOfTuples) -> + let children = ResizeArray> [||] + handleTerm t children + + for t1, t2, t3, _ in listOfTuples do + handleTerm t1 children + handleTerm t2 children + handleTerm t3 children + + markAsVisited pathConditionVertexType.Slice (children.ToArray()) + | Ite(_) -> markAsVisited pathConditionVertexType.Ite [||] + + pathConditionDelta + +let collectGameState (basicBlocks: ResizeArray) filterStates = + let vertices = ResizeArray<_>() let allStates = HashSet<_>() - + let activeStates = basicBlocks |> Seq.collect (fun basicBlock -> basicBlock.AssociatedStates) |> Seq.map (fun s -> s.Id) |> fun x -> HashSet x - - for currentBasicBlock in basicBlocks do + + let pathConditionDelta = ResizeArray() + + for currentBasicBlock in basicBlocks do let states = currentBasicBlock.AssociatedStates - |> Seq.map (fun s -> - State(s.Id, - (uint <| s.CodeLocation.offset - currentBasicBlock.StartOffset + 1) * 1u, - s.PathConditionSize, - s.VisitedAgainVertices, - s.VisitedNotCoveredVerticesInZone, - s.VisitedNotCoveredVerticesOutOfZone, - s.StepWhenMovedLastTime, - s.InstructionsVisitedInCurrentBlock, - s.History |> Seq.map (fun kvp -> kvp.Value) |> Array.ofSeq, - s.Children |> Array.map (fun s -> s.Id) - |> (fun x -> if filterStates - then Array.filter activeStates.Contains x - else x) - ) + |> Seq.map (fun (s: IGraphTrackableState) -> + let pathCondition = s.PathCondition |> PC.toSeq + + for term in pathCondition do + pathConditionDelta.AddRange(collectPathCondition term) + + State( + s.Id, + (uint <| s.CodeLocation.offset - currentBasicBlock.StartOffset + 1) + * 1u, + PathConditionVertex( + id = getFirstFreePathConditionVertexId (), + pathConditionVertexType = pathConditionVertexType.PathConditionRoot, + children = [| for p in pathCondition -> pathConditionVertices.[p].Id |] + ), + s.VisitedAgainVertices, + s.VisitedNotCoveredVerticesInZone, + s.VisitedNotCoveredVerticesOutOfZone, + s.StepWhenMovedLastTime, + s.InstructionsVisitedInCurrentBlock, + s.History |> Seq.map (fun kvp -> kvp.Value) |> Array.ofSeq, + s.Children + |> Array.map (fun s -> s.Id) + |> (fun x -> + if filterStates then + Array.filter activeStates.Contains x + else + x) + ) |> allStates.Add |> ignore - s.Id - ) + + s.Id) |> Array.ofSeq - + GameMapVertex( currentBasicBlock.Id, currentBasicBlock.IsGoal, - uint <| currentBasicBlock.FinalOffset - currentBasicBlock.StartOffset + 1, + uint + <| currentBasicBlock.FinalOffset - currentBasicBlock.StartOffset + 1, currentBasicBlock.IsCovered, currentBasicBlock.IsVisited, currentBasicBlock.IsTouched, currentBasicBlock.ContainsCall, currentBasicBlock.ContainsThrow, - states) + states + ) |> vertices.Add - + let edges = ResizeArray<_>() - + for basicBlock in basicBlocks do for outgoingEdges in basicBlock.OutgoingEdges do for targetBasicBlock in outgoingEdges.Value do - GameMapEdge (basicBlock.Id, - targetBasicBlock.Id, - GameEdgeLabel (int outgoingEdges.Key)) + GameMapEdge(basicBlock.Id, targetBasicBlock.Id, GameEdgeLabel(int outgoingEdges.Key)) |> edges.Add - - GameState (vertices.ToArray(), allStates |> Array.ofSeq, edges.ToArray()) - + + GameState(vertices.ToArray(), allStates |> Array.ofSeq, pathConditionDelta.ToArray(), edges.ToArray()) + let collectGameStateDelta () = let basicBlocks = HashSet<_>(Application.applicationGraphDelta.TouchedBasicBlocks) + for method in Application.applicationGraphDelta.LoadedMethods do for basicBlock in method.BasicBlocks do basicBlock.IsGoal <- method.InCoverageZone let added = basicBlocks.Add(basicBlock) () - collectGameState (ResizeArray basicBlocks) false -let dumpGameState fileForResultWithoutExtension (movedStateId:uint) = + collectGameState (ResizeArray basicBlocks) false + +let dumpGameState fileForResultWithoutExtension (movedStateId: uint) = let basicBlocks = ResizeArray<_>() + for method in Application.loadedMethods do for basicBlock in method.Key.BasicBlocks do basicBlock.IsGoal <- method.Key.InCoverageZone basicBlocks.Add(basicBlock) - + let gameState = collectGameState basicBlocks true let statesInfoToDump = collectStatesInfoToDump basicBlocks - let gameStateJson = JsonSerializer.Serialize gameState + let gameStateJson = JsonSerializer.Serialize gameState let statesInfoJson = JsonSerializer.Serialize statesInfoToDump File.WriteAllText(fileForResultWithoutExtension + "_gameState", gameStateJson) File.WriteAllText(fileForResultWithoutExtension + "_statesInfo", statesInfoJson) File.WriteAllText(fileForResultWithoutExtension + "_movedState", string movedStateId) - -let computeReward (statisticsBeforeStep:Statistics) (statisticsAfterStep:Statistics) = + +let computeReward (statisticsBeforeStep: Statistics) (statisticsAfterStep: Statistics) = let rewardForCoverage = - (statisticsAfterStep.CoveredVerticesInZone - statisticsBeforeStep.CoveredVerticesInZone) * 1u - let rewardForVisitedInstructions = - (statisticsAfterStep.VisitedInstructionsInZone - statisticsBeforeStep.VisitedInstructionsInZone) * 1u - let maxPossibleReward = (statisticsBeforeStep.TotalVisibleVerticesInZone - statisticsBeforeStep.CoveredVerticesInZone) * 1u - - Reward (rewardForCoverage, rewardForVisitedInstructions, maxPossibleReward) - \ No newline at end of file + (statisticsAfterStep.CoveredVerticesInZone + - statisticsBeforeStep.CoveredVerticesInZone) + * 1u + + let rewardForVisitedInstructions = + (statisticsAfterStep.VisitedInstructionsInZone + - statisticsBeforeStep.VisitedInstructionsInZone) + * 1u + + let maxPossibleReward = + (statisticsBeforeStep.TotalVisibleVerticesInZone + - statisticsBeforeStep.CoveredVerticesInZone) + * 1u + + Reward(rewardForCoverage, rewardForVisitedInstructions, maxPossibleReward) diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs index 1ae0927da..17ba5e6b7 100644 --- a/VSharp.ML.GameServer.Runner/Main.fs +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -23,22 +23,22 @@ type ExplorationResult = val TestsCount: uint val ErrorsCount: uint val StepsCount: uint + new(actualCoverage, testsCount, errorsCount, stepsCount) = - { - ActualCoverage = actualCoverage - TestsCount = testsCount - ErrorsCount = errorsCount - StepsCount = stepsCount - } + { ActualCoverage = actualCoverage + TestsCount = testsCount + ErrorsCount = errorsCount + StepsCount = stepsCount } type Mode = | Server = 0 | Generator = 1 + type CliArguments = | [] Port of int | [] DatasetBasePath of string | [] DatasetDescription of string - | [] Mode of Mode + | [] Mode of Mode | [] OutFolder of string | [] StepsToSerialize of uint | [] UseGPU @@ -62,21 +62,21 @@ type CliArguments = let mutable inTrainMode = true let explore (gameMap: GameMap) options = - let assembly = - RunnerProgram.TryLoadAssembly <| FileInfo gameMap.AssemblyFullName - let method = - RunnerProgram.ResolveMethod (assembly, gameMap.NameOfObjectToCover) - let statistics = - TestGenerator.Cover (method, options) + let assembly = RunnerProgram.TryLoadAssembly <| FileInfo gameMap.AssemblyFullName + let method = RunnerProgram.ResolveMethod(assembly, gameMap.NameOfObjectToCover) + let statistics = TestGenerator.Cover(method, options) + let actualCoverage = try let testsDir = statistics.OutputDir let _expectedCoverage = 100 - let exploredMethodInfo = - AssemblyManager.NormalizeMethod method + let exploredMethodInfo = AssemblyManager.NormalizeMethod method + let status, actualCoverage, message = - VSharp.Test.TestResultChecker.Check (testsDir, exploredMethodInfo :?> MethodInfo, _expectedCoverage) + VSharp.Test.TestResultChecker.Check(testsDir, exploredMethodInfo :?> MethodInfo, _expectedCoverage) + printfn $"Actual coverage for {gameMap.MapName}: {actualCoverage}" + if actualCoverage < 0 then 0u else @@ -85,7 +85,7 @@ let explore (gameMap: GameMap) options = printfn $"Coverage checking problem:{e.Message} \n {e.StackTrace}" 0u - ExplorationResult ( + ExplorationResult( actualCoverage, statistics.TestsCount * 1u, statistics.ErrorsCount * 1u, @@ -94,11 +94,12 @@ let explore (gameMap: GameMap) options = let loadGameMaps (datasetDescriptionFilePath: string) = - let jsonString = - File.ReadAllText datasetDescriptionFilePath - let maps = ResizeArray () + let jsonString = File.ReadAllText datasetDescriptionFilePath + let maps = ResizeArray() + for map in System.Text.Json.JsonSerializer.Deserialize jsonString do maps.Add map + maps let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = @@ -111,6 +112,7 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = serializeOutgoingMessage message |> System.Text.Encoding.UTF8.GetBytes |> ByteSegment + webSocket.send Text byteResponse true let oracle = @@ -123,17 +125,20 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = | Feedback.ServerError s -> OutgoingMessage.ServerError s | Feedback.MoveReward reward -> OutgoingMessage.MoveReward reward | Feedback.IncorrectPredictedStateId i -> OutgoingMessage.IncorrectPredictedStateId i + do! sendResponse message } + match Async.RunSynchronously res with - | Choice1Of2 () -> () + | Choice1Of2() -> () | Choice2Of2 error -> failwithf $"Error: %A{error}" let predict = let mutable cnt = 0u + fun (gameState: GameState) -> let toDot drawHistory = - let file = Path.Join ("dot", $"{cnt}.dot") + let file = Path.Join("dot", $"{cnt}.dot") gameState.ToDot file drawHistory cnt <- cnt + 1u //toDot false @@ -141,48 +146,54 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = socket { do! sendResponse (ReadyForNextStep gameState) let! msg = webSocket.read () + let res = match msg with | (Text, data, true) -> let msg = deserializeInputMessage data + match msg with | Step stepParams -> (stepParams.StateId) | _ -> failwithf $"Unexpected message: %A{msg}" | _ -> failwithf $"Unexpected message: %A{msg}" + return res } + match Async.RunSynchronously res with | Choice1Of2 i -> i | Choice2Of2 error -> failwithf $"Error: %A{error}" - Oracle (predict, feedback) + Oracle(predict, feedback) while loop do let! msg = webSocket.read () + match msg with | (Text, data, true) -> let message = deserializeInputMessage data + match message with | ServerStop -> loop <- false | Start gameMap -> printfn $"Start map {gameMap.MapName}, port {port}" let stepsToStart = gameMap.StepsToStart let stepsToPlay = gameMap.StepsToPlay + let aiTrainingOptions = - { - stepsToSwitchToAI = stepsToStart - stepsToPlay = stepsToPlay - defaultSearchStrategy = - match gameMap.DefaultSearcher with - | searcher.BFS -> BFSMode - | searcher.DFS -> DFSMode - | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." - serializeSteps = false - mapName = gameMap.MapName - oracle = Some oracle - } + { stepsToSwitchToAI = stepsToStart + stepsToPlay = stepsToPlay + defaultSearchStrategy = + match gameMap.DefaultSearcher with + | searcher.BFS -> BFSMode + | searcher.DFS -> DFSMode + | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." + serializeSteps = false + mapName = gameMap.MapName + oracle = Some oracle } + let options = - VSharpOptions ( + VSharpOptions( timeout = 15 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.AI, @@ -190,20 +201,23 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = stepsLimit = uint (stepsToPlay + stepsToStart), solverTimeout = 2 ) - let explorationResult = - explore gameMap options + + let explorationResult = explore gameMap options Application.reset () - API.Reset () - HashMap.hashMap.Clear () + API.Reset() + HashMap.hashMap.Clear() + Serializer.pathConditionVertices.Clear() + do! sendResponse ( - GameOver ( + GameOver( explorationResult.ActualCoverage, explorationResult.TestsCount, explorationResult.ErrorsCount ) ) + printfn $"Finish map {gameMap.MapName}, port {port}" | x -> failwithf $"Unexpected message: %A{x}" @@ -215,36 +229,33 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = } let app port outputDirectory : WebPart = - choose - [ - path "/gameServer" >=> handShake (ws port outputDirectory) - ] + choose [ path "/gameServer" >=> handShake (ws port outputDirectory) ] let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArray) stepsToSerialize = for map in maps do if map.StepsToStart = 0u then printfn $"Generation for {map.MapName} started." + let map = - GameMap ( + GameMap( map.StepsToPlay, map.StepsToStart, - Path.Combine (datasetBasePath, map.AssemblyFullName), + Path.Combine(datasetBasePath, map.AssemblyFullName), map.DefaultSearcher, map.NameOfObjectToCover, map.MapName ) + let aiTrainingOptions = - { - stepsToSwitchToAI = 0u - stepsToPlay = 0u - defaultSearchStrategy = searchMode.BFSMode - serializeSteps = true - mapName = map.MapName - oracle = None - } + { stepsToSwitchToAI = 0u + stepsToPlay = 0u + defaultSearchStrategy = searchMode.BFSMode + serializeSteps = true + mapName = map.MapName + oracle = None } let options = - VSharpOptions ( + VSharpOptions( timeout = 5 * 60, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.ExecutionTreeContributedCoverage, @@ -252,28 +263,34 @@ let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArra solverTimeout = 2, aiAgentTrainingOptions = aiTrainingOptions ) + let folderForResults = Serializer.getFolderToStoreSerializationResult outputDirectory map.MapName + if Directory.Exists folderForResults then - Directory.Delete (folderForResults, true) - let _ = - Directory.CreateDirectory folderForResults + Directory.Delete(folderForResults, true) + + let _ = Directory.CreateDirectory folderForResults let explorationResult = explore map options - File.WriteAllText ( - Path.Join (folderForResults, "result"), + + File.WriteAllText( + Path.Join(folderForResults, "result"), $"{explorationResult.ActualCoverage} {explorationResult.TestsCount} {explorationResult.StepsCount} {explorationResult.ErrorsCount}" ) + printfn $"Generation for {map.MapName} finished with coverage {explorationResult.ActualCoverage}, tests {explorationResult.TestsCount}, steps {explorationResult.StepsCount},errors {explorationResult.ErrorsCount}." + Application.reset () - API.Reset () - HashMap.hashMap.Clear () + API.Reset() + HashMap.hashMap.Clear() [] let main args = let parser = - ArgumentParser.Create (programName = "VSharp.ML.GameServer.Runner.exe") + ArgumentParser.Create(programName = "VSharp.ML.GameServer.Runner.exe") + let args = parser.Parse args let mode = args.GetResult <@ Mode @> @@ -298,19 +315,16 @@ let main args = | Some steps -> steps | None -> 500u - let useGPU = - (args.TryGetResult <@ UseGPU @>).IsSome + let useGPU = (args.TryGetResult <@ UseGPU @>).IsSome - let optimize = - (args.TryGetResult <@ Optimize @>).IsSome + let optimize = (args.TryGetResult <@ Optimize @>).IsSome - let outputDirectory = - Path.Combine (Directory.GetCurrentDirectory (), string port) + let outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), string port) if Directory.Exists outputDirectory then - Directory.Delete (outputDirectory, true) - let testsDirInfo = - Directory.CreateDirectory outputDirectory + Directory.Delete(outputDirectory, true) + + let testsDirInfo = Directory.CreateDirectory outputDirectory printfn $"outputDir: {outputDirectory}" match mode with @@ -319,11 +333,7 @@ let main args = startWebServer { defaultConfig with logger = Targets.create Verbose [||] - bindings = - [ - HttpBinding.createSimple HTTP "127.0.0.1" port - ] - } + bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" port ] } (app port outputDirectory) with e -> printfn $"Failed on port {port}" diff --git a/VSharp.ML.GameServer/Messages.fs b/VSharp.ML.GameServer/Messages.fs index aa5fdedf4..43c60c51b 100644 --- a/VSharp.ML.GameServer/Messages.fs +++ b/VSharp.ML.GameServer/Messages.fs @@ -4,99 +4,187 @@ open System.Text open System.Text.Json open System.Text.Json.Serialization open VSharp - + type searcher = | BFS = 0 | DFS = 1 - + [] type RawInputMessage = val MessageType: string val MessageBody: string + [] - new (messageType, messageBody) = {MessageBody = messageBody; MessageType = messageType} + new(messageType, messageBody) = + { MessageBody = messageBody + MessageType = messageType } type IRawOutgoingMessageBody = interface end -type [] test -type [] error -type [] step -type [] percent -type [] basicBlockGlobalId -type [] instruction +[] +type pathConditionVertexId + +type pathConditionVertexType = + | UnaryMinus = 0 + | BitwiseNot = 1 + | BitwiseAnd = 2 + | BitwiseOr = 3 + | BitwiseXor = 4 + | LogicalNot = 5 + | LogicalAnd = 6 + | LogicalOr = 7 + | LogicalXor = 8 + | Equal = 9 + | NotEqual = 10 + | Greater = 11 + | Greater_Un = 12 + | Less = 13 + | Less_Un = 14 + | GreaterOrEqual = 15 + | GreaterOrEqual_Un = 16 + | LessOrEqual = 17 + | LessOrEqual_Un = 18 + | Add = 19 + | AddNoOvf = 20 + | AddNoOvf_Un = 21 + | Subtract = 22 + | SubNoOvf = 23 + | SubNoOvf_Un = 24 + | Divide = 25 + | Divide_Un = 26 + | Multiply = 27 + | MultiplyNoOvf = 28 + | MultiplyNoOvf_Un = 29 + | Remainder = 30 + | Remainder_Un = 31 + | ShiftLeft = 32 + | ShiftRight = 33 + | ShiftRight_Un = 34 + | Nop = 35 + | Concrete = 36 + | Constant = 37 + | Struct = 39 + | HeapRef = 40 + | Ref = 41 + | Ptr = 42 + | Slice = 43 + | Ite = 44 + | StandardFunctionApplication = 45 + | Cast = 46 + | Combine = 47 + | PathConditionRoot = 48 + [] -type GameOverMessageBody = - interface IRawOutgoingMessageBody - val ActualCoverage: uint - val TestsCount: uint32 - val ErrorsCount: uint32 - new (actualCoverage, testsCount, errorsCount) = {ActualCoverage = actualCoverage; TestsCount = testsCount; ErrorsCount = errorsCount} - +type PathConditionVertex = + val Id: uint + val Type: pathConditionVertexType + val Children: array> + + new(id, pathConditionVertexType, children) = + { Id = id + Type = pathConditionVertexType + Children = children } + +[] +type test + +[] +type error + +[] +type step + +[] +type percent + +[] +type basicBlockGlobalId + +[] +type instruction + +[] +type GameOverMessageBody = + interface IRawOutgoingMessageBody + val ActualCoverage: uint + val TestsCount: uint32 + val ErrorsCount: uint32 + + new(actualCoverage, testsCount, errorsCount) = + { ActualCoverage = actualCoverage + TestsCount = testsCount + ErrorsCount = errorsCount } + [] type RawOutgoingMessage = val MessageType: string val MessageBody: obj - new (messageType, messageBody) = {MessageBody = messageBody; MessageType = messageType} - -type [] stateId + + new(messageType, messageBody) = + { MessageBody = messageBody + MessageType = messageType } + +[] +type stateId [] type GameStep = - val StateId: uint - + val StateId: uint + [] - new (stateId) = {StateId = stateId} - + new(stateId) = { StateId = stateId } + [] type StateHistoryElem = val GraphVertexId: uint val NumOfVisits: uint val StepWhenVisitedLastTime: uint - new (graphVertexId, numOfVisits, stepWhenVisitedLastTime) = - { - GraphVertexId = graphVertexId - NumOfVisits = numOfVisits - StepWhenVisitedLastTime = stepWhenVisitedLastTime - } + + new(graphVertexId, numOfVisits, stepWhenVisitedLastTime) = + { GraphVertexId = graphVertexId + NumOfVisits = numOfVisits + StepWhenVisitedLastTime = stepWhenVisitedLastTime } [] type State = val Id: uint val Position: uint // to basic block id - val PathConditionSize: uint + val PathCondition: PathConditionVertex val VisitedAgainVertices: uint val VisitedNotCoveredVerticesInZone: uint val VisitedNotCoveredVerticesOutOfZone: uint val StepWhenMovedLastTime: uint val InstructionsVisitedInCurrentBlock: uint val History: array - val Children: array> - new(id, - position, - pathConditionSize, - visitedAgainVertices, - visitedNotCoveredVerticesInZone, - visitedNotCoveredVerticesOutOfZone, - stepWhenMovedLastTime, - instructionsVisitedInCurrentBlock, - history, - children) = - { - Id = id - Position = position - PathConditionSize = pathConditionSize - VisitedAgainVertices = visitedAgainVertices - VisitedNotCoveredVerticesInZone = visitedNotCoveredVerticesInZone - VisitedNotCoveredVerticesOutOfZone = visitedNotCoveredVerticesOutOfZone - StepWhenMovedLastTime = stepWhenMovedLastTime - InstructionsVisitedInCurrentBlock = instructionsVisitedInCurrentBlock - History = history - Children = children - } - -[] + val Children: array> + + new + ( + id, + position, + pathCondition, + visitedAgainVertices, + visitedNotCoveredVerticesInZone, + visitedNotCoveredVerticesOutOfZone, + stepWhenMovedLastTime, + instructionsVisitedInCurrentBlock, + history, + children + ) = + { Id = id + Position = position + PathCondition = pathCondition + VisitedAgainVertices = visitedAgainVertices + VisitedNotCoveredVerticesInZone = visitedNotCoveredVerticesInZone + VisitedNotCoveredVerticesOutOfZone = visitedNotCoveredVerticesOutOfZone + StepWhenMovedLastTime = stepWhenMovedLastTime + InstructionsVisitedInCurrentBlock = instructionsVisitedInCurrentBlock + History = history + Children = children } + +[] type GameMapVertex = val Id: uint val InCoverageZone: bool @@ -107,98 +195,129 @@ type GameMapVertex = val ContainsCall: bool val ContainsThrow: bool val States: uint[] - new (id, - inCoverageZone, - basicBlockSize, - containsCall, - containsThrow, - coveredByTest, - visitedByState, - touchedByState, - states) = - { - Id = id - InCoverageZone = inCoverageZone - BasicBlockSize = basicBlockSize - CoveredByTest = coveredByTest - VisitedByState = visitedByState - TouchedByState = touchedByState - ContainsCall = containsCall - ContainsThrow = containsThrow - States = states - } + + new + ( + id, + inCoverageZone, + basicBlockSize, + containsCall, + containsThrow, + coveredByTest, + visitedByState, + touchedByState, + states + ) = + { Id = id + InCoverageZone = inCoverageZone + BasicBlockSize = basicBlockSize + CoveredByTest = coveredByTest + VisitedByState = visitedByState + TouchedByState = touchedByState + ContainsCall = containsCall + ContainsThrow = containsThrow + States = states } [] type GameEdgeLabel = val Token: int - new (token) = {Token = token} + new(token) = { Token = token } [] type GameMapEdge = val VertexFrom: uint val VertexTo: uint val Label: GameEdgeLabel - new (vFrom, vTo, label) = {VertexFrom = vFrom; VertexTo = vTo; Label = label} - + + new(vFrom, vTo, label) = + { VertexFrom = vFrom + VertexTo = vTo + Label = label } + [] type GameState = interface IRawOutgoingMessageBody val GraphVertices: GameMapVertex[] val States: State[] + val PathConditionVertices: PathConditionVertex[] val Map: GameMapEdge[] - new (graphVertices, states, map) = {GraphVertices = graphVertices; States = states; Map = map} - + + new(graphVertices, states, pathConditionVertices, map) = + { GraphVertices = graphVertices + States = states + PathConditionVertices = pathConditionVertices + Map = map } + member this.ToDot file drawHistoryEdges = let vertices = ResizeArray<_>() let edges = ResizeArray<_>() + for v in this.GraphVertices do - let color = if v.CoveredByTest - then "green" - elif v.VisitedByState - then "red" - elif v.TouchedByState - then "yellow" - else "white" + let color = + if v.CoveredByTest then "green" + elif v.VisitedByState then "red" + elif v.TouchedByState then "yellow" + else "white" + vertices.Add($"{v.Id} [label={v.Id}, shape=box, style=filled, fillcolor={color}]") + for s in v.States do edges.Add($"99{s}00 -> {v.Id} [label=L]") + for s in this.States do vertices.Add($"99{s.Id}00 [label={s.Id}, shape=circle]") + for v in s.Children do edges.Add($"99{s.Id}00 -> 99{v}00 [label=ch]") - if drawHistoryEdges - then + + if drawHistoryEdges then for v in s.History do edges.Add($"99{s.Id}00 -> {v.GraphVertexId} [label={v.NumOfVisits}]") + for e in this.Map do edges.Add($"{e.VertexFrom}->{e.VertexTo}[label={e.Label.Token}]") + let dot = - seq - { - "digraph g{" - yield! vertices - yield! edges - "}" - } + seq { + "digraph g{" + yield! vertices + yield! edges + "}" + } + System.IO.File.WriteAllLines(file, dot) - -type [] coverageReward -type [] visitedInstructionsReward -type [] maxPossibleReward - + +[] +type coverageReward + +[] +type visitedInstructionsReward + +[] +type maxPossibleReward + [] type MoveReward = val ForCoverage: uint val ForVisitedInstructions: uint - new (forCoverage, forVisitedInstructions) = {ForCoverage = forCoverage; ForVisitedInstructions = forVisitedInstructions} + + new(forCoverage, forVisitedInstructions) = + { ForCoverage = forCoverage + ForVisitedInstructions = forVisitedInstructions } [] type Reward = interface IRawOutgoingMessageBody val ForMove: MoveReward val MaxPossibleReward: uint - new (forMove, maxPossibleReward) = {ForMove = forMove; MaxPossibleReward = maxPossibleReward} - new (forCoverage, forVisitedInstructions, maxPossibleReward) = {ForMove = MoveReward(forCoverage,forVisitedInstructions); MaxPossibleReward = maxPossibleReward} + + new(forMove, maxPossibleReward) = + { ForMove = forMove + MaxPossibleReward = maxPossibleReward } + + new(forCoverage, forVisitedInstructions, maxPossibleReward) = + { ForMove = MoveReward(forCoverage, forVisitedInstructions) + MaxPossibleReward = maxPossibleReward } type Feedback = | MoveReward of Reward @@ -209,83 +328,80 @@ type Feedback = type GameMap = val StepsToPlay: uint val StepsToStart: uint + [)>] - val DefaultSearcher: searcher + val DefaultSearcher: searcher + val AssemblyFullName: string val NameOfObjectToCover: string val MapName: string - new (stepsToPlay, stepsToStart, assembly, defaultSearcher, objectToCover) = - { - StepsToPlay = stepsToPlay - StepsToStart = stepsToStart - AssemblyFullName = assembly - NameOfObjectToCover = objectToCover - DefaultSearcher = defaultSearcher - MapName = $"{objectToCover}_{defaultSearcher}_{stepsToStart}" - } - + + new(stepsToPlay, stepsToStart, assembly, defaultSearcher, objectToCover) = + { StepsToPlay = stepsToPlay + StepsToStart = stepsToStart + AssemblyFullName = assembly + NameOfObjectToCover = objectToCover + DefaultSearcher = defaultSearcher + MapName = $"{objectToCover}_{defaultSearcher}_{stepsToStart}" } + [] - new (stepsToPlay, stepsToStart, assemblyFullName, defaultSearcher, nameOfObjectToCover, mapName) = - { - StepsToPlay = stepsToPlay - StepsToStart = stepsToStart - AssemblyFullName = assemblyFullName - DefaultSearcher = defaultSearcher - NameOfObjectToCover = nameOfObjectToCover - MapName = mapName - } + new(stepsToPlay, stepsToStart, assemblyFullName, defaultSearcher, nameOfObjectToCover, mapName) = + { StepsToPlay = stepsToPlay + StepsToStart = stepsToStart + AssemblyFullName = assemblyFullName + DefaultSearcher = defaultSearcher + NameOfObjectToCover = nameOfObjectToCover + MapName = mapName } type InputMessage = | ServerStop | Start of GameMap | Step of GameStep - + [] - type ServerErrorMessageBody = - interface IRawOutgoingMessageBody - val ErrorMessage: string - new (errorMessage) = {ErrorMessage = errorMessage} +type ServerErrorMessageBody = + interface IRawOutgoingMessageBody + val ErrorMessage: string + new(errorMessage) = { ErrorMessage = errorMessage } [] - type IncorrectPredictedStateIdMessageBody = - interface IRawOutgoingMessageBody - val StateId: uint - new (stateId) = {StateId = stateId} - +type IncorrectPredictedStateIdMessageBody = + interface IRawOutgoingMessageBody + val StateId: uint + new(stateId) = { StateId = stateId } + type OutgoingMessage = - | GameOver of uint*uint32*uint32 + | GameOver of uint * uint32 * uint32 | MoveReward of Reward | IncorrectPredictedStateId of uint | ReadyForNextStep of GameState | ServerError of string - -let (|MsgTypeStart|MsgTypeStep|MsgStop|) (str:string) = + +let (|MsgTypeStart|MsgTypeStep|MsgStop|) (str: string) = let normalized = str.ToLowerInvariant().Trim() - if normalized = "start" - then MsgTypeStart - elif normalized = "step" - then MsgTypeStep - elif normalized = "stop" - then MsgStop + + if normalized = "start" then MsgTypeStart + elif normalized = "step" then MsgTypeStep + elif normalized = "stop" then MsgStop else failwithf $"Unexpected message type %s{str}" - -let deserializeInputMessage (messageData:byte[]) = + +let deserializeInputMessage (messageData: byte[]) = let rawInputMessage = let str = Encoding.UTF8.GetString messageData str |> JsonSerializer.Deserialize + match rawInputMessage.MessageType with | MsgStop -> ServerStop - | MsgTypeStart -> Start (JsonSerializer.Deserialize rawInputMessage.MessageBody) - | MsgTypeStep -> Step (JsonSerializer.Deserialize(rawInputMessage.MessageBody)) + | MsgTypeStart -> Start(JsonSerializer.Deserialize rawInputMessage.MessageBody) + | MsgTypeStep -> Step(JsonSerializer.Deserialize(rawInputMessage.MessageBody)) -let serializeOutgoingMessage (message:OutgoingMessage) = +let serializeOutgoingMessage (message: OutgoingMessage) = match message with - | GameOver (actualCoverage,testsCount, errorsCount) -> RawOutgoingMessage("GameOver", box (GameOverMessageBody (actualCoverage, testsCount, errorsCount))) + | GameOver(actualCoverage, testsCount, errorsCount) -> + RawOutgoingMessage("GameOver", box (GameOverMessageBody(actualCoverage, testsCount, errorsCount))) | MoveReward reward -> RawOutgoingMessage("MoveReward", reward) - | IncorrectPredictedStateId stateId -> RawOutgoingMessage("IncorrectPredictedStateId", IncorrectPredictedStateIdMessageBody stateId) + | IncorrectPredictedStateId stateId -> + RawOutgoingMessage("IncorrectPredictedStateId", IncorrectPredictedStateIdMessageBody stateId) | ReadyForNextStep state -> RawOutgoingMessage("ReadyForNextStep", state) | ServerError errorMessage -> RawOutgoingMessage("ServerError", ServerErrorMessageBody errorMessage) |> JsonSerializer.Serialize - - - \ No newline at end of file diff --git a/VSharp.SILI.Core/PathCondition.fs b/VSharp.SILI.Core/PathCondition.fs index f30f80b5c..cd17e694a 100644 --- a/VSharp.SILI.Core/PathCondition.fs +++ b/VSharp.SILI.Core/PathCondition.fs @@ -7,7 +7,7 @@ type pathCondition = pset // - PC does not contain True // - if PC contains False then False is the only element in PC -module internal PC = +module PC = let public empty : pathCondition = PersistentSet.empty let public isEmpty pc = PersistentSet.isEmpty pc diff --git a/VSharp.SILI/CILState.fs b/VSharp.SILI/CILState.fs index 44a90cb96..a8ef1cdeb 100644 --- a/VSharp.SILI/CILState.fs +++ b/VSharp.SILI/CILState.fs @@ -544,7 +544,7 @@ module CilState = override this.CodeLocation = this.approximateLoc override this.CallStack = Memory.StackTrace this.state.memory.Stack |> List.map (fun m -> m :?> Method) override this.Id = this.internalId - override this.PathConditionSize with get () = PersistentSet.cardinality this.state.pc |> uint32 + override this.PathCondition with get () = this.state.pc override this.VisitedAgainVertices with get () = this.visitedAgainVertices override this.VisitedNotCoveredVerticesInZone with get () = this.visitedNotCoveredVerticesInZone override this.VisitedNotCoveredVerticesOutOfZone with get () = this.visitedNotCoveredVerticesOutOfZone From f7129f63dff0cc571928d943d59766a413b5a8cb Mon Sep 17 00:00:00 2001 From: Anya Chistyakova <57658770+Anya497@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:43:00 +0300 Subject: [PATCH 07/12] Add path condition root to GameState. (#93) --- VSharp.IL/Serializer.fs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/VSharp.IL/Serializer.fs b/VSharp.IL/Serializer.fs index f42e9d46f..182dd899a 100644 --- a/VSharp.IL/Serializer.fs +++ b/VSharp.IL/Serializer.fs @@ -488,15 +488,20 @@ let collectGameState (basicBlocks: ResizeArray) filterStates = for term in pathCondition do pathConditionDelta.AddRange(collectPathCondition term) - State( - s.Id, - (uint <| s.CodeLocation.offset - currentBasicBlock.StartOffset + 1) - * 1u, + let pathConditionRoot = PathConditionVertex( id = getFirstFreePathConditionVertexId (), pathConditionVertexType = pathConditionVertexType.PathConditionRoot, children = [| for p in pathCondition -> pathConditionVertices.[p].Id |] - ), + ) + + pathConditionDelta.Add pathConditionRoot + + State( + s.Id, + (uint <| s.CodeLocation.offset - currentBasicBlock.StartOffset + 1) + * 1u, + pathConditionRoot, s.VisitedAgainVertices, s.VisitedNotCoveredVerticesInZone, s.VisitedNotCoveredVerticesOutOfZone, From 4e90c5123f91ef4a79af03b8a710c4c62ec06f5d Mon Sep 17 00:00:00 2001 From: David Akhmedov <113194669+Parzival-05@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:26:22 +0300 Subject: [PATCH 08/12] Support of new training process (#89) * Support of new training process --- VSharp.API/VSharp.cs | 6 +- VSharp.API/VSharpOptions.cs | 8 +- VSharp.Explorer/AISearcher.fs | 211 ++++++++++------ VSharp.Explorer/Explorer.fs | 362 +++++++++++++++------------ VSharp.Explorer/Options.fs | 109 ++++---- VSharp.ML.GameServer.Runner/Main.fs | 231 ++++++++++++----- VSharp.Test/Benchmarks/Benchmarks.cs | 6 +- VSharp.Test/IntegrationTests.cs | 13 +- 8 files changed, 596 insertions(+), 350 deletions(-) diff --git a/VSharp.API/VSharp.cs b/VSharp.API/VSharp.cs index f6b186161..20ea57652 100644 --- a/VSharp.API/VSharp.cs +++ b/VSharp.API/VSharp.cs @@ -191,10 +191,10 @@ private static Statistics StartExploration( stopOnCoverageAchieved: 100, randomSeed: options.RandomSeed, stepsLimit: options.StepsLimit, - aiAgentTrainingOptions: options.AIAgentTrainingOptions == null ? FSharpOption.None : FSharpOption.Some(options.AIAgentTrainingOptions), + aiOptions: options.AIOptions == null ? FSharpOption.None : FSharpOption.Some(options.AIOptions), pathToModel: options.PathToModel == null ? FSharpOption.None : FSharpOption.Some(options.PathToModel), - useGPU: options.UseGPU == null ? FSharpOption.None : FSharpOption.Some(options.UseGPU), - optimize: options.Optimize == null ? FSharpOption.None : FSharpOption.Some(options.Optimize) + useGPU: options.UseGPU, + optimize: options.Optimize ); var fuzzerOptions = diff --git a/VSharp.API/VSharpOptions.cs b/VSharp.API/VSharpOptions.cs index 7d6d7901b..c26eca6ef 100644 --- a/VSharp.API/VSharpOptions.cs +++ b/VSharp.API/VSharpOptions.cs @@ -113,7 +113,7 @@ public readonly record struct VSharpOptions public readonly bool ReleaseBranches = DefaultReleaseBranches; public readonly int RandomSeed = DefaultRandomSeed; public readonly uint StepsLimit = DefaultStepsLimit; - public readonly AIAgentTrainingOptions AIAgentTrainingOptions = null; + public readonly AIOptions? AIOptions = null; public readonly string PathToModel = DefaultPathToModel; public readonly bool UseGPU = false; public readonly bool Optimize = false; @@ -133,7 +133,7 @@ public readonly record struct VSharpOptions /// If true and timeout is specified, a part of allotted time in the end is given to execute remaining states without branching. /// Fixed seed for random operations. Used if greater than or equal to zero. /// Number of symbolic machine steps to stop execution after. Zero value means no limit. - /// Settings for AI searcher training. + /// Settings for AI searcher training. /// Path to ONNX file with model to use in AI searcher. /// Specifies whether the ONNX execution session should use a CUDA-enabled GPU. /// Enabling options like parallel execution and various graph transformations to enhance performance of ONNX. @@ -150,7 +150,7 @@ public VSharpOptions( bool releaseBranches = DefaultReleaseBranches, int randomSeed = DefaultRandomSeed, uint stepsLimit = DefaultStepsLimit, - AIAgentTrainingOptions aiAgentTrainingOptions = null, + AIOptions? aiOptions = null, string pathToModel = DefaultPathToModel, bool useGPU = false, bool optimize = false) @@ -167,7 +167,7 @@ public VSharpOptions( ReleaseBranches = releaseBranches; RandomSeed = randomSeed; StepsLimit = stepsLimit; - AIAgentTrainingOptions = aiAgentTrainingOptions; + AIOptions = aiOptions; PathToModel = pathToModel; UseGPU = useGPU; Optimize = optimize; diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs index b02f9a423..3e99b67bb 100644 --- a/VSharp.Explorer/AISearcher.fs +++ b/VSharp.Explorer/AISearcher.fs @@ -2,20 +2,31 @@ namespace VSharp.Explorer open System.Collections.Generic open Microsoft.ML.OnnxRuntime +open System +open System.Text +open System.Text.Json open VSharp open VSharp.IL.Serializer open VSharp.ML.GameServer.Messages -type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option) = +type AIMode = + | Runner + | TrainingSendModel + | TrainingSendEachStep + + +type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option) = let stepsToSwitchToAI = - match aiAgentTrainingOptions with + match aiAgentTrainingMode with | None -> 0u - | Some options -> options.stepsToSwitchToAI + | Some(SendModel options) -> options.aiAgentTrainingOptions.stepsToSwitchToAI + | Some(SendEachStep options) -> options.aiAgentTrainingOptions.stepsToSwitchToAI let stepsToPlay = - match aiAgentTrainingOptions with + match aiAgentTrainingMode with | None -> 0u - | Some options -> options.stepsToPlay + | Some(SendModel options) -> options.aiAgentTrainingOptions.stepsToPlay + | Some(SendEachStep options) -> options.aiAgentTrainingOptions.stepsToPlay let mutable lastCollectedStatistics = Statistics() let mutable defaultSearcherSteps = 0u @@ -25,14 +36,17 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option BFSSearcher() :> IForwardSearcher - | Some options -> - match options.defaultSearchStrategy with + let pickSearcher = + function | BFSMode -> BFSSearcher() :> IForwardSearcher | DFSMode -> DFSSearcher() :> IForwardSearcher | x -> failwithf $"Unexpected default searcher {x}. DFS and BFS supported for now." + match aiAgentTrainingMode with + | None -> BFSSearcher() :> IForwardSearcher + | Some(SendModel options) -> pickSearcher options.aiAgentTrainingOptions.aiBaseOptions.defaultSearchStrategy + | Some(SendEachStep options) -> pickSearcher options.aiAgentTrainingOptions.aiBaseOptions.defaultSearchStrategy + let mutable stepsPlayed = 0u let isInAIMode () = @@ -41,59 +55,6 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option() let availableStates = HashSet<_>() - let updateGameState (delta: GameState) = - match gameState with - | None -> gameState <- Some delta - | Some s -> - let updatedBasicBlocks = delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet - let updatedStates = delta.States |> Array.map (fun s -> s.Id) |> HashSet - - let vertices = - s.GraphVertices - |> Array.filter (fun v -> updatedBasicBlocks.Contains v.Id |> not) - |> ResizeArray<_> - - vertices.AddRange delta.GraphVertices - - let edges = - s.Map - |> Array.filter (fun e -> updatedBasicBlocks.Contains e.VertexFrom |> not) - |> ResizeArray<_> - - edges.AddRange delta.Map - let activeStates = vertices |> Seq.collect (fun v -> v.States) |> HashSet - - let states = - let part1 = - s.States - |> Array.filter (fun s -> activeStates.Contains s.Id && (not <| updatedStates.Contains s.Id)) - |> ResizeArray<_> - - part1.AddRange delta.States - - part1.ToArray() - |> Array.map (fun s -> - State( - s.Id, - s.Position, - s.PathCondition, - s.VisitedAgainVertices, - s.VisitedNotCoveredVerticesInZone, - s.VisitedNotCoveredVerticesOutOfZone, - s.StepWhenMovedLastTime, - s.InstructionsVisitedInCurrentBlock, - s.History, - s.Children |> Array.filter activeStates.Contains - )) - - let pathConditionVertices = - ResizeArray s.PathConditionVertices - - pathConditionVertices.AddRange delta.PathConditionVertices - - gameState <- - Some - <| GameState(vertices.ToArray(), states, pathConditionVertices.ToArray(), edges.ToArray()) let init states = @@ -128,15 +89,18 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option ignore - let inTrainMode = aiAgentTrainingOptions.IsSome - + let aiMode = + match aiAgentTrainingMode with + | Some(SendEachStep _) -> TrainingSendEachStep + | Some(SendModel _) -> TrainingSendModel + | None -> Runner let pick selector = if useDefaultSearcher then defaultSearcherSteps <- defaultSearcherSteps + 1u if Seq.length availableStates > 0 then let gameStateDelta = collectGameStateDelta () - updateGameState gameStateDelta + gameState <- AISearcher.updateGameState gameStateDelta gameState let statistics = computeStatistics gameState.Value Application.applicationGraphDelta.Clear() lastCollectedStatistics <- statistics @@ -149,7 +113,7 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option 0u then - gameStateDelta - else - gameState.Value + match aiMode with + | TrainingSendEachStep + | TrainingSendModel -> + if stepsPlayed > 0u then + gameStateDelta + else + gameState.Value + | Runner -> gameState.Value let stateId = oracle.Predict toPredict afterFirstAIPeek <- true @@ -179,13 +147,77 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option) = + match gameState with + | None -> Some delta + | Some s -> + let updatedBasicBlocks = delta.GraphVertices |> Array.map (fun b -> b.Id) |> HashSet + let updatedStates = delta.States |> Array.map (fun s -> s.Id) |> HashSet + + let vertices = + s.GraphVertices + |> Array.filter (fun v -> updatedBasicBlocks.Contains v.Id |> not) + |> ResizeArray<_> + + vertices.AddRange delta.GraphVertices + + let edges = + s.Map + |> Array.filter (fun e -> updatedBasicBlocks.Contains e.VertexFrom |> not) + |> ResizeArray<_> + + edges.AddRange delta.Map + let activeStates = vertices |> Seq.collect (fun v -> v.States) |> HashSet + + let states = + let part1 = + s.States + |> Array.filter (fun s -> activeStates.Contains s.Id && (not <| updatedStates.Contains s.Id)) + |> ResizeArray<_> + + part1.AddRange delta.States + + part1.ToArray() + |> Array.map (fun s -> + State( + s.Id, + s.Position, + s.PathCondition, + s.VisitedAgainVertices, + s.VisitedNotCoveredVerticesInZone, + s.VisitedNotCoveredVerticesOutOfZone, + s.StepWhenMovedLastTime, + s.InstructionsVisitedInCurrentBlock, + s.History, + s.Children |> Array.filter activeStates.Contains + )) + + let pathConditionVertices = ResizeArray s.PathConditionVertices - new(pathToONNX: string, useGPU: bool, optimize: bool) = + pathConditionVertices.AddRange delta.PathConditionVertices + + Some + <| GameState(vertices.ToArray(), states, pathConditionVertices.ToArray(), edges.ToArray()) + + static member convertOutputToJson (output: IDisposableReadOnlyCollection) = + seq { 0 .. output.Count - 1 } + |> Seq.map (fun i -> output[i].GetTensorDataAsSpan().ToArray()) + + + + new + ( + pathToONNX: string, + useGPU: bool, + optimize: bool, + aiAgentTrainingModelOptions: Option + ) = let numOfVertexAttributes = 7 let numOfStateAttributes = 7 let numOfHistoryEdgeAttributes = 2 - let createOracle (pathToONNX: string) = + + let createOracleRunner (pathToONNX: string, aiAgentTrainingModelOptions: Option) = let sessionOptions = if useGPU then SessionOptions.MakeSessionOptionWithCudaProvider(0) @@ -199,10 +231,21 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option + currentGameState <- AISearcher.updateGameState gameStateOrDelta currentGameState + | _ -> currentGameState <- Some gameStateOrDelta + + let gameState = currentGameState.Value let stateIds = Dictionary, int>() let verticesIds = Dictionary, int>() @@ -243,7 +286,7 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option + aiAgentOptions.stepSaver ( + AIGameStep(gameState = gameStateOrDelta, output = AISearcher.convertOutputToJson output) + ) + | None -> () + + stepsPlayed <- stepsPlayed + 1 + let weighedStates = output[0].GetTensorDataAsSpan().ToArray() let id = weighedStates |> Array.mapi (fun i v -> i, v) |> Array.maxBy snd |> fst @@ -357,7 +411,12 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingOptions: Option Some(SendModel aiAgentTrainingModelOptions) + | None -> None + + AISearcher(createOracleRunner (pathToONNX, aiAgentTrainingModelOptions), aiAgentTrainingOptions) interface IForwardSearcher with override x.Init states = init states diff --git a/VSharp.Explorer/Explorer.fs b/VSharp.Explorer/Explorer.fs index ec79cca9f..bfd2951b4 100644 --- a/VSharp.Explorer/Explorer.fs +++ b/VSharp.Explorer/Explorer.fs @@ -12,7 +12,6 @@ open VSharp.Core open VSharp.Interpreter.IL open CilState open VSharp.Explorer -open VSharp.ML.GameServer.Messages open VSharp.Solver open VSharp.IL.Serializer @@ -25,19 +24,16 @@ type IReporter = type EntryPointConfiguration(mainArguments: string[]) = - member x.Args = - if mainArguments = null then None else Some mainArguments + member x.Args = if mainArguments = null then None else Some mainArguments type WebConfiguration (mainArguments: string[], environmentName: string, contentRootPath: DirectoryInfo, applicationName: string) = inherit EntryPointConfiguration(mainArguments) - member internal x.ToWebConfig () = - { - environmentName = environmentName - contentRootPath = contentRootPath - applicationName = applicationName - } + member internal x.ToWebConfig() = + { environmentName = environmentName + contentRootPath = contentRootPath + applicationName = applicationName } type private IExplorer = abstract member Reset: seq -> unit @@ -45,15 +41,18 @@ type private IExplorer = type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVMStatistics, reporter: IReporter) = let options = explorationOptions.svmOptions + let folderToStoreSerializationResult = - match options.aiAgentTrainingOptions with - | None -> "" - | Some options -> + match options.aiOptions with + | Some(DatasetGenerator aiOptions) -> + let mapName = aiOptions.mapName + getFolderToStoreSerializationResult (Path.GetDirectoryName explorationOptions.outputDirectory.FullName) - options.mapName - let hasTimeout = - explorationOptions.timeout.TotalMilliseconds > 0 + mapName + | _ -> "" + + let hasTimeout = explorationOptions.timeout.TotalMilliseconds > 0 let solverTimeout = if options.solverTimeout > 0 then @@ -67,8 +66,8 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM -1 let branchReleaseTimeout = - let doubleTimeout = - double explorationOptions.timeout.TotalMilliseconds + let doubleTimeout = double explorationOptions.timeout.TotalMilliseconds + if not hasTimeout then Double.PositiveInfinity elif not options.releaseBranches then doubleTimeout else doubleTimeout * 80.0 / 100.0 @@ -76,29 +75,29 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM let hasStepsLimit = options.stepsLimit > 0u do - API.ConfigureSolver (SolverPool.mkSolver (solverTimeout)) - VSharp.System.SetUp.ConfigureInternalCalls () - API.ConfigureChars (options.prettyChars) + API.ConfigureSolver(SolverPool.mkSolver (solverTimeout)) + VSharp.System.SetUp.ConfigureInternalCalls() + API.ConfigureChars(options.prettyChars) let mutable branchesReleased = false let mutable isStopped = false - let mutable isCoverageAchieved: unit -> bool = - always false + let mutable isCoverageAchieved: unit -> bool = always false - let emptyState = - Memory.EmptyIsolatedState () - let interpreter = ILInterpreter () + let emptyState = Memory.EmptyIsolatedState() + let interpreter = ILInterpreter() do if options.visualize then DotVisualizer explorationOptions.outputDirectory :> IVisualizer |> Application.setVisualizer + SetMaxBuferSize options.maxBufferSize TestGenerator.setMaxBufferSize options.maxBufferSize let isSat pc = // TODO: consider trivial cases emptyState.pc <- pc + match SolverInteraction.checkSat emptyState with | SolverInteraction.SmtSat _ | SolverInteraction.SmtUnknown _ -> true @@ -110,72 +109,78 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM None else Some options.randomSeed + match mode with | AIMode -> - match options.aiAgentTrainingOptions with + let useGPU = options.useGPU + let optimize = options.optimize + + match options.aiOptions with | Some aiOptions -> - match aiOptions.oracle with - | Some oracle -> AISearcher (oracle, options.aiAgentTrainingOptions) :> IForwardSearcher - | None -> failwith "Empty oracle for AI searcher." + match aiOptions with + | Training aiAgentTrainingOptions -> + match aiAgentTrainingOptions with + | SendEachStep aiAgentTrainingEachStepOptions -> + match aiAgentTrainingEachStepOptions.aiAgentTrainingOptions.oracle with + | Some oracle -> AISearcher(oracle, Some aiAgentTrainingOptions) :> IForwardSearcher + | None -> failwith "Empty oracle for AI searcher training (send each step mode)." + | SendModel aiAgentTrainingModelOptions -> + match options.pathToModel with + | Some path -> + AISearcher(path, useGPU, optimize, Some aiAgentTrainingModelOptions) :> IForwardSearcher + | None -> failwith "Empty model for AI searcher training (send model mode)." + | DatasetGenerator aiOptions -> mkForwardSearcher aiOptions.defaultSearchStrategy | None -> match options.pathToModel with - | Some s -> - let useGPU = - options.useGPU.IsSome && options.useGPU.Value - let optimize = - options.optimize.IsSome && options.optimize.Value - AISearcher (s, useGPU, optimize) + | Some s -> AISearcher(s, useGPU, optimize, None) | None -> failwith "Empty model for AI searcher." - | BFSMode -> BFSSearcher () :> IForwardSearcher - | DFSMode -> DFSSearcher () :> IForwardSearcher + | BFSMode -> BFSSearcher() :> IForwardSearcher + | DFSMode -> DFSSearcher() :> IForwardSearcher | ShortestDistanceBasedMode -> ShortestDistanceBasedSearcher statistics :> IForwardSearcher | RandomShortestDistanceBasedMode -> - RandomShortestDistanceBasedSearcher (statistics, getRandomSeedOption ()) :> IForwardSearcher + RandomShortestDistanceBasedSearcher(statistics, getRandomSeedOption ()) :> IForwardSearcher | ContributedCoverageMode -> DFSSortedByContributedCoverageSearcher statistics :> IForwardSearcher - | ExecutionTreeMode -> ExecutionTreeSearcher (getRandomSeedOption ()) + | ExecutionTreeMode -> ExecutionTreeSearcher(getRandomSeedOption ()) | FairMode baseMode -> - FairSearcher ((fun _ -> mkForwardSearcher baseMode), uint branchReleaseTimeout, statistics) + FairSearcher((fun _ -> mkForwardSearcher baseMode), uint branchReleaseTimeout, statistics) :> IForwardSearcher - | InterleavedMode (base1, stepCount1, base2, stepCount2) -> - InterleavedSearcher ( - [ - mkForwardSearcher base1, stepCount1 - mkForwardSearcher base2, stepCount2 - ] - ) + | InterleavedMode(base1, stepCount1, base2, stepCount2) -> + InterleavedSearcher([ mkForwardSearcher base1, stepCount1; mkForwardSearcher base2, stepCount2 ]) :> IForwardSearcher let mutable searcher: IBidirectionalSearcher = match options.explorationMode with - | TestCoverageMode (_, searchMode) -> + | TestCoverageMode(_, searchMode) -> let baseSearcher = if options.recThreshold > 0u then - GuidedSearcher ( + GuidedSearcher( mkForwardSearcher searchMode, - RecursionBasedTargetManager (statistics, options.recThreshold) + RecursionBasedTargetManager(statistics, options.recThreshold) ) :> IForwardSearcher else mkForwardSearcher searchMode - BidirectionalSearcher (baseSearcher, BackwardSearcher (), DummyTargetedSearcher.DummyTargetedSearcher ()) + + BidirectionalSearcher(baseSearcher, BackwardSearcher(), DummyTargetedSearcher.DummyTargetedSearcher()) :> IBidirectionalSearcher | StackTraceReproductionMode _ -> __notImplemented__ () let releaseBranches () = if not branchesReleased then branchesReleased <- true - statistics.OnBranchesReleased () - ReleaseBranches () + statistics.OnBranchesReleased() + ReleaseBranches() + let dfsSearcher = DFSSortedByContributedCoverageSearcher statistics :> IForwardSearcher - let bidirectionalSearcher = - OnlyForwardSearcher (dfsSearcher) - dfsSearcher.Init <| searcher.States () + + let bidirectionalSearcher = OnlyForwardSearcher(dfsSearcher) + dfsSearcher.Init <| searcher.States() searcher <- bidirectionalSearcher let reportStateIncomplete (state: cilState) = searcher.Remove state - statistics.IncompleteStates.Add (state) + statistics.IncompleteStates.Add(state) Application.terminateState state reporter.ReportIIE state.iie.Value @@ -184,33 +189,38 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM let reportState (suite: testSuite) (cilState: cilState) = try let isNewHistory () = - let methodHistory = - Set.filter (fun h -> h.method.InCoverageZone) cilState.history + let methodHistory = Set.filter (fun h -> h.method.InCoverageZone) cilState.history + Set.isEmpty methodHistory || Set.exists (not << statistics.IsBasicBlockCoveredByTest) methodHistory + let isError = suite.IsErrorSuite + let isNewTest = match suite with | Test -> isNewHistory () - | Error (msg, isFatal) -> statistics.IsNewError cilState msg isFatal + | Error(msg, isFatal) -> statistics.IsNewError cilState msg isFatal + if isNewTest then let state = cilState.state - let callStackSize = - Memory.CallStackSize state + let callStackSize = Memory.CallStackSize state let entryMethod = cilState.EntryMethod - let hasException = - cilState.IsUnhandledException + let hasException = cilState.IsUnhandledException + if isError && not hasException then if entryMethod.HasParameterOnStack then Memory.ForcePopFrames (callStackSize - 2) state else Memory.ForcePopFrames (callStackSize - 1) state + match TestGenerator.state2test suite entryMethod state with | Some test -> - statistics.TrackFinished (cilState, isError) + statistics.TrackFinished(cilState, isError) + match suite with | Test -> reporter.ReportFinished test | Error _ -> reporter.ReportException test + if isCoverageAchieved () then isStopped <- true | None -> () @@ -223,10 +233,11 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM | :? InsufficientInformationException as e -> if not state.IsIIEState then state.SetIIE e + reportStateIncomplete state | _ -> searcher.Remove state - statistics.InternalFails.Add (e) + statistics.InternalFails.Add(e) Application.terminateState state reporter.ReportInternalFail state.EntryMethod e @@ -234,7 +245,7 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM match e with | :? InsufficientInformationException as e -> reportIncomplete e | _ -> - statistics.InternalFails.Add (e) + statistics.InternalFails.Add(e) reporter.ReportInternalFail method e let reportFinished (state: cilState) = @@ -246,9 +257,9 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM let wrapOnError isFatal (state: cilState) errorMessage = if not <| String.IsNullOrWhiteSpace errorMessage then Logger.info $"Error in {state.EntryMethod.FullName}: {errorMessage}" + Application.terminateState state - let testSuite = - Error (errorMessage, isFatal) + let testSuite = Error(errorMessage, isFatal) reportState testSuite state let reportError = wrapOnError false @@ -259,55 +270,66 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM hasTimeout && statistics.CurrentExplorationTime.TotalMilliseconds >= explorationOptions.timeout.TotalMilliseconds + let shouldReleaseBranches () = options.releaseBranches && statistics.CurrentExplorationTime.TotalMilliseconds >= branchReleaseTimeout + let isStepsLimitReached () = hasStepsLimit && statistics.StepsCount >= options.stepsLimit static member private AllocateByRefParameters initialState (method: Method) = let allocateIfByRef (pi: ParameterInfo) = let parameterType = pi.ParameterType + if parameterType.IsByRef then if Memory.CallStackSize initialState = 0 then Memory.NewStackFrame initialState None [] - let typ = parameterType.GetElementType () + + let typ = parameterType.GetElementType() let position = pi.Position + 1 + let stackRef = Memory.AllocateTemporaryLocalVariableOfType initialState pi.Name position typ + Some stackRef else None + method.Parameters |> Array.map allocateIfByRef |> Array.toList - member private x.FormIsolatedInitialStates (method: Method, initialState: state) = + member private x.FormIsolatedInitialStates(method: Method, initialState: state) = try initialState.model <- Memory.EmptyModel method let declaringType = method.DeclaringType - let cilState = - cilState.CreateInitial method initialState + let cilState = cilState.CreateInitial method initialState + let this = if method.HasThis then if Types.IsValueType declaringType then Memory.NewStackFrame initialState None [] + Memory.AllocateTemporaryLocalVariableOfType initialState "this" 0 declaringType |> Some else - let this = - Memory.MakeSymbolicThis initialState method + let this = Memory.MakeSymbolicThis initialState method !!(IsNullReference this) |> AddConstraint initialState Some this else None - let parameters = - SVMExplorer.AllocateByRefParameters initialState method + + let parameters = SVMExplorer.AllocateByRefParameters initialState method Memory.InitFunctionFrame initialState method this (Some parameters) + match this with | Some this -> SolveThisType initialState this | _ -> () + let cilStates = ILInterpreter.CheckDisallowNullAttribute method None cilState false id + assert (List.length cilStates = 1) + if not method.IsStaticConstructor then let cilState = List.head cilStates interpreter.InitializeStatics cilState declaringType List.singleton @@ -325,78 +347,85 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM assert method.IsStatic let optionArgs = config.Args let parameters = method.Parameters - let hasParameters = - Array.length parameters > 0 + let hasParameters = Array.length parameters > 0 + let state = { initialState with - complete = not hasParameters || Option.isSome optionArgs - } + complete = not hasParameters || Option.isSome optionArgs } + state.model <- Memory.EmptyModel method + match optionArgs with | Some args -> let stringType = typeof let argsNumber = MakeNumber args.Length - let argsRef = - Memory.AllocateConcreteVectorArray state argsNumber stringType args - let args = - Some (List.singleton (Some argsRef)) + let argsRef = Memory.AllocateConcreteVectorArray state argsNumber stringType args + let args = Some(List.singleton (Some argsRef)) Memory.InitFunctionFrame state method None args | None when hasParameters -> Memory.InitFunctionFrame state method None None // NOTE: if args are symbolic, constraint 'args != null' is added assert (Array.length parameters = 1) let argsParameter = Array.head parameters - let argsParameterTerm = - Memory.ReadArgument state argsParameter + let argsParameterTerm = Memory.ReadArgument state argsParameter AddConstraint state (!!(IsNullReference argsParameterTerm)) // Filling model with default args to match PC let modelState = match state.model with | StateModel modelState -> modelState | _ -> __unreachable__ () + let argsForModel = Memory.AllocateVectorArray modelState (MakeNumber 0) typeof + Memory.WriteStackLocation modelState (ParameterKey argsParameter) argsForModel | None -> let args = Some List.empty Memory.InitFunctionFrame state method None args + Memory.InitializeStaticMembers state method.DeclaringType + let initialState = match config with | :? WebConfiguration as config -> - let webConfig = config.ToWebConfig () + let webConfig = config.ToWebConfig() cilState.CreateWebInitial method state webConfig | _ -> cilState.CreateInitial method state + [ initialState ] with e -> reportInternalFail method e [] - member private x.Forward (s: cilState) = + member private x.Forward(s: cilState) = let loc = s.approximateLoc let ip = s.CurrentIp let stackSize = s.StackSize // TODO: update pobs when visiting new methods; use coverageZone - let goodStates, iieStates, errors = - interpreter.ExecuteOneInstruction s + let goodStates, iieStates, errors = interpreter.ExecuteOneInstruction s + for s in goodStates @ iieStates @ errors do if not s.HasRuntimeExceptionOrError then statistics.TrackStepForward s ip stackSize + let goodStates, toReportFinished = goodStates |> List.partition (fun s -> s.IsExecutable || s.IsIsolated) + toReportFinished |> List.iter reportFinished - let errors, _ = - errors |> List.partition (fun s -> not s.HasReportedError) + let errors, _ = errors |> List.partition (fun s -> not s.HasReportedError) + let errors, toReportExceptions = errors |> List.partition (fun s -> s.IsIsolated || not s.IsStoppedByException) + let runtimeExceptions, userExceptions = toReportExceptions |> List.partition (fun s -> s.HasRuntimeExceptionOrError) + runtimeExceptions |> List.iter (fun state -> reportError state null) userExceptions |> List.iter reportFinished - let iieStates, toReportIIE = - iieStates |> List.partition (fun s -> s.IsIsolated) + let iieStates, toReportIIE = iieStates |> List.partition (fun s -> s.IsIsolated) toReportIIE |> List.iter reportStateIncomplete let mutable sIsStopped = false + let newStates = match goodStates, iieStates, errors with | s' :: goodStates, _, _ when LanguagePrimitives.PhysicalEquality s s' -> goodStates @ iieStates @ errors @@ -410,36 +439,39 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM Application.moveState loc s (Seq.cast<_> newStates) statistics.TrackFork s newStates searcher.UpdateStates s newStates + if sIsStopped then searcher.Remove s member private x.Backward p' (s': cilState) = assert (s'.CurrentLoc = p'.loc) let sLvl = s'.Level + if p'.lvl >= sLvl then let lvl = p'.lvl - sLvl let pc = Memory.WLP s'.state p'.pc + match isSat pc with | true when not s'.IsIsolated -> searcher.Answer p' (Witnessed s') | true -> statistics.TrackStepBackward p' s' + let p = - { - loc = s'.StartingLoc - lvl = lvl - pc = pc - } + { loc = s'.StartingLoc + lvl = lvl + pc = pc } + Logger.trace $"Backward:\nWas: {p'}\nNow: {p}\n\n" Application.addGoal p.loc searcher.UpdatePobs p' p | false -> Logger.trace "UNSAT for pob = %O and s'.PC = %s" p' (API.Print.PrintPC s'.state.pc) - member private x.BidirectionalSymbolicExecution () = + member private x.BidirectionalSymbolicExecution() = let mutable action = Stop let pick () = - match searcher.Pick () with + match searcher.Pick() with | Stop -> false | a -> action <- a @@ -452,21 +484,26 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM && pick () do if shouldReleaseBranches () then releaseBranches () + match action with | GoFront s -> try - if - options.aiAgentTrainingOptions.IsSome - && options.aiAgentTrainingOptions.Value.serializeSteps - then + let needToSerialize = + match options.aiOptions with + | Some(DatasetGenerator _) -> true + | _ -> false + + if needToSerialize then dumpGameState - (Path.Combine (folderToStoreSerializationResult, string firstFreeEpisodeNumber)) + (Path.Combine(folderToStoreSerializationResult, string firstFreeEpisodeNumber)) s.internalId + firstFreeEpisodeNumber <- firstFreeEpisodeNumber + 1 - x.Forward (s) + + x.Forward(s) with e -> reportStateInternalFail s e - | GoBack (s, p) -> + | GoBack(s, p) -> try x.Backward p s with e -> @@ -474,43 +511,46 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM | Stop -> __unreachable__ () member private x.AnswerPobs initialStates = - statistics.ExplorationStarted () + statistics.ExplorationStarted() // For backward compatibility. TODO: remove main pobs at all let mainPobs = [] Application.spawnStates (Seq.cast<_> initialStates) mainPobs |> Seq.map (fun pob -> pob.loc) |> Seq.toArray |> Application.addGoals searcher.Init initialStates mainPobs + initialStates |> Seq.filter (fun s -> s.IsIIEState) |> Seq.iter reportStateIncomplete - x.BidirectionalSymbolicExecution () - searcher.Statuses () + + x.BidirectionalSymbolicExecution() + + searcher.Statuses() |> Seq.iter (fun (pob, status) -> match status with | pobStatus.Unknown -> Logger.warning $"Unknown status for pob at {pob.loc}" - | _ -> () - ) + | _ -> ()) interface IExplorer with member x.Reset entryMethods = HashMap.clear () - API.Reset () + API.Reset() SolverPool.reset () - searcher.Reset () + searcher.Reset() isStopped <- false branchesReleased <- false SolverInteraction.setOnSolverStarted statistics.SolverStarted SolverInteraction.setOnSolverStopped statistics.SolverStopped - AcquireBranches () + AcquireBranches() isCoverageAchieved <- always false + match options.explorationMode with | TestCoverageMode _ -> if options.stopOnCoverageAchieved > 0 then let checkCoverage () = - let cov = - statistics.GetCurrentCoverage entryMethods + let cov = statistics.GetCurrentCoverage entryMethods cov >= options.stopOnCoverageAchieved + isCoverageAchieved <- checkCoverage | StackTraceReproductionMode _ -> __notImplemented__ () @@ -519,30 +559,33 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM try try interpreter.ConfigureErrorReporter reportError reportFatalError - let isolatedInitialStates = - isolated |> List.collect x.FormIsolatedInitialStates + let isolatedInitialStates = isolated |> List.collect x.FormIsolatedInitialStates + let entryPointsInitialStates = entryPoints |> List.collect x.FormEntryPointInitialStates + let iieStates, initialStates = isolatedInitialStates @ entryPointsInitialStates |> List.partition (fun state -> state.IsIIEState) + iieStates |> List.iter reportStateIncomplete - statistics.SetStatesGetter (fun () -> searcher.States ()) - statistics.SetStatesCountGetter (fun () -> searcher.StatesCount) + statistics.SetStatesGetter(fun () -> searcher.States()) + statistics.SetStatesCountGetter(fun () -> searcher.StatesCount) + if not initialStates.IsEmpty then x.AnswerPobs initialStates with e -> reportCrash e finally try - statistics.ExplorationFinished () - API.Restore () - searcher.Reset () + statistics.ExplorationFinished() + API.Restore() + searcher.Reset() with e -> reportCrash e } - member private x.Stop () = isStopped <- true + member private x.Stop() = isStopped <- true type private FuzzerExplorer(explorationOptions: ExplorationOptions, statistics: SVMStatistics) = @@ -551,22 +594,19 @@ type private FuzzerExplorer(explorationOptions: ExplorationOptions, statistics: member x.Reset _ = () member x.StartExploration isolated _ = - let saveStatistic = - statistics.SetBasicBlocksAsCoveredByTest - let outputDir = - explorationOptions.outputDirectory.FullName - let cancellationTokenSource = - new CancellationTokenSource () - cancellationTokenSource.CancelAfter (explorationOptions.timeout) - let methodsBase = - isolated |> List.map (fun (m, _) -> (m :> IMethod).MethodBase) + let saveStatistic = statistics.SetBasicBlocksAsCoveredByTest + let outputDir = explorationOptions.outputDirectory.FullName + let cancellationTokenSource = new CancellationTokenSource() + cancellationTokenSource.CancelAfter(explorationOptions.timeout) + let methodsBase = isolated |> List.map (fun (m, _) -> (m :> IMethod).MethodBase) + task { try - let targetAssemblyPath = - (Seq.head methodsBase).Module.Assembly.Location + let targetAssemblyPath = (Seq.head methodsBase).Module.Assembly.Location let onCancelled () = Logger.warning "Fuzzer canceled" + let interactor = - Fuzzer.Interactor ( + Fuzzer.Interactor( targetAssemblyPath, methodsBase, cancellationTokenSource.Token, @@ -574,7 +614,8 @@ type private FuzzerExplorer(explorationOptions: ExplorationOptions, statistics: saveStatistic, onCancelled ) - do! interactor.StartFuzzing () + + do! interactor.StartFuzzing() with e -> Logger.error $"Fuzzer unhandled exception: {e}" raise e @@ -582,20 +623,19 @@ type private FuzzerExplorer(explorationOptions: ExplorationOptions, statistics: type public Explorer(options: ExplorationOptions, reporter: IReporter) = - let statistics = - new SVMStatistics (Seq.empty, true) + let statistics = new SVMStatistics(Seq.empty, true) let explorers = let createFuzzer () = - FuzzerExplorer (options, statistics) :> IExplorer + FuzzerExplorer(options, statistics) :> IExplorer let createSVM () = - SVMExplorer (options, statistics, reporter) :> IExplorer + SVMExplorer(options, statistics, reporter) :> IExplorer match options.explorationModeOptions with | Fuzzing _ -> createFuzzer () |> Array.singleton | SVM _ -> createSVM () |> Array.singleton - | Combined _ -> [| createFuzzer () ; createSVM () |] + | Combined _ -> [| createFuzzer (); createSVM () |] let inCoverageZone coverageZone (entryMethods: Method list) = match coverageZone with @@ -610,33 +650,37 @@ type public Explorer(options: ExplorationOptions, reporter: IReporter) = |> List.exists (fun m -> method.Module.ModuleHandle = m.Module.ModuleHandle) member private x.TrySubstituteTypeParameters (state: state) (methodBase: MethodBase) = - let method = - Application.getMethod methodBase + let method = Application.getMethod methodBase + let getConcreteType = function | ConcreteType t -> Some t | _ -> None + try if method.ContainsGenericParameters then match SolveGenericMethodParameters state.typeStorage method with - | Some (classParams, methodParams) -> - let classParams = - classParams |> Array.choose getConcreteType - let methodParams = - methodParams |> Array.choose getConcreteType + | Some(classParams, methodParams) -> + let classParams = classParams |> Array.choose getConcreteType + let methodParams = methodParams |> Array.choose getConcreteType let declaringType = methodBase.DeclaringType + let isSuitableType () = not declaringType.IsGenericType || classParams.Length = declaringType.GetGenericArguments().Length + let isSuitableMethod () = methodBase.IsConstructor || not methodBase.IsGenericMethod || methodParams.Length = methodBase.GetGenericArguments().Length + if isSuitableType () && isSuitableMethod () then let reflectedType = Reflection.concretizeTypeParameters methodBase.ReflectedType classParams + let method = Reflection.concretizeMethodParameters reflectedType methodBase methodParams + Some method else None @@ -650,6 +694,7 @@ type public Explorer(options: ExplorationOptions, reporter: IReporter) = member x.Reset entryMethods = statistics.Reset entryMethods Application.setCoverageZone (inCoverageZone options.coverageZone entryMethods) + for explorer in explorers do explorer.Reset entryMethods @@ -660,20 +705,20 @@ type public Explorer(options: ExplorationOptions, reporter: IReporter) = try let trySubstituteTypeParameters method = - let emptyState = - Memory.EmptyIsolatedState () + let emptyState = Memory.EmptyIsolatedState() (Option.defaultValue method (x.TrySubstituteTypeParameters emptyState method), emptyState) + let isolated = isolated |> Seq.map trySubstituteTypeParameters |> Seq.map (fun (m, s) -> Application.getMethod m, s) |> Seq.toList + let entryPoints = entryPoints |> Seq.map (fun (m, a) -> let m, s = trySubstituteTypeParameters m - (Application.getMethod m, a, s) - ) + (Application.getMethod m, a, s)) |> Seq.toList (List.map fst isolated) @ (List.map (fun (x, _, _) -> x) entryPoints) |> x.Reset @@ -681,8 +726,7 @@ type public Explorer(options: ExplorationOptions, reporter: IReporter) = let explorationTasks = explorers |> Array.map (fun e -> e.StartExploration isolated entryPoints) - let finished = - Task.WaitAll (explorationTasks, options.timeout) + let finished = Task.WaitAll(explorationTasks, options.timeout) if not finished then Logger.warning "Exploration cancelled" @@ -699,4 +743,4 @@ type public Explorer(options: ExplorationOptions, reporter: IReporter) = member x.Statistics = statistics interface IDisposable with - member x.Dispose () = (statistics :> IDisposable).Dispose () + member x.Dispose() = (statistics :> IDisposable).Dispose() diff --git a/VSharp.Explorer/Options.fs b/VSharp.Explorer/Options.fs index 4b29327d8..1d94d900b 100644 --- a/VSharp.Explorer/Options.fs +++ b/VSharp.Explorer/Options.fs @@ -3,6 +3,8 @@ namespace VSharp.Explorer open System.Diagnostics open System.IO open VSharp.ML.GameServer.Messages +open System.Net.Sockets +open Microsoft.ML.OnnxRuntime type searchMode = | DFSMode @@ -27,20 +29,17 @@ type explorationMode = type fuzzerIsolation = | Process type FuzzerOptions = - { - isolation: fuzzerIsolation - coverageZone: coverageZone - } + { isolation: fuzzerIsolation + coverageZone: coverageZone } [] type Oracle = val Predict: GameState -> uint val Feedback: Feedback -> unit + new(predict, feedback) = - { - Predict = predict - Feedback = feedback - } + { Predict = predict + Feedback = feedback } /// /// Options used in AI agent training. @@ -51,34 +50,62 @@ type Oracle = /// Determine whether steps should be serialized. /// Name of map to play. /// Name of map to play. + +[] +type AIGameStep = + interface IRawOutgoingMessageBody + val GameState: GameState + val Output: seq> + + new(gameState, output) = + { GameState = gameState + Output = output } + + +type AIBaseOptions = + { defaultSearchStrategy: searchMode + mapName: string } + type AIAgentTrainingOptions = - { - stepsToSwitchToAI: uint - stepsToPlay: uint - defaultSearchStrategy: searchMode - serializeSteps: bool - mapName: string - oracle: Option - } + { aiBaseOptions: AIBaseOptions + stepsToSwitchToAI: uint + stepsToPlay: uint + oracle: option } + +type AIAgentTrainingEachStepOptions = + { aiAgentTrainingOptions: AIAgentTrainingOptions } + + +type AIAgentTrainingModelOptions = + { aiAgentTrainingOptions: AIAgentTrainingOptions + outputDirectory: string + stepSaver: AIGameStep -> Unit } + + +type AIAgentTrainingMode = + | SendEachStep of AIAgentTrainingEachStepOptions + | SendModel of AIAgentTrainingModelOptions + +type AIOptions = + | Training of AIAgentTrainingMode + | DatasetGenerator of AIBaseOptions type SVMOptions = - { - explorationMode: explorationMode - recThreshold: uint - solverTimeout: int - visualize: bool - releaseBranches: bool - maxBufferSize: int - prettyChars: bool // If true, 33 <= char <= 126, otherwise any char - checkAttributes: bool - stopOnCoverageAchieved: int - randomSeed: int - stepsLimit: uint - aiAgentTrainingOptions: Option - pathToModel: Option - useGPU: Option - optimize: Option - } + { explorationMode: explorationMode + recThreshold: uint + solverTimeout: int + visualize: bool + releaseBranches: bool + maxBufferSize: int + prettyChars: bool // If true, 33 <= char <= 126, otherwise any char + checkAttributes: bool + stopOnCoverageAchieved: int + randomSeed: int + stepsLimit: uint + aiOptions: Option + pathToModel: Option + useGPU: bool + optimize: bool } type explorationModeOptions = | Fuzzing of FuzzerOptions @@ -86,29 +113,27 @@ type explorationModeOptions = | Combined of SVMOptions * FuzzerOptions type ExplorationOptions = - { - timeout: System.TimeSpan - outputDirectory: DirectoryInfo - explorationModeOptions: explorationModeOptions - } + { timeout: System.TimeSpan + outputDirectory: DirectoryInfo + explorationModeOptions: explorationModeOptions } member this.fuzzerOptions = match this.explorationModeOptions with | Fuzzing x -> x - | Combined (_, x) -> x + | Combined(_, x) -> x | _ -> failwith "" member this.svmOptions = match this.explorationModeOptions with | SVM x -> x - | Combined (x, _) -> x + | Combined(x, _) -> x | _ -> failwith "" member this.coverageZone = match this.explorationModeOptions with | SVM x -> match x.explorationMode with - | TestCoverageMode (coverageZone, _) -> coverageZone + | TestCoverageMode(coverageZone, _) -> coverageZone | StackTraceReproductionMode _ -> failwith "" - | Combined (_, x) -> x.coverageZone + | Combined(_, x) -> x.coverageZone | Fuzzing x -> x.coverageZone diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs index 17ba5e6b7..3d04b64e2 100644 --- a/VSharp.ML.GameServer.Runner/Main.fs +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -1,4 +1,7 @@ +open System open System.IO +open System.Text.Json +open System.Net.Sockets open System.Reflection open Argu open Microsoft.FSharp.Core @@ -16,7 +19,6 @@ open VSharp.IL open VSharp.ML.GameServer.Messages open VSharp.Runner - [] type ExplorationResult = val ActualCoverage: uint @@ -33,6 +35,10 @@ type ExplorationResult = type Mode = | Server = 0 | Generator = 1 + | SendModel = 2 + +let TIMEOUT_FOR_TRAINING = 15 * 60 +let SOLVER_TIMEOUT_FOR_TRAINING = 2 type CliArguments = | [] Port of int @@ -41,8 +47,15 @@ type CliArguments = | [] Mode of Mode | [] OutFolder of string | [] StepsToSerialize of uint - | [] UseGPU - | [] Optimize + | [] Model of string + | [] StepsToPlay of uint + | [] DefaultSearcher of string + | [] StepsToStart of uint + | [] AssemblyFullName of string + | [] NameOfObjectToCover of string + | [] MapName of string + | [] UseGPU of bool + | [] Optimize of bool interface IArgParserTemplate with member s.Usage = @@ -55,14 +68,22 @@ type CliArguments = "Mode to run application. Server --- to train network, Generator --- to generate data for training." | OutFolder _ -> "Folder to store generated data." | StepsToSerialize _ -> "Maximal number of steps for each method to serialize." - | UseGPU -> "Specifies whether the ONNX execution session should use a CUDA-enabled GPU." - | Optimize -> + | Model _ -> """Path to ONNX model (use it for training in mode "SendModel")""" + | StepsToPlay _ -> """Steps required to play (after `StepsToStart` steps)""" + | DefaultSearcher _ -> """Defines the default searcher algorithm (BFS | DFS)""" + | StepsToStart _ -> """Steps required to start the game""" + | AssemblyFullName _ -> """Path to the DLL that contains the game map implementation""" + | NameOfObjectToCover _ -> """The name of the object that needs to be covered""" + | MapName _ -> """The name of the map used in the game""" + | UseGPU _ -> "Specifies whether the ONNX execution session should use a CUDA-enabled GPU." + | Optimize _ -> "Enabling options like parallel execution and various graph transformations to enhance performance of ONNX." let mutable inTrainMode = true let explore (gameMap: GameMap) options = let assembly = RunnerProgram.TryLoadAssembly <| FileInfo gameMap.AssemblyFullName + let method = RunnerProgram.ResolveMethod(assembly, gameMap.NameOfObjectToCover) let statistics = TestGenerator.Cover(method, options) @@ -92,7 +113,6 @@ let explore (gameMap: GameMap) options = statistics.StepsCount * 1u ) - let loadGameMaps (datasetDescriptionFilePath: string) = let jsonString = File.ReadAllText datasetDescriptionFilePath let maps = ResizeArray() @@ -181,25 +201,28 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = let stepsToPlay = gameMap.StepsToPlay let aiTrainingOptions = - { stepsToSwitchToAI = stepsToStart + { aiBaseOptions = + { defaultSearchStrategy = + match gameMap.DefaultSearcher with + | searcher.BFS -> BFSMode + | searcher.DFS -> DFSMode + | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." + mapName = gameMap.MapName } + stepsToSwitchToAI = stepsToStart stepsToPlay = stepsToPlay - defaultSearchStrategy = - match gameMap.DefaultSearcher with - | searcher.BFS -> BFSMode - | searcher.DFS -> DFSMode - | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." - serializeSteps = false - mapName = gameMap.MapName oracle = Some oracle } + let aiOptions: AIOptions = + (Training(SendEachStep { aiAgentTrainingOptions = aiTrainingOptions })) + let options = VSharpOptions( - timeout = 15 * 60, + timeout = TIMEOUT_FOR_TRAINING, outputDirectory = outputDirectory, searchStrategy = SearchStrategy.AI, - aiAgentTrainingOptions = aiTrainingOptions, + aiOptions = aiOptions, stepsLimit = uint (stepsToPlay + stepsToStart), - solverTimeout = 2 + solverTimeout = SOLVER_TIMEOUT_FOR_TRAINING ) let explorationResult = explore gameMap options @@ -231,6 +254,9 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = let app port outputDirectory : WebPart = choose [ path "/gameServer" >=> handShake (ws port outputDirectory) ] +let serializeExplorationResult (explorationResult: ExplorationResult) = + $"{explorationResult.ActualCoverage} {explorationResult.TestsCount} {explorationResult.StepsCount} {explorationResult.ErrorsCount}" + let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArray) stepsToSerialize = for map in maps do if map.StepsToStart = 0u then @@ -246,13 +272,9 @@ let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArra map.MapName ) - let aiTrainingOptions = - { stepsToSwitchToAI = 0u - stepsToPlay = 0u - defaultSearchStrategy = searchMode.BFSMode - serializeSteps = true - mapName = map.MapName - oracle = None } + let aiBaseOptions = + { defaultSearchStrategy = BFSMode + mapName = map.MapName } let options = VSharpOptions( @@ -261,7 +283,7 @@ let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArra searchStrategy = SearchStrategy.ExecutionTreeContributedCoverage, stepsLimit = stepsToSerialize, solverTimeout = 2, - aiAgentTrainingOptions = aiTrainingOptions + aiOptions = DatasetGenerator aiBaseOptions ) let folderForResults = @@ -274,10 +296,7 @@ let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArra let explorationResult = explore map options - File.WriteAllText( - Path.Join(folderForResults, "result"), - $"{explorationResult.ActualCoverage} {explorationResult.TestsCount} {explorationResult.StepsCount} {explorationResult.ErrorsCount}" - ) + File.WriteAllText(Path.Join(folderForResults, "result"), serializeExplorationResult explorationResult) printfn $"Generation for {map.MapName} finished with coverage {explorationResult.ActualCoverage}, tests {explorationResult.TestsCount}, steps {explorationResult.StepsCount},errors {explorationResult.ErrorsCount}." @@ -286,49 +305,110 @@ let generateDataForPretraining outputDirectory datasetBasePath (maps: ResizeArra API.Reset() HashMap.hashMap.Clear() +let runTrainingSendModelMode + outputDirectory + (gameMap: GameMap) + (pathToModel: string) + (useGPU: bool) + (optimize: bool) + (port: int) + = + printfn $"Run infer on {gameMap.MapName} have started." + let stepsToStart = gameMap.StepsToStart + let stepsToPlay = gameMap.StepsToPlay + + let aiTrainingOptions = + { aiBaseOptions = + { defaultSearchStrategy = + match gameMap.DefaultSearcher with + | searcher.BFS -> BFSMode + | searcher.DFS -> DFSMode + | x -> failwithf $"Unexpected searcher {x}. Use DFS or BFS for now." + + mapName = gameMap.MapName } + stepsToSwitchToAI = stepsToStart + stepsToPlay = stepsToPlay + oracle = None } + + let steps = ResizeArray() + let stepSaver (aiGameStep: AIGameStep) = steps.Add aiGameStep + + let aiOptions: AIOptions = + Training( + SendModel + { aiAgentTrainingOptions = aiTrainingOptions + outputDirectory = outputDirectory + stepSaver = stepSaver } + ) + + let options = + VSharpOptions( + timeout = TIMEOUT_FOR_TRAINING, + outputDirectory = outputDirectory, + searchStrategy = SearchStrategy.AI, + solverTimeout = SOLVER_TIMEOUT_FOR_TRAINING, + stepsLimit = uint (stepsToPlay + stepsToStart), + aiOptions = aiOptions, + pathToModel = pathToModel, + useGPU = useGPU, + optimize = optimize + ) + + let explorationResult = explore gameMap options + + File.WriteAllText( + Path.Join(outputDirectory, gameMap.MapName + "result"), + serializeExplorationResult explorationResult + ) + + printfn + $"Running for {gameMap.MapName} finished with coverage {explorationResult.ActualCoverage}, tests {explorationResult.TestsCount}, steps {explorationResult.StepsCount},errors {explorationResult.ErrorsCount}." + + let stream = + let host = "localhost" // TODO: working within a local network + let client = new TcpClient() + client.Connect(host, port) + client.SendBufferSize <- 4096 + client.GetStream() + + let needToSendSteps = + let buffer = Array.zeroCreate 1 + let bytesRead = stream.Read(buffer, 0, 1) + + if bytesRead = 0 then + failwith "Connection is closed?!" + + stream.Close() + buffer.[0] <> byte 0 + + if needToSendSteps then + File.WriteAllText(Path.Join(outputDirectory, gameMap.MapName + "_steps"), JsonSerializer.Serialize steps) + [] let main args = let parser = ArgumentParser.Create(programName = "VSharp.ML.GameServer.Runner.exe") let args = parser.Parse args - let mode = args.GetResult <@ Mode @> - let port = - match args.TryGetResult <@ Port @> with - | Some port -> port - | None -> 8100 - - let datasetBasePath = - match args.TryGetResult <@ DatasetBasePath @> with - | Some path -> path - | None -> "" - - let datasetDescription = - match args.TryGetResult <@ DatasetDescription @> with - | Some path -> path - | None -> "" - - let stepsToSerialize = - match args.TryGetResult <@ StepsToSerialize @> with - | Some steps -> steps - | None -> 500u - - let useGPU = (args.TryGetResult <@ UseGPU @>).IsSome + let port = args.GetResult(Port, defaultValue = 8100) - let optimize = (args.TryGetResult <@ Optimize @>).IsSome + let outputDirectory = + args.GetResult(OutFolder, defaultValue = Path.Combine(Directory.GetCurrentDirectory(), string port)) - let outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), string port) + let cleanOutputDirectory () = + if Directory.Exists outputDirectory then + Directory.Delete(outputDirectory, true) - if Directory.Exists outputDirectory then - Directory.Delete(outputDirectory, true) + Directory.CreateDirectory outputDirectory - let testsDirInfo = Directory.CreateDirectory outputDirectory printfn $"outputDir: {outputDirectory}" match mode with | Mode.Server -> + let _ = cleanOutputDirectory () + try startWebServer { defaultConfig with @@ -338,7 +418,46 @@ let main args = with e -> printfn $"Failed on port {port}" printfn $"{e.Message}" + | Mode.SendModel -> + let model = args.GetResult(Model, defaultValue = "models/model.onnx") + + let stepsToPlay = args.GetResult <@ StepsToPlay @> + + let defaultSearcher = + let s = args.GetResult <@ DefaultSearcher @> + let upperedS = String.map System.Char.ToUpper s + + match upperedS with + | "BFS" -> searcher.BFS + | "DFS" -> searcher.DFS + | _ -> failwith "Use BFS or DFS as a default searcher" + + let stepsToStart = args.GetResult <@ StepsToStart @> + let assemblyFullName = args.GetResult <@ AssemblyFullName @> + let nameOfObjectToCover = args.GetResult <@ NameOfObjectToCover @> + let mapName = args.GetResult <@ MapName @> + + let gameMap = + GameMap( + stepsToPlay = stepsToPlay, + stepsToStart = stepsToStart, + assemblyFullName = assemblyFullName, + defaultSearcher = defaultSearcher, + nameOfObjectToCover = nameOfObjectToCover, + mapName = mapName + ) + + let useGPU = args.GetResult(UseGPU, defaultValue = false) + let optimize = args.GetResult(Optimize, defaultValue = false) + + runTrainingSendModelMode outputDirectory gameMap model useGPU optimize port | Mode.Generator -> + let datasetDescription = args.GetResult <@ DatasetDescription @> + let datasetBasePath = args.GetResult <@ DatasetBasePath @> + let stepsToSerialize = args.GetResult(StepsToSerialize, defaultValue = 500u) + + + let _ = cleanOutputDirectory () let maps = loadGameMaps datasetDescription generateDataForPretraining outputDirectory datasetBasePath maps stepsToSerialize | x -> failwithf $"Unexpected mode {x}." diff --git a/VSharp.Test/Benchmarks/Benchmarks.cs b/VSharp.Test/Benchmarks/Benchmarks.cs index 12e022ffd..8e0f49aaa 100644 --- a/VSharp.Test/Benchmarks/Benchmarks.cs +++ b/VSharp.Test/Benchmarks/Benchmarks.cs @@ -93,10 +93,10 @@ public static BenchmarkResult Run( stopOnCoverageAchieved: -1, randomSeed: randomSeed, stepsLimit: stepsLimit, - aiAgentTrainingOptions: null, + aiOptions: null, pathToModel: null, - useGPU: null, - optimize: null + useGPU: false, + optimize: false ); var fuzzerOptions = new FuzzerOptions( diff --git a/VSharp.Test/IntegrationTests.cs b/VSharp.Test/IntegrationTests.cs index 00ed34653..6346f9a57 100644 --- a/VSharp.Test/IntegrationTests.cs +++ b/VSharp.Test/IntegrationTests.cs @@ -136,7 +136,6 @@ static TestSvmAttribute() private readonly bool _useGPU; private readonly bool _optimize; - public TestSvmAttribute( int expectedCoverage = -1, uint recThresholdForTest = 1u, @@ -176,10 +175,10 @@ public TestSvmAttribute( _fuzzerIsolation = fuzzerIsolation; _explorationMode = explorationMode; _randomSeed = randomSeed; + _stepsLimit = stepsLimit; _pathToModel = pathToModel; _useGPU = useGPU; _optimize = optimize; - _stepsLimit = stepsLimit; } public TestCommand Wrap(TestCommand command) @@ -200,10 +199,10 @@ public TestCommand Wrap(TestCommand command) _explorationMode, _randomSeed, _stepsLimit, + _hasExternMocking, _pathToModel, _useGPU, - _optimize, - _hasExternMocking + _optimize ); } @@ -263,10 +262,10 @@ public TestSvmCommand( ExplorationMode explorationMode, int randomSeed, uint stepsLimit, + bool hasExternMocking, string pathToModel, bool useGPU, - bool optimize, - bool hasExternMocking) : base(innerCommand) + bool optimize) : base(innerCommand) { _baseCoverageZone = coverageZone; _baseSearchStrat = TestContext.Parameters[SearchStrategyParameterName] == null ? @@ -478,7 +477,7 @@ private TestResult Explore(TestExecutionContext context) stopOnCoverageAchieved: _expectedCoverage ?? -1, randomSeed: _randomSeed, stepsLimit: _stepsLimit, - aiAgentTrainingOptions: null, + aiOptions: null, pathToModel: _pathToModel, useGPU: _useGPU, optimize: _optimize From 010e5fe91711c60ccfc36e1396ddb40bcabd0002 Mon Sep 17 00:00:00 2001 From: Anya Chistyakova <57658770+Anya497@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:48:18 +0300 Subject: [PATCH 09/12] Fix path condition collection (#94) * Save all path condition vertices in dumpGameState. * Reset id counter. * Fix problem with multiple ids for a single term. * Reset collections. Fix bug with missing vertices in GameState. * Replace Dictionary with HashSet. * Refactor. --- VSharp.Explorer/Explorer.fs | 2 + VSharp.IL/Serializer.fs | 73 ++++++++++++++++------------- VSharp.ML.GameServer.Runner/Main.fs | 2 + 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/VSharp.Explorer/Explorer.fs b/VSharp.Explorer/Explorer.fs index bfd2951b4..94eb5afef 100644 --- a/VSharp.Explorer/Explorer.fs +++ b/VSharp.Explorer/Explorer.fs @@ -498,6 +498,8 @@ type private SVMExplorer(explorationOptions: ExplorationOptions, statistics: SVM (Path.Combine(folderToStoreSerializationResult, string firstFreeEpisodeNumber)) s.internalId + pathConditionVertices.Clear() + resetPathConditionVertexIdCounter () firstFreeEpisodeNumber <- firstFreeEpisodeNumber + 1 x.Forward(s) diff --git a/VSharp.IL/Serializer.fs b/VSharp.IL/Serializer.fs index 182dd899a..81550267b 100644 --- a/VSharp.IL/Serializer.fs +++ b/VSharp.IL/Serializer.fs @@ -175,6 +175,7 @@ let calculateStateMetrics interproceduralGraphDistanceFrom (state: IGraphTrackab let mutable notVisitedBasicBlocksInZone = 0 let mutable notTouchedBasicBlocksInZone = 0 let basicBlocks = HashSet<_>() + currentBasicBlock.OutgoingEdges.Values |> Seq.iter basicBlocks.UnionWith basicBlocks @@ -348,41 +349,47 @@ let collectStatesInfoToDump (basicBlocks: ResizeArray) = statesInfoToDump -let getFirstFreePathConditionVertexId = +let getFirstFreePathConditionVertexId, resetPathConditionVertexIdCounter = let mutable count = 0u fun () -> let res = count count <- count + 1u res + , fun () -> count <- 0u -let pathConditionVertices = Dictionary() - -let collectPathCondition term = // TODO: Support other operations - let termsToVisit = - Stack> [| (term, getFirstFreePathConditionVertexId ()) |] +let pathConditionVertices = HashSet() +let termsWithId = Dictionary>() +let collectPathCondition + term + (termsWithId: Dictionary>) + (processedPathConditionVertices: HashSet) + = // TODO: Support other operations + let termsToVisit = Stack [| term |] let pathConditionDelta = ResizeArray() + let getIdForTerm term = + if termsWithId.ContainsKey term then + termsWithId.[term] + else + let newId = getFirstFreePathConditionVertexId () + termsWithId.Add(term, newId) + newId + + let handleChild term (children: ResizeArray<_>) = + children.Add(getIdForTerm term) + termsToVisit.Push term + while termsToVisit.Count > 0 do - let currentTerm, currentTermId = termsToVisit.Pop() + let currentTerm = termsToVisit.Pop() let markAsVisited (vertexType: pathConditionVertexType) children = - let newVertex = PathConditionVertex(currentTermId, vertexType, children) - pathConditionVertices.Add(currentTerm, newVertex) + let newVertex = PathConditionVertex(getIdForTerm currentTerm, vertexType, children) + processedPathConditionVertices.Add currentTerm |> ignore pathConditionDelta.Add newVertex - let handleTerm term (children: ResizeArray<_>) = - let termId = - if not <| pathConditionVertices.ContainsKey term then - getFirstFreePathConditionVertexId () - else - pathConditionVertices.[term].Id - - children.Add termId - termsToVisit.Push((term, termId)) - - if not <| pathConditionVertices.ContainsKey currentTerm then + if not <| processedPathConditionVertices.Contains currentTerm then match currentTerm.term with | Nop -> markAsVisited pathConditionVertexType.Nop [||] | Concrete(_, _) -> markAsVisited pathConditionVertexType.Constant [||] @@ -391,7 +398,7 @@ let collectPathCondition term = // TODO: Support other operations let children = ResizeArray>() for t in termList do - handleTerm t children + handleChild t children let children = children.ToArray() @@ -443,30 +450,30 @@ let collectPathCondition term = // TODO: Support other operations let children = ResizeArray>() for _, t in PersistentDict.toSeq fields do - handleTerm t children + handleChild t children markAsVisited pathConditionVertexType.Struct (children.ToArray()) | HeapRef(_, _) -> markAsVisited pathConditionVertexType.HeapRef [||] | Ref(_) -> markAsVisited pathConditionVertexType.Ref [||] | Ptr(_, _, t) -> let children = ResizeArray> [||] - handleTerm t children + handleChild t children markAsVisited pathConditionVertexType.Ptr (children.ToArray()) | Slice(t, listOfTuples) -> let children = ResizeArray> [||] - handleTerm t children + handleChild t children for t1, t2, t3, _ in listOfTuples do - handleTerm t1 children - handleTerm t2 children - handleTerm t3 children + handleChild t1 children + handleChild t2 children + handleChild t3 children markAsVisited pathConditionVertexType.Slice (children.ToArray()) | Ite(_) -> markAsVisited pathConditionVertexType.Ite [||] pathConditionDelta -let collectGameState (basicBlocks: ResizeArray) filterStates = +let collectGameState (basicBlocks: ResizeArray) filterStates processedPathConditionVertices termsWithId = let vertices = ResizeArray<_>() let allStates = HashSet<_>() @@ -486,13 +493,13 @@ let collectGameState (basicBlocks: ResizeArray) filterStates = let pathCondition = s.PathCondition |> PC.toSeq for term in pathCondition do - pathConditionDelta.AddRange(collectPathCondition term) + pathConditionDelta.AddRange(collectPathCondition term termsWithId processedPathConditionVertices) let pathConditionRoot = PathConditionVertex( id = getFirstFreePathConditionVertexId (), pathConditionVertexType = pathConditionVertexType.PathConditionRoot, - children = [| for p in pathCondition -> pathConditionVertices.[p].Id |] + children = [| for p in pathCondition -> termsWithId.[p] |] ) pathConditionDelta.Add pathConditionRoot @@ -556,7 +563,7 @@ let collectGameStateDelta () = let added = basicBlocks.Add(basicBlock) () - collectGameState (ResizeArray basicBlocks) false + collectGameState (ResizeArray basicBlocks) false pathConditionVertices termsWithId let dumpGameState fileForResultWithoutExtension (movedStateId: uint) = let basicBlocks = ResizeArray<_>() @@ -566,7 +573,9 @@ let dumpGameState fileForResultWithoutExtension (movedStateId: uint) = basicBlock.IsGoal <- method.Key.InCoverageZone basicBlocks.Add(basicBlock) - let gameState = collectGameState basicBlocks true + let gameState = + collectGameState basicBlocks true (HashSet()) (Dictionary>()) + let statesInfoToDump = collectStatesInfoToDump basicBlocks let gameStateJson = JsonSerializer.Serialize gameState let statesInfoJson = JsonSerializer.Serialize statesInfoToDump diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs index 3d04b64e2..1b4b9a207 100644 --- a/VSharp.ML.GameServer.Runner/Main.fs +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -231,6 +231,8 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = API.Reset() HashMap.hashMap.Clear() Serializer.pathConditionVertices.Clear() + Serializer.resetPathConditionVertexIdCounter () + Serializer.termsWithId.Clear() do! sendResponse ( From 690daedf557b231c930f0dd62acb0500498a7787 Mon Sep 17 00:00:00 2001 From: Anya Chistyakova <57658770+Anya497@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:58:59 +0300 Subject: [PATCH 10/12] Convert PathConditionVertices from GameState to tensor. (#95) --- VSharp.Explorer/AISearcher.fs | 73 ++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs index 3e99b67bb..6272ff025 100644 --- a/VSharp.Explorer/AISearcher.fs +++ b/VSharp.Explorer/AISearcher.fs @@ -213,7 +213,8 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option ) = let numOfVertexAttributes = 7 - let numOfStateAttributes = 7 + let numOfStateAttributes = 6 + let numOfPathConditionVertexAttributes = 49 let numOfHistoryEdgeAttributes = 2 @@ -248,10 +249,33 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option, int>() let verticesIds = Dictionary, int>() + let pathConditionVerticesIds = Dictionary, int>() let networkInput = let res = Dictionary<_, _>() + let pathConditionVertices, numOfPcToPcEdges = + let mutable numOfPcToPcEdges = 0 + + let shape = + [| int64 gameState.PathConditionVertices.Length + numOfPathConditionVertexAttributes |] + + let attributes = + Array.zeroCreate ( + gameState.PathConditionVertices.Length * numOfPathConditionVertexAttributes + ) + + for i in 0 .. gameState.PathConditionVertices.Length - 1 do + let v = gameState.PathConditionVertices.[i] + numOfPcToPcEdges <- numOfPcToPcEdges + v.Children.Length * 2 + pathConditionVerticesIds.Add(v.Id, i) + let j = i * numOfPathConditionVertexAttributes + attributes.[j + int v.Type] <- float32 1u + + OrtValue.CreateTensorValueFromMemory(attributes, shape), numOfPcToPcEdges + + let gameVertices = let shape = [| int64 gameState.GraphVertices.Length; numOfVertexAttributes |] @@ -285,8 +309,6 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option Array.iter (fun state -> @@ -334,6 +384,14 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option Array.iteri (fun i historyElem -> let j = firstFreePositionInHistoryIndex + i @@ -352,7 +410,8 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option Date: Mon, 14 Apr 2025 11:25:41 +0300 Subject: [PATCH 11/12] Fix ONNX converter (#96) * Fix indices in ONNX converter. --- VSharp.Explorer/AISearcher.fs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs index 6272ff025..639f2f651 100644 --- a/VSharp.Explorer/AISearcher.fs +++ b/VSharp.Explorer/AISearcher.fs @@ -94,6 +94,7 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option TrainingSendEachStep | Some(SendModel _) -> TrainingSendModel | None -> Runner + let pick selector = if useDefaultSearcher then defaultSearcherSteps <- defaultSearcherSteps + 1u @@ -122,7 +123,7 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option Runner && stepsToPlay = stepsPlayed then None else let toPredict = @@ -136,6 +137,7 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option gameState.Value let stateId = oracle.Predict toPredict + afterFirstAIPeek <- true let state = availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) lastCollectedStatistics <- statistics @@ -147,6 +149,7 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option) = match gameState with | None -> Some delta @@ -199,11 +202,11 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option) = + static member convertOutputToJson(output: IDisposableReadOnlyCollection) = seq { 0 .. output.Count - 1 } |> Seq.map (fun i -> output[i].GetTensorDataAsSpan().ToArray()) - + new ( @@ -309,11 +312,11 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option Date: Fri, 22 Aug 2025 10:03:24 +0300 Subject: [PATCH 12/12] Fix steps counting (#97) * fix: Allow AISearcher to pick a step without relying on stepsPlayed * feat: Add sending of steps count * Rename PathCondition according to client's naming * Revert "Rename PathCondition according to client's naming" This reverts commit e9cb37576b8c29625f79082b3da87287197e105f. * Fix AISearcher bug with stepsPlayed --- VSharp.Explorer/AISearcher.fs | 46 +++++++++++++---------------- VSharp.ML.GameServer.Runner/Main.fs | 1 + VSharp.ML.GameServer/Messages.fs | 10 ++++--- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/VSharp.Explorer/AISearcher.fs b/VSharp.Explorer/AISearcher.fs index 639f2f651..49a450b38 100644 --- a/VSharp.Explorer/AISearcher.fs +++ b/VSharp.Explorer/AISearcher.fs @@ -123,32 +123,28 @@ type internal AISearcher(oracle: Oracle, aiAgentTrainingMode: Option Runner && stepsToPlay = stepsPlayed then + let toPredict = + match aiMode with + | TrainingSendEachStep + | TrainingSendModel -> + if stepsPlayed > 0u then + gameStateDelta + else + gameState.Value + | Runner -> gameState.Value + + let stateId = oracle.Predict toPredict + afterFirstAIPeek <- true + let state = availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) + lastCollectedStatistics <- statistics + stepsPlayed <- stepsPlayed + 1u + + match state with + | Some state -> Some state + | None -> + incorrectPredictedStateId <- true + oracle.Feedback(Feedback.IncorrectPredictedStateId stateId) None - else - let toPredict = - match aiMode with - | TrainingSendEachStep - | TrainingSendModel -> - if stepsPlayed > 0u then - gameStateDelta - else - gameState.Value - | Runner -> gameState.Value - - let stateId = oracle.Predict toPredict - - afterFirstAIPeek <- true - let state = availableStates |> Seq.tryFind (fun s -> s.internalId = stateId) - lastCollectedStatistics <- statistics - stepsPlayed <- stepsPlayed + 1u - - match state with - | Some state -> Some state - | None -> - incorrectPredictedStateId <- true - oracle.Feedback(Feedback.IncorrectPredictedStateId stateId) - None static member updateGameState (delta: GameState) (gameState: Option) = match gameState with diff --git a/VSharp.ML.GameServer.Runner/Main.fs b/VSharp.ML.GameServer.Runner/Main.fs index 1b4b9a207..9f409b36f 100644 --- a/VSharp.ML.GameServer.Runner/Main.fs +++ b/VSharp.ML.GameServer.Runner/Main.fs @@ -239,6 +239,7 @@ let ws port outputDirectory (webSocket: WebSocket) (context: HttpContext) = GameOver( explorationResult.ActualCoverage, explorationResult.TestsCount, + explorationResult.StepsCount, explorationResult.ErrorsCount ) ) diff --git a/VSharp.ML.GameServer/Messages.fs b/VSharp.ML.GameServer/Messages.fs index 43c60c51b..7f13570f7 100644 --- a/VSharp.ML.GameServer/Messages.fs +++ b/VSharp.ML.GameServer/Messages.fs @@ -109,11 +109,13 @@ type GameOverMessageBody = interface IRawOutgoingMessageBody val ActualCoverage: uint val TestsCount: uint32 + val StepsCount: uint32 val ErrorsCount: uint32 - new(actualCoverage, testsCount, errorsCount) = + new(actualCoverage, testsCount, stepsCount, errorsCount) = { ActualCoverage = actualCoverage TestsCount = testsCount + StepsCount = stepsCount ErrorsCount = errorsCount } [] @@ -371,7 +373,7 @@ type IncorrectPredictedStateIdMessageBody = new(stateId) = { StateId = stateId } type OutgoingMessage = - | GameOver of uint * uint32 * uint32 + | GameOver of uint * uint32 * uint32 * uint32 | MoveReward of Reward | IncorrectPredictedStateId of uint | ReadyForNextStep of GameState @@ -397,8 +399,8 @@ let deserializeInputMessage (messageData: byte[]) = let serializeOutgoingMessage (message: OutgoingMessage) = match message with - | GameOver(actualCoverage, testsCount, errorsCount) -> - RawOutgoingMessage("GameOver", box (GameOverMessageBody(actualCoverage, testsCount, errorsCount))) + | GameOver(actualCoverage, testsCount, stepsCount, errorsCount) -> + RawOutgoingMessage("GameOver", box (GameOverMessageBody(actualCoverage, testsCount, stepsCount, errorsCount))) | MoveReward reward -> RawOutgoingMessage("MoveReward", reward) | IncorrectPredictedStateId stateId -> RawOutgoingMessage("IncorrectPredictedStateId", IncorrectPredictedStateIdMessageBody stateId)