diff --git a/src/fsharp/fsi/fsi.fs b/src/fsharp/fsi/fsi.fs index 8b1e5cae835..d87dd6e04dd 100644 --- a/src/fsharp/fsi/fsi.fs +++ b/src/fsharp/fsi/fsi.fs @@ -68,6 +68,10 @@ type FsiValue(reflectionValue:obj, reflectionType:Type, fsharpType:FSharpType) = member x.ReflectionType = reflectionType member x.FSharpType = fsharpType +[] +type FsiBoundValue(name: string, value: FsiValue) = + member _.Name = name + member _.Value = value [] module internal Utilities = @@ -927,6 +931,7 @@ type internal FsiDynamicCompilerState = tcState : TcState tcImports : TcImports ilxGenerator : IlxGen.IlxAssemblyGenerator + boundValues : NameMap // Why is this not in FsiOptions? timing : bool debugBreak : bool } @@ -1114,6 +1119,13 @@ type internal FsiDynamicCompiler // Return the new state and the environment at the end of the last input, ready for further inputs. (istate,tcEnvAtEndOfLastInput,declaredImpls) + let tryGetGeneratedValue istate cenv v = + match istate.ilxGenerator.LookupGeneratedValue(valuePrinter.GetEvaluationContext(istate.emEnv), v) with + | Some (res, ty) -> + Some (FsiValue(res, ty, FSharpType(cenv, v.Type))) + | _ -> + None + let nextFragmentId() = fragmentId <- fragmentId + 1; fragmentId let mkFragmentPath i = @@ -1146,6 +1158,7 @@ type internal FsiDynamicCompiler // Find all new declarations the EvaluationListener let mutable itValue = None + let mutable boundValues = newState.boundValues try let contents = FSharpAssemblyContents(tcGlobals, tcState.Ccu, Some tcState.CcuSig, tcImports, declaredImpls) let contentFile = contents.ImplementationFiles.[0] @@ -1161,11 +1174,11 @@ type internal FsiDynamicCompiler if v.IsModuleValueOrMember && not v.IsMember then let fsiValueOpt = match v.Item with - | Item.Value vref -> - let optValue = newState.ilxGenerator.LookupGeneratedValue(valuePrinter.GetEvaluationContext(newState.emEnv), vref.Deref) - match optValue with - | Some (res, ty) -> Some(FsiValue(res, ty, FSharpType(cenv, vref.Type))) - | None -> None + | Item.Value vref -> + let fsiValueOpt = tryGetGeneratedValue newState cenv vref.Deref + if fsiValueOpt.IsSome then + boundValues <- boundValues |> NameMap.add v.CompiledName vref.Deref + fsiValueOpt | _ -> None if v.CompiledName = "it" then @@ -1191,7 +1204,7 @@ type internal FsiDynamicCompiler | _ -> () with _ -> () - newState, Completed itValue + { newState with boundValues = boundValues }, Completed itValue /// Evaluate the given expression and produce a new interactive state. member fsiDynamicCompiler.EvalParsedExpression (ctok, errorLogger: ErrorLogger, istate, expr: SynExpr) = @@ -1382,6 +1395,28 @@ type internal FsiDynamicCompiler let istate = (istate, sourceFiles, inputs) |||> List.fold2 (fun istate sourceFile input -> fsiDynamicCompiler.ProcessMetaCommandsFromInputAsInteractiveCommands(ctok, istate, sourceFile, input)) fsiDynamicCompiler.EvalParsedSourceFiles (ctok, errorLogger, istate, inputs) + member __.GetBoundValues istate = + let cenv = SymbolEnv(istate.tcGlobals, istate.tcState.Ccu, Some istate.tcState.CcuSig, istate.tcImports) + [ for pair in istate.boundValues do + let nm = pair.Key + let v = pair.Value + match tryGetGeneratedValue istate cenv v with + | Some fsiValue -> + yield FsiBoundValue(nm, fsiValue) + | _ -> + () ] + + member __.TryFindBoundValue(istate, nm) = + match istate.boundValues.TryFind nm with + | Some v -> + let cenv = SymbolEnv(istate.tcGlobals, istate.tcState.Ccu, Some istate.tcState.CcuSig, istate.tcImports) + match tryGetGeneratedValue istate cenv v with + | Some fsiValue -> + Some (FsiBoundValue(nm, fsiValue)) + | _ -> + None + | _ -> + None member __.GetInitialInteractiveState () = let tcConfig = TcConfig.Create(tcConfigB,validate=false) @@ -1399,6 +1434,7 @@ type internal FsiDynamicCompiler tcState = tcState tcImports = tcImports ilxGenerator = ilxGenerator + boundValues = NameMap.empty timing = false debugBreak = false } @@ -2753,6 +2789,12 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i /// Event fires when a root-level value is bound to an identifier, e.g., via `let x = ...`. member __.ValueBound = fsiDynamicCompiler.ValueBound + member __.GetBoundValues() = + fsiDynamicCompiler.GetBoundValues fsiInteractionProcessor.CurrentState + + member __.TryFindBoundValue(name: string) = + fsiDynamicCompiler.TryFindBoundValue(fsiInteractionProcessor.CurrentState, name) + /// Performs these steps: /// - Load the dummy interaction, if any /// - Set up exception handling, if any diff --git a/src/fsharp/fsi/fsi.fsi b/src/fsharp/fsi/fsi.fsi index de19b7ae266..02229148c31 100644 --- a/src/fsharp/fsi/fsi.fsi +++ b/src/fsharp/fsi/fsi.fsi @@ -23,6 +23,16 @@ type FsiValue = member FSharpType : FSharpType #endif +[] +/// Represents an evaluated F# value that is bound to an identifier +type FsiBoundValue = + + /// The identifier of the value + member Name : string + + /// The evaluated F# value + member Value : FsiValue + [] type EvaluationEventArgs = inherit System.EventArgs @@ -250,6 +260,12 @@ type FsiEvaluationSession = /// Event fires when a root-level value is bound to an identifier, e.g., via `let x = ...`. member ValueBound : IEvent + /// Gets the root-level values that are bound to an identifier + member GetBoundValues : unit -> FsiBoundValue list + + /// Tries to find a root-level value that is bound to the given identifier + member TryFindBoundValue : name: string -> FsiBoundValue option + /// Load the dummy interaction, load the initial files, and, /// if interacting, start the background thread to read the standard input. /// diff --git a/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj b/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj index 790fdc64317..6c7246549e1 100644 --- a/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj +++ b/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj @@ -18,6 +18,7 @@ + diff --git a/tests/FSharp.Compiler.UnitTests/FsiTests.fs b/tests/FSharp.Compiler.UnitTests/FsiTests.fs new file mode 100644 index 00000000000..54053d1f982 --- /dev/null +++ b/tests/FSharp.Compiler.UnitTests/FsiTests.fs @@ -0,0 +1,243 @@ +module FSharp.Compiler.UnitTests.FsiTests + +open System.IO +open FSharp.Compiler.Interactive.Shell +open NUnit.Framework + +let createFsiSession () = + // Intialize output and input streams + let inStream = new StringReader("") + let outStream = new CompilerOutputStream() + let errStream = new CompilerOutputStream() + + // Build command line arguments & start FSI session + let argv = [| "C:\\fsi.exe" |] + let allArgs = Array.append argv [|"--noninteractive"|] + + let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration() + FsiEvaluationSession.Create(fsiConfig, allArgs, inStream, new StreamWriter(outStream), new StreamWriter(errStream), collectible = true) + +[] +let ``No bound values at the start of FSI session`` () = + use fsiSession = createFsiSession () + let values = fsiSession.GetBoundValues() + Assert.IsEmpty values + +[] +let ``Bound value has correct name`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + + let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne + + Assert.AreEqual("x", boundValue.Name) + +[] +let ``Bound value has correct value`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let y = 2") + + let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne + + Assert.AreEqual(2, boundValue.Value.ReflectionValue) + +[] +let ``Bound value has correct type`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let z = 3") + + let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne + + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + +[] +let ``Seven bound values are ordered and have their correct name`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + fsiSession.EvalInteraction("let y = 2") + fsiSession.EvalInteraction("let z = 3") + fsiSession.EvalInteraction("let a = 4") + fsiSession.EvalInteraction("let ccc = 5") + fsiSession.EvalInteraction("let b = 6") + fsiSession.EvalInteraction("let aa = 7") + + let names = fsiSession.GetBoundValues() |> List.map (fun x -> x.Name) + + Assert.AreEqual(["a";"aa";"b";"ccc";"x";"y";"z"], names) + +[] +let ``Seven bound values are ordered and have their correct value`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + fsiSession.EvalInteraction("let y = 2") + fsiSession.EvalInteraction("let z = 3") + fsiSession.EvalInteraction("let a = 4") + fsiSession.EvalInteraction("let ccc = 5") + fsiSession.EvalInteraction("let b = 6") + fsiSession.EvalInteraction("let aa = 7") + + let values = fsiSession.GetBoundValues() |> List.map (fun x -> x.Value.ReflectionValue) + + Assert.AreEqual([4;7;6;5;1;2;3], values) + +[] +let ``Seven bound values are ordered and have their correct type`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + fsiSession.EvalInteraction("let y = 2") + fsiSession.EvalInteraction("let z = 3") + fsiSession.EvalInteraction("let a = 4.") + fsiSession.EvalInteraction("let ccc = 5") + fsiSession.EvalInteraction("let b = 6.f") + fsiSession.EvalInteraction("let aa = 7") + + let types = fsiSession.GetBoundValues() |> List.map (fun x -> x.Value.ReflectionType) + + Assert.AreEqual([typeof;typeof;typeof;typeof;typeof;typeof;typeof], types) + +[] +let ``Able to find a bound value by the identifier`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + fsiSession.EvalInteraction("let y = 2") + fsiSession.EvalInteraction("let z = 3") + fsiSession.EvalInteraction("let a = 4") + fsiSession.EvalInteraction("let ccc = 5") + fsiSession.EvalInteraction("let b = 6") + fsiSession.EvalInteraction("let aa = 7") + + let boundValueOpt = fsiSession.TryFindBoundValue "ccc" + + Assert.IsTrue boundValueOpt.IsSome + +[] +let ``Able to find a bound value by the identifier and has valid info`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1.") + fsiSession.EvalInteraction("let y = 2.") + fsiSession.EvalInteraction("let z = 3") + fsiSession.EvalInteraction("let a = 4.") + fsiSession.EvalInteraction("let ccc = 5.") + fsiSession.EvalInteraction("let b = 6.") + fsiSession.EvalInteraction("let aa = 7.") + + let boundValue = (fsiSession.TryFindBoundValue "z").Value + + Assert.AreEqual("z", boundValue.Name) + Assert.AreEqual(3, boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + +[] +let ``Not Able to find a bound value by the identifier`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + fsiSession.EvalInteraction("let y = 2") + fsiSession.EvalInteraction("let z = 3") + fsiSession.EvalInteraction("let a = 4") + fsiSession.EvalInteraction("let ccc = 5") + fsiSession.EvalInteraction("let b = 6") + fsiSession.EvalInteraction("let aa = 7") + + let boundValueOpt = fsiSession.TryFindBoundValue "aaa" + + Assert.IsTrue boundValueOpt.IsNone + +[] +let ``The 'it' value does not exist at the start of a FSI session`` () = + use fsiSession = createFsiSession () + + let boundValueOpt = fsiSession.TryFindBoundValue "it" + + Assert.IsTrue boundValueOpt.IsNone + +[] +let ``The 'it' bound value does exists after a value is not explicitly bound`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("456") + + let boundValueOpt = fsiSession.TryFindBoundValue "it" + + Assert.IsTrue boundValueOpt.IsSome + +[] +let ``The 'it' value does exists after a value is not explicitly bound and has valid info`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("456") + + let boundValue = (fsiSession.TryFindBoundValue "it").Value + + Assert.AreEqual("it", boundValue.Name) + Assert.AreEqual(456, boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + +[] +let ``The latest shadowed value is only available`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne + + Assert.AreEqual("x", boundValue.Name) + Assert.AreEqual(1, boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + + fsiSession.EvalInteraction("let x = (1, 2)") + let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne + + Assert.AreEqual("x", boundValue.Name) + Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + +[] +let ``The latest shadowed value is only available and can be found`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + let boundValue = (fsiSession.TryFindBoundValue "x").Value + + Assert.AreEqual("x", boundValue.Name) + Assert.AreEqual(1, boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + + fsiSession.EvalInteraction("let x = (1, 2)") + let boundValue = (fsiSession.TryFindBoundValue "x").Value + + Assert.AreEqual("x", boundValue.Name) + Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + +[] +let ``Values are successfully shadowed even with intermediate interactions`` () = + use fsiSession = createFsiSession () + + fsiSession.EvalInteraction("let x = 1") + fsiSession.EvalInteraction("let z = 100") + fsiSession.EvalInteraction("let x = (1, 2)") + fsiSession.EvalInteraction("let w = obj ()") + + let boundValues = fsiSession.GetBoundValues() + + Assert.AreEqual(3, boundValues.Length) + + let boundValue = boundValues |> List.find (fun x -> x.Name = "x") + + Assert.AreEqual("x", boundValue.Name) + Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) + + let boundValue = (fsiSession.TryFindBoundValue "x").Value + + Assert.AreEqual("x", boundValue.Name) + Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue) + Assert.AreEqual(typeof, boundValue.Value.ReflectionType) \ No newline at end of file