Skip to content

Commit 7f72d30

Browse files
committed
Add pattern-variable inlay hints and map hint rendering
1 parent b4ebc0d commit 7f72d30

6 files changed

Lines changed: 370 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to FScript are documented in this file.
44

5+
## [0.29.0]
6+
7+
- Improved LSP inlay/type rendering for map-related inference (`int|string` map-key domain and `unknown map` display).
8+
- Added LSP inlay type hints for pattern-bound variables (for example `| Some x ->` now shows `x: int`).
9+
- Added regression coverage for map-key/type inlay rendering and option-pattern variable inlay hints.
10+
511
## [0.28.0]
612

713
- Added local variable type hover in LSP.

src/FScript.Language/TypeInfer.fs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ module TypeInfer =
348348
| PWildcard _ -> Map.empty, Types.freshVar()
349349
| PVar (name, _) ->
350350
let tv = Types.freshVar()
351+
recordLocalVariableDeclaration name (Ast.spanOfPattern pat) tv
351352
Map.ofList [ name, tv ], tv
352353
| PLiteral (lit, _) ->
353354
let t =
@@ -691,13 +692,15 @@ module TypeInfer =
691692
let caseBoundNames = envPat |> Map.toList |> List.map fst
692693
let sBody, tBody, _ =
693694
withLocalBindings caseBoundNames (fun () ->
695+
let mutable envBodyCase = envCase'
694696
match guard with
695697
| Some guardExpr ->
696-
let sGuard, tGuard, _ = inferExpr typeDefs constructors envCase' guardExpr
698+
let sGuard, tGuard, _ = inferExpr typeDefs constructors envBodyCase guardExpr
697699
let sGuardBool = unify typeDefs (applyType sGuard tGuard) TBool caseSpan
698700
sAcc <- compose sGuardBool (compose sGuard sAcc)
701+
envBodyCase <- applyEnv (compose sGuardBool sGuard) envBodyCase
699702
| None -> ()
700-
inferExpr typeDefs constructors envCase' body)
703+
inferExpr typeDefs constructors envBodyCase body)
701704
let tBody' = applyType sBody tBody
702705
resultTypeOpt <-
703706
match resultTypeOpt with
@@ -1069,10 +1072,11 @@ module TypeInfer =
10691072
let s1, t1, _ = inferExpr typeDefs constructors env exprVal
10701073
applySubstToCapturedLocals startIndex s1
10711074
let env' = applyEnv s1 env
1072-
validateMapKeyTypes span t1
1073-
let scheme = Types.generalize env' t1
1075+
let inferred = applyType s1 t1
1076+
validateMapKeyTypes span inferred
1077+
let scheme = Types.generalize env' inferred
10741078
env <- env' |> Map.add name scheme
1075-
typed.Add(TSLet(name, exprVal, t1, false, isExported, span))
1079+
typed.Add(TSLet(name, exprVal, inferred, false, isExported, span))
10761080
| SLetRecGroup(bindings, isExported, span) ->
10771081
let startIndex =
10781082
match telemetry with

src/FScript.LanguageServer/LspHandlers.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,22 @@ module LspHandlers =
266266
hint["paddingLeft"] <- JsonValue.Create(true)
267267
hints.Add(hint :> JsonNode))
268268

269+
// Type hints for pattern-bound variables (for example: `Some x`).
270+
doc.PatternTypeHints
271+
|> List.iter (fun (span, label) ->
272+
let hintLine = max 0 (span.End.Line - 1)
273+
let hintChar = max 0 (span.End.Column - 1)
274+
if positionInRange hintLine hintChar (startLine, startChar, endLine, endChar) then
275+
let hint = JsonObject()
276+
let pos = JsonObject()
277+
pos["line"] <- JsonValue.Create(hintLine)
278+
pos["character"] <- JsonValue.Create(hintChar)
279+
hint["position"] <- pos
280+
hint["label"] <- JsonValue.Create(label)
281+
hint["kind"] <- JsonValue.Create(1)
282+
hint["paddingLeft"] <- JsonValue.Create(true)
283+
hints.Add(hint :> JsonNode))
284+
269285
for lineIndex = 0 to lines.Length - 1 do
270286
let lineText = lines[lineIndex].TrimEnd('\r')
271287
let mutable i = 0

src/FScript.LanguageServer/LspModel.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ module LspModel =
5757
ParameterTypeTargets: Map<string, string>
5858
FunctionParameters: Map<string, string list>
5959
ParameterTypeHints: (Span * string) list
60+
PatternTypeHints: (Span * string) list
6061
LocalVariableTypeHints: (Span * string * string) list
6162
// Variable occurrences keyed by identifier, sourced from AST spans.
6263
// This avoids text-based false positives (for example record field labels).

src/FScript.LanguageServer/LspSymbols.fs

Lines changed: 192 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,60 @@ open FScript.Language
88
module LspSymbols =
99
open LspModel
1010

11+
let private collectMapKeyDomainVars (t: Type) =
12+
let rec collect acc ty =
13+
match ty with
14+
| TMap (TVar v, valueType) ->
15+
collect (Set.add v acc) valueType
16+
| TMap (keyType, valueType) ->
17+
collect (collect acc keyType) valueType
18+
| TList inner
19+
| TOption inner -> collect acc inner
20+
| TTuple items ->
21+
items |> List.fold collect acc
22+
| TRecord fields ->
23+
fields |> Map.values |> Seq.fold collect acc
24+
| TFun (a, b) ->
25+
collect (collect acc a) b
26+
| _ -> acc
27+
28+
collect Set.empty t
29+
30+
let private lspTypeToStringWithKeyDomainVars (keyDomainVars: Set<int>) (t: Type) =
31+
let rec go t =
32+
match t with
33+
| TUnit -> "unit"
34+
| TInt -> "int"
35+
| TFloat -> "float"
36+
| TBool -> "bool"
37+
| TString -> "string"
38+
| TList t1 -> sprintf "%s list" (postfixArg t1)
39+
| TTuple ts -> ts |> List.map go |> String.concat " * " |> sprintf "(%s)"
40+
| TRecord fields ->
41+
fields
42+
|> Map.toList
43+
|> List.map (fun (name, fieldType) -> sprintf "%s: %s" name (go fieldType))
44+
|> String.concat "; "
45+
|> sprintf "{ %s }"
46+
| TMap (_, tv) ->
47+
sprintf "%s map" (postfixArg tv)
48+
| TOption t1 -> sprintf "%s option" (postfixArg t1)
49+
| TFun (a, b) -> sprintf "(%s -> %s)" (go a) (go b)
50+
| TNamed n -> n
51+
| TUnion (name, _) -> name
52+
| TTypeToken -> "type"
53+
| TVar v when Set.contains v keyDomainVars -> "int|string"
54+
| TVar _ -> "unknown"
55+
and postfixArg t =
56+
match t with
57+
| TFun _ | TTuple _ | TRecord _ -> sprintf "(%s)" (go t)
58+
| _ -> go t
59+
go t
60+
61+
let private lspTypeToString (t: Type) =
62+
let keyDomainVars = collectMapKeyDomainVars t
63+
lspTypeToStringWithKeyDomainVars keyDomainVars t
64+
1165
let rec private typeRefToString (typeRef: TypeRef) =
1266
match typeRef with
1367
| TRName name -> name
@@ -91,15 +145,15 @@ module LspSymbols =
91145
| true, TypeInfer.TSLet(_, _, t, _, _, _) ->
92146
{ Name = name
93147
Kind = symbolKindForType t
94-
TypeText = Some (Types.typeToString t)
148+
TypeText = Some (lspTypeToString t)
95149
TypeTargetName = typeTargetFromType t
96150
Span = span }
97151
| true, TypeInfer.TSLetRecGroup(bindings, _, _) ->
98152
match bindings |> List.tryFind (fun (n, _, _, _) -> n = name) with
99153
| Some (_, _, t, _) ->
100154
{ Name = name
101155
Kind = symbolKindForType t
102-
TypeText = Some (Types.typeToString t)
156+
TypeText = Some (lspTypeToString t)
103157
TypeTargetName = typeTargetFromType t
104158
Span = span }
105159
| None ->
@@ -419,13 +473,14 @@ module LspSymbols =
419473
match typedByName.TryGetValue(name) with
420474
| true, t when not parameters.IsEmpty ->
421475
let argTypes = takeParamTypes t parameters.Length
476+
let keyDomainVars = collectMapKeyDomainVars t
422477
(parameters, argTypes)
423478
||> List.zip
424479
|> List.choose (fun (param, argType) ->
425480
if param.Annotation.IsSome then
426481
None
427482
else
428-
Some (param.Span, $": {Types.typeToString argType}"))
483+
Some (param.Span, $": {lspTypeToStringWithKeyDomainVars keyDomainVars argType}"))
429484
| _ ->
430485
[]
431486

@@ -445,6 +500,134 @@ module LspSymbols =
445500
| _ ->
446501
[])
447502

503+
let private collectPatternVariableSpans (program: Program) =
504+
let rec collectPattern (acc: (string * Span) list) (pattern: Pattern) =
505+
match pattern with
506+
| PVar (name, span) ->
507+
(name, span) :: acc
508+
| PCons (head, tail, _) ->
509+
collectPattern (collectPattern acc head) tail
510+
| PTuple (items, _) ->
511+
items |> List.fold collectPattern acc
512+
| PRecord (fields, _) ->
513+
fields |> List.fold (fun state (_, p) -> collectPattern state p) acc
514+
| PMap (clauses, tailOpt, _) ->
515+
let withClauses =
516+
clauses
517+
|> List.fold (fun state (k, v) ->
518+
let withKey = collectPattern state k
519+
collectPattern withKey v) acc
520+
match tailOpt with
521+
| Some tail -> collectPattern withClauses tail
522+
| None -> withClauses
523+
| PSome (inner, _) ->
524+
collectPattern acc inner
525+
| PUnionCase (_, _, payload, _) ->
526+
match payload with
527+
| Some p -> collectPattern acc p
528+
| None -> acc
529+
| PWildcard _
530+
| PLiteral _
531+
| PNil _
532+
| PNone _
533+
| PTypeRef _ -> acc
534+
535+
let rec collectExpr (acc: (string * Span) list) (expr: Expr) =
536+
match expr with
537+
| EMatch (scrutinee, cases, _) ->
538+
let withScrutinee = collectExpr acc scrutinee
539+
cases
540+
|> List.fold (fun state (pat, guard, body, _) ->
541+
let withPattern = collectPattern state pat
542+
let withGuard =
543+
match guard with
544+
| Some g -> collectExpr withPattern g
545+
| None -> withPattern
546+
collectExpr withGuard body) withScrutinee
547+
| ELambda (_, body, _) ->
548+
collectExpr acc body
549+
| EApply (f, a, _) ->
550+
collectExpr (collectExpr acc f) a
551+
| EIf (c, t, f, _) ->
552+
collectExpr (collectExpr (collectExpr acc c) t) f
553+
| ERaise (inner, _) ->
554+
collectExpr acc inner
555+
| EFor (_, source, body, _) ->
556+
collectExpr (collectExpr acc source) body
557+
| ELet (_, value, body, _, _) ->
558+
collectExpr (collectExpr acc value) body
559+
| ELetRecGroup (bindings, body, _) ->
560+
let withBindings =
561+
bindings |> List.fold (fun state (_, _, value, _) -> collectExpr state value) acc
562+
collectExpr withBindings body
563+
| EList (items, _)
564+
| ETuple (items, _) ->
565+
items |> List.fold collectExpr acc
566+
| ERange (startExpr, endExpr, _) ->
567+
collectExpr (collectExpr acc startExpr) endExpr
568+
| ERecord (fields, _)
569+
| EStructuralRecord (fields, _) ->
570+
fields |> List.fold (fun state (_, value) -> collectExpr state value) acc
571+
| EMap (entries, _) ->
572+
entries
573+
|> List.fold (fun state entry ->
574+
match entry with
575+
| MEKeyValue (k, v) ->
576+
collectExpr (collectExpr state k) v
577+
| MESpread spread ->
578+
collectExpr state spread) acc
579+
| ERecordUpdate (baseExpr, fields, _)
580+
| EStructuralRecordUpdate (baseExpr, fields, _) ->
581+
let withBase = collectExpr acc baseExpr
582+
fields |> List.fold (fun state (_, value) -> collectExpr state value) withBase
583+
| EFieldGet (target, _, _) ->
584+
collectExpr acc target
585+
| EIndexGet (target, key, _)
586+
| ECons (target, key, _)
587+
| EAppend (target, key, _)
588+
| EBinOp (_, target, key, _) ->
589+
collectExpr (collectExpr acc target) key
590+
| ESome (inner, _)
591+
| EParen (inner, _) ->
592+
collectExpr acc inner
593+
| EInterpolatedString (parts, _) ->
594+
parts
595+
|> List.fold (fun state part ->
596+
match part with
597+
| IPText _ -> state
598+
| IPExpr embedded -> collectExpr state embedded) acc
599+
| EUnit _
600+
| ELiteral _
601+
| EVar _
602+
| ENone _
603+
| ETypeOf _
604+
| ENameOf _ -> acc
605+
606+
program
607+
|> List.fold (fun state stmt ->
608+
match stmt with
609+
| SLet (_, _, expr, _, _, _) ->
610+
collectExpr state expr
611+
| SLetRecGroup (bindings, _, _) ->
612+
bindings |> List.fold (fun inner (_, _, expr, _) -> collectExpr inner expr) state
613+
| SExpr expr ->
614+
collectExpr state expr
615+
| _ -> state) []
616+
|> List.rev
617+
618+
let private buildPatternTypeHints (program: Program) (localTypes: TypeInfer.LocalVariableTypeInfo list) =
619+
let localByNameAndSpan =
620+
localTypes
621+
|> List.map (fun entry -> (entry.Name, entry.Span.Start.Line, entry.Span.Start.Column, entry.Span.End.Line, entry.Span.End.Column), entry.Type)
622+
|> Map.ofList
623+
624+
collectPatternVariableSpans program
625+
|> List.choose (fun (name, span) ->
626+
let key = (name, span.Start.Line, span.Start.Column, span.End.Line, span.End.Column)
627+
localByNameAndSpan
628+
|> Map.tryFind key
629+
|> Option.map (fun t -> span, $": {lspTypeToString t}"))
630+
448631
let analyzeDocument (uri: string) (text: string) =
449632
let sourceName =
450633
if uri.StartsWith("file://", StringComparison.OrdinalIgnoreCase) then
@@ -459,6 +642,7 @@ module LspSymbols =
459642
let mutable parameterTypeTargets : Map<string, string> = Map.empty
460643
let mutable functionParameters : Map<string, string list> = Map.empty
461644
let mutable parameterTypeHints : (Span * string) list = []
645+
let mutable patternTypeHints : (Span * string) list = []
462646
let mutable localVariableTypeHints : (Span * string * string) list = []
463647

464648
let mutable parsedProgram : Program option = None
@@ -473,23 +657,26 @@ module LspSymbols =
473657
if isIncludeProgram program then
474658
symbols <- buildSymbolsFromProgram program None
475659
parameterTypeHints <- buildParameterTypeHints program None
660+
patternTypeHints <- []
476661
else
477662
try
478663
let typed, localTypes = TypeInfer.inferProgramWithLocalVariableTypes program
479664
symbols <- buildSymbolsFromProgram program (Some typed)
480665
parameterTypeHints <- buildParameterTypeHints program (Some typed)
666+
patternTypeHints <- buildPatternTypeHints program localTypes
481667
localVariableTypeHints <-
482668
localTypes
483669
|> List.filter (fun entry ->
484670
match entry.Span.Start.File with
485671
| Some file -> String.Equals(file, sourceName, StringComparison.OrdinalIgnoreCase)
486672
| None -> true)
487-
|> List.map (fun entry -> entry.Span, entry.Name, Types.typeToString entry.Type)
673+
|> List.map (fun entry -> entry.Span, entry.Name, lspTypeToString entry.Type)
488674
with
489675
| TypeException err ->
490676
diagnostics.Add(diagnostic 1 "type" err.Span err.Message)
491677
symbols <- buildSymbolsFromProgram program None
492678
parameterTypeHints <- buildParameterTypeHints program None
679+
patternTypeHints <- []
493680

494681
with
495682
| ParseException err ->
@@ -512,7 +699,7 @@ module LspSymbols =
512699
diagnostics.Add(diagnostic 2 "unused" span $"Unused top-level binding '{name}'")
513700
| None -> ()
514701

515-
documents[uri] <- { Text = text; Symbols = symbols; RecordParameterFields = recordParamFields; ParameterTypeTargets = parameterTypeTargets; FunctionParameters = functionParameters; ParameterTypeHints = parameterTypeHints; LocalVariableTypeHints = localVariableTypeHints; VariableOccurrences = occurrences }
702+
documents[uri] <- { Text = text; Symbols = symbols; RecordParameterFields = recordParamFields; ParameterTypeTargets = parameterTypeTargets; FunctionParameters = functionParameters; ParameterTypeHints = parameterTypeHints; PatternTypeHints = patternTypeHints; LocalVariableTypeHints = localVariableTypeHints; VariableOccurrences = occurrences }
516703
publishDiagnostics uri (diagnostics |> Seq.toList)
517704

518705
let tryResolveSymbol (doc: DocumentState) (line: int) (character: int) : TopLevelSymbol option =

0 commit comments

Comments
 (0)