Skip to content

Commit 8b7934f

Browse files
authored
Added GetBoundValues and TryFindBoundValue APIs to FsiEvaluationSession (#9068)
* Added GetBoundValues/TryFindBoundValue to FsiSession. Added FsiTests. * Added more tests
1 parent d14b5d3 commit 8b7934f

File tree

4 files changed

+308
-6
lines changed

4 files changed

+308
-6
lines changed

src/fsharp/fsi/fsi.fs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ type FsiValue(reflectionValue:obj, reflectionType:Type, fsharpType:FSharpType) =
6868
member x.ReflectionType = reflectionType
6969
member x.FSharpType = fsharpType
7070

71+
[<Sealed>]
72+
type FsiBoundValue(name: string, value: FsiValue) =
73+
member _.Name = name
74+
member _.Value = value
7175

7276
[<AutoOpen>]
7377
module internal Utilities =
@@ -927,6 +931,7 @@ type internal FsiDynamicCompilerState =
927931
tcState : TcState
928932
tcImports : TcImports
929933
ilxGenerator : IlxGen.IlxAssemblyGenerator
934+
boundValues : NameMap<Val>
930935
// Why is this not in FsiOptions?
931936
timing : bool
932937
debugBreak : bool }
@@ -1114,6 +1119,13 @@ type internal FsiDynamicCompiler
11141119
// Return the new state and the environment at the end of the last input, ready for further inputs.
11151120
(istate,tcEnvAtEndOfLastInput,declaredImpls)
11161121

1122+
let tryGetGeneratedValue istate cenv v =
1123+
match istate.ilxGenerator.LookupGeneratedValue(valuePrinter.GetEvaluationContext(istate.emEnv), v) with
1124+
| Some (res, ty) ->
1125+
Some (FsiValue(res, ty, FSharpType(cenv, v.Type)))
1126+
| _ ->
1127+
None
1128+
11171129
let nextFragmentId() = fragmentId <- fragmentId + 1; fragmentId
11181130

11191131
let mkFragmentPath i =
@@ -1146,6 +1158,7 @@ type internal FsiDynamicCompiler
11461158

11471159
// Find all new declarations the EvaluationListener
11481160
let mutable itValue = None
1161+
let mutable boundValues = newState.boundValues
11491162
try
11501163
let contents = FSharpAssemblyContents(tcGlobals, tcState.Ccu, Some tcState.CcuSig, tcImports, declaredImpls)
11511164
let contentFile = contents.ImplementationFiles.[0]
@@ -1161,11 +1174,11 @@ type internal FsiDynamicCompiler
11611174
if v.IsModuleValueOrMember && not v.IsMember then
11621175
let fsiValueOpt =
11631176
match v.Item with
1164-
| Item.Value vref ->
1165-
let optValue = newState.ilxGenerator.LookupGeneratedValue(valuePrinter.GetEvaluationContext(newState.emEnv), vref.Deref)
1166-
match optValue with
1167-
| Some (res, ty) -> Some(FsiValue(res, ty, FSharpType(cenv, vref.Type)))
1168-
| None -> None
1177+
| Item.Value vref ->
1178+
let fsiValueOpt = tryGetGeneratedValue newState cenv vref.Deref
1179+
if fsiValueOpt.IsSome then
1180+
boundValues <- boundValues |> NameMap.add v.CompiledName vref.Deref
1181+
fsiValueOpt
11691182
| _ -> None
11701183

11711184
if v.CompiledName = "it" then
@@ -1191,7 +1204,7 @@ type internal FsiDynamicCompiler
11911204
| _ -> ()
11921205
with _ -> ()
11931206

1194-
newState, Completed itValue
1207+
{ newState with boundValues = boundValues }, Completed itValue
11951208

11961209
/// Evaluate the given expression and produce a new interactive state.
11971210
member fsiDynamicCompiler.EvalParsedExpression (ctok, errorLogger: ErrorLogger, istate, expr: SynExpr) =
@@ -1382,6 +1395,28 @@ type internal FsiDynamicCompiler
13821395
let istate = (istate, sourceFiles, inputs) |||> List.fold2 (fun istate sourceFile input -> fsiDynamicCompiler.ProcessMetaCommandsFromInputAsInteractiveCommands(ctok, istate, sourceFile, input))
13831396
fsiDynamicCompiler.EvalParsedSourceFiles (ctok, errorLogger, istate, inputs)
13841397

1398+
member __.GetBoundValues istate =
1399+
let cenv = SymbolEnv(istate.tcGlobals, istate.tcState.Ccu, Some istate.tcState.CcuSig, istate.tcImports)
1400+
[ for pair in istate.boundValues do
1401+
let nm = pair.Key
1402+
let v = pair.Value
1403+
match tryGetGeneratedValue istate cenv v with
1404+
| Some fsiValue ->
1405+
yield FsiBoundValue(nm, fsiValue)
1406+
| _ ->
1407+
() ]
1408+
1409+
member __.TryFindBoundValue(istate, nm) =
1410+
match istate.boundValues.TryFind nm with
1411+
| Some v ->
1412+
let cenv = SymbolEnv(istate.tcGlobals, istate.tcState.Ccu, Some istate.tcState.CcuSig, istate.tcImports)
1413+
match tryGetGeneratedValue istate cenv v with
1414+
| Some fsiValue ->
1415+
Some (FsiBoundValue(nm, fsiValue))
1416+
| _ ->
1417+
None
1418+
| _ ->
1419+
None
13851420

13861421
member __.GetInitialInteractiveState () =
13871422
let tcConfig = TcConfig.Create(tcConfigB,validate=false)
@@ -1399,6 +1434,7 @@ type internal FsiDynamicCompiler
13991434
tcState = tcState
14001435
tcImports = tcImports
14011436
ilxGenerator = ilxGenerator
1437+
boundValues = NameMap.empty
14021438
timing = false
14031439
debugBreak = false
14041440
}
@@ -2753,6 +2789,12 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i
27532789
/// Event fires when a root-level value is bound to an identifier, e.g., via `let x = ...`.
27542790
member __.ValueBound = fsiDynamicCompiler.ValueBound
27552791

2792+
member __.GetBoundValues() =
2793+
fsiDynamicCompiler.GetBoundValues fsiInteractionProcessor.CurrentState
2794+
2795+
member __.TryFindBoundValue(name: string) =
2796+
fsiDynamicCompiler.TryFindBoundValue(fsiInteractionProcessor.CurrentState, name)
2797+
27562798
/// Performs these steps:
27572799
/// - Load the dummy interaction, if any
27582800
/// - Set up exception handling, if any

src/fsharp/fsi/fsi.fsi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ type FsiValue =
2323
member FSharpType : FSharpType
2424
#endif
2525

26+
[<Sealed>]
27+
/// Represents an evaluated F# value that is bound to an identifier
28+
type FsiBoundValue =
29+
30+
/// The identifier of the value
31+
member Name : string
32+
33+
/// The evaluated F# value
34+
member Value : FsiValue
35+
2636
[<Class>]
2737
type EvaluationEventArgs =
2838
inherit System.EventArgs
@@ -250,6 +260,12 @@ type FsiEvaluationSession =
250260
/// Event fires when a root-level value is bound to an identifier, e.g., via `let x = ...`.
251261
member ValueBound : IEvent<obj * System.Type * string>
252262

263+
/// Gets the root-level values that are bound to an identifier
264+
member GetBoundValues : unit -> FsiBoundValue list
265+
266+
/// Tries to find a root-level value that is bound to the given identifier
267+
member TryFindBoundValue : name: string -> FsiBoundValue option
268+
253269
/// Load the dummy interaction, load the initial files, and,
254270
/// if interacting, start the background thread to read the standard input.
255271
///

tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Compile Include="ProductVersion.fs" />
1919
<Compile Include="EditDistance.fs" />
2020
<Compile Include="SuggestionBuffer.fs" />
21+
<Compile Include="FsiTests.fs" />
2122
</ItemGroup>
2223

2324
<ItemGroup>
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
module FSharp.Compiler.UnitTests.FsiTests
2+
3+
open System.IO
4+
open FSharp.Compiler.Interactive.Shell
5+
open NUnit.Framework
6+
7+
let createFsiSession () =
8+
// Intialize output and input streams
9+
let inStream = new StringReader("")
10+
let outStream = new CompilerOutputStream()
11+
let errStream = new CompilerOutputStream()
12+
13+
// Build command line arguments & start FSI session
14+
let argv = [| "C:\\fsi.exe" |]
15+
let allArgs = Array.append argv [|"--noninteractive"|]
16+
17+
let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration()
18+
FsiEvaluationSession.Create(fsiConfig, allArgs, inStream, new StreamWriter(outStream), new StreamWriter(errStream), collectible = true)
19+
20+
[<Test>]
21+
let ``No bound values at the start of FSI session`` () =
22+
use fsiSession = createFsiSession ()
23+
let values = fsiSession.GetBoundValues()
24+
Assert.IsEmpty values
25+
26+
[<Test>]
27+
let ``Bound value has correct name`` () =
28+
use fsiSession = createFsiSession ()
29+
30+
fsiSession.EvalInteraction("let x = 1")
31+
32+
let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne
33+
34+
Assert.AreEqual("x", boundValue.Name)
35+
36+
[<Test>]
37+
let ``Bound value has correct value`` () =
38+
use fsiSession = createFsiSession ()
39+
40+
fsiSession.EvalInteraction("let y = 2")
41+
42+
let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne
43+
44+
Assert.AreEqual(2, boundValue.Value.ReflectionValue)
45+
46+
[<Test>]
47+
let ``Bound value has correct type`` () =
48+
use fsiSession = createFsiSession ()
49+
50+
fsiSession.EvalInteraction("let z = 3")
51+
52+
let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne
53+
54+
Assert.AreEqual(typeof<int>, boundValue.Value.ReflectionType)
55+
56+
[<Test>]
57+
let ``Seven bound values are ordered and have their correct name`` () =
58+
use fsiSession = createFsiSession ()
59+
60+
fsiSession.EvalInteraction("let x = 1")
61+
fsiSession.EvalInteraction("let y = 2")
62+
fsiSession.EvalInteraction("let z = 3")
63+
fsiSession.EvalInteraction("let a = 4")
64+
fsiSession.EvalInteraction("let ccc = 5")
65+
fsiSession.EvalInteraction("let b = 6")
66+
fsiSession.EvalInteraction("let aa = 7")
67+
68+
let names = fsiSession.GetBoundValues() |> List.map (fun x -> x.Name)
69+
70+
Assert.AreEqual(["a";"aa";"b";"ccc";"x";"y";"z"], names)
71+
72+
[<Test>]
73+
let ``Seven bound values are ordered and have their correct value`` () =
74+
use fsiSession = createFsiSession ()
75+
76+
fsiSession.EvalInteraction("let x = 1")
77+
fsiSession.EvalInteraction("let y = 2")
78+
fsiSession.EvalInteraction("let z = 3")
79+
fsiSession.EvalInteraction("let a = 4")
80+
fsiSession.EvalInteraction("let ccc = 5")
81+
fsiSession.EvalInteraction("let b = 6")
82+
fsiSession.EvalInteraction("let aa = 7")
83+
84+
let values = fsiSession.GetBoundValues() |> List.map (fun x -> x.Value.ReflectionValue)
85+
86+
Assert.AreEqual([4;7;6;5;1;2;3], values)
87+
88+
[<Test>]
89+
let ``Seven bound values are ordered and have their correct type`` () =
90+
use fsiSession = createFsiSession ()
91+
92+
fsiSession.EvalInteraction("let x = 1")
93+
fsiSession.EvalInteraction("let y = 2")
94+
fsiSession.EvalInteraction("let z = 3")
95+
fsiSession.EvalInteraction("let a = 4.")
96+
fsiSession.EvalInteraction("let ccc = 5")
97+
fsiSession.EvalInteraction("let b = 6.f")
98+
fsiSession.EvalInteraction("let aa = 7")
99+
100+
let types = fsiSession.GetBoundValues() |> List.map (fun x -> x.Value.ReflectionType)
101+
102+
Assert.AreEqual([typeof<float>;typeof<int>;typeof<float32>;typeof<int>;typeof<int>;typeof<int>;typeof<int>], types)
103+
104+
[<Test>]
105+
let ``Able to find a bound value by the identifier`` () =
106+
use fsiSession = createFsiSession ()
107+
108+
fsiSession.EvalInteraction("let x = 1")
109+
fsiSession.EvalInteraction("let y = 2")
110+
fsiSession.EvalInteraction("let z = 3")
111+
fsiSession.EvalInteraction("let a = 4")
112+
fsiSession.EvalInteraction("let ccc = 5")
113+
fsiSession.EvalInteraction("let b = 6")
114+
fsiSession.EvalInteraction("let aa = 7")
115+
116+
let boundValueOpt = fsiSession.TryFindBoundValue "ccc"
117+
118+
Assert.IsTrue boundValueOpt.IsSome
119+
120+
[<Test>]
121+
let ``Able to find a bound value by the identifier and has valid info`` () =
122+
use fsiSession = createFsiSession ()
123+
124+
fsiSession.EvalInteraction("let x = 1.")
125+
fsiSession.EvalInteraction("let y = 2.")
126+
fsiSession.EvalInteraction("let z = 3")
127+
fsiSession.EvalInteraction("let a = 4.")
128+
fsiSession.EvalInteraction("let ccc = 5.")
129+
fsiSession.EvalInteraction("let b = 6.")
130+
fsiSession.EvalInteraction("let aa = 7.")
131+
132+
let boundValue = (fsiSession.TryFindBoundValue "z").Value
133+
134+
Assert.AreEqual("z", boundValue.Name)
135+
Assert.AreEqual(3, boundValue.Value.ReflectionValue)
136+
Assert.AreEqual(typeof<int>, boundValue.Value.ReflectionType)
137+
138+
[<Test>]
139+
let ``Not Able to find a bound value by the identifier`` () =
140+
use fsiSession = createFsiSession ()
141+
142+
fsiSession.EvalInteraction("let x = 1")
143+
fsiSession.EvalInteraction("let y = 2")
144+
fsiSession.EvalInteraction("let z = 3")
145+
fsiSession.EvalInteraction("let a = 4")
146+
fsiSession.EvalInteraction("let ccc = 5")
147+
fsiSession.EvalInteraction("let b = 6")
148+
fsiSession.EvalInteraction("let aa = 7")
149+
150+
let boundValueOpt = fsiSession.TryFindBoundValue "aaa"
151+
152+
Assert.IsTrue boundValueOpt.IsNone
153+
154+
[<Test>]
155+
let ``The 'it' value does not exist at the start of a FSI session`` () =
156+
use fsiSession = createFsiSession ()
157+
158+
let boundValueOpt = fsiSession.TryFindBoundValue "it"
159+
160+
Assert.IsTrue boundValueOpt.IsNone
161+
162+
[<Test>]
163+
let ``The 'it' bound value does exists after a value is not explicitly bound`` () =
164+
use fsiSession = createFsiSession ()
165+
166+
fsiSession.EvalInteraction("456")
167+
168+
let boundValueOpt = fsiSession.TryFindBoundValue "it"
169+
170+
Assert.IsTrue boundValueOpt.IsSome
171+
172+
[<Test>]
173+
let ``The 'it' value does exists after a value is not explicitly bound and has valid info`` () =
174+
use fsiSession = createFsiSession ()
175+
176+
fsiSession.EvalInteraction("456")
177+
178+
let boundValue = (fsiSession.TryFindBoundValue "it").Value
179+
180+
Assert.AreEqual("it", boundValue.Name)
181+
Assert.AreEqual(456, boundValue.Value.ReflectionValue)
182+
Assert.AreEqual(typeof<int>, boundValue.Value.ReflectionType)
183+
184+
[<Test>]
185+
let ``The latest shadowed value is only available`` () =
186+
use fsiSession = createFsiSession ()
187+
188+
fsiSession.EvalInteraction("let x = 1")
189+
let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne
190+
191+
Assert.AreEqual("x", boundValue.Name)
192+
Assert.AreEqual(1, boundValue.Value.ReflectionValue)
193+
Assert.AreEqual(typeof<int>, boundValue.Value.ReflectionType)
194+
195+
fsiSession.EvalInteraction("let x = (1, 2)")
196+
let boundValue = fsiSession.GetBoundValues() |> List.exactlyOne
197+
198+
Assert.AreEqual("x", boundValue.Name)
199+
Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue)
200+
Assert.AreEqual(typeof<int * int>, boundValue.Value.ReflectionType)
201+
202+
[<Test>]
203+
let ``The latest shadowed value is only available and can be found`` () =
204+
use fsiSession = createFsiSession ()
205+
206+
fsiSession.EvalInteraction("let x = 1")
207+
let boundValue = (fsiSession.TryFindBoundValue "x").Value
208+
209+
Assert.AreEqual("x", boundValue.Name)
210+
Assert.AreEqual(1, boundValue.Value.ReflectionValue)
211+
Assert.AreEqual(typeof<int>, boundValue.Value.ReflectionType)
212+
213+
fsiSession.EvalInteraction("let x = (1, 2)")
214+
let boundValue = (fsiSession.TryFindBoundValue "x").Value
215+
216+
Assert.AreEqual("x", boundValue.Name)
217+
Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue)
218+
Assert.AreEqual(typeof<int * int>, boundValue.Value.ReflectionType)
219+
220+
[<Test>]
221+
let ``Values are successfully shadowed even with intermediate interactions`` () =
222+
use fsiSession = createFsiSession ()
223+
224+
fsiSession.EvalInteraction("let x = 1")
225+
fsiSession.EvalInteraction("let z = 100")
226+
fsiSession.EvalInteraction("let x = (1, 2)")
227+
fsiSession.EvalInteraction("let w = obj ()")
228+
229+
let boundValues = fsiSession.GetBoundValues()
230+
231+
Assert.AreEqual(3, boundValues.Length)
232+
233+
let boundValue = boundValues |> List.find (fun x -> x.Name = "x")
234+
235+
Assert.AreEqual("x", boundValue.Name)
236+
Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue)
237+
Assert.AreEqual(typeof<int * int>, boundValue.Value.ReflectionType)
238+
239+
let boundValue = (fsiSession.TryFindBoundValue "x").Value
240+
241+
Assert.AreEqual("x", boundValue.Name)
242+
Assert.AreEqual((1, 2), boundValue.Value.ReflectionValue)
243+
Assert.AreEqual(typeof<int * int>, boundValue.Value.ReflectionType)

0 commit comments

Comments
 (0)