From dcfe4cb03b6e8ba38f53f04aa6f718606a985a9f Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Wed, 21 Aug 2024 22:53:58 -0400 Subject: [PATCH 1/3] Implement key renaming in mapped types --- src/Escalier.Data/Library.fs | 9 ++- src/Escalier.TypeChecker.Tests/Tests.fs | 79 +++++++++++++++++++++++-- src/Escalier.TypeChecker/Unify.fs | 63 +++++++++++++++++++- 3 files changed, 144 insertions(+), 7 deletions(-) diff --git a/src/Escalier.Data/Library.fs b/src/Escalier.Data/Library.fs index f6f9c7f..da5b361 100644 --- a/src/Escalier.Data/Library.fs +++ b/src/Escalier.Data/Library.fs @@ -1096,7 +1096,14 @@ module Type = | TypeKind.Unary(op, arg) -> $"{op}{printType ctx arg}" | TypeKind.Wildcard -> "_" | TypeKind.TemplateLiteral { Parts = parts; Exprs = types } -> - failwith "TODO: printType - TypeKind.TemplateLiteral" + let mutable output = "" + + for part, t in List.zip (List.take types.Length parts) types do + output <- output + part + $"${{{printType ctx t}}}" + + output <- output + List.last parts + + $"`{output}`" | TypeKind.Intrinsic -> "intrinsic" | TypeKind.IntrinsicInstance { Name = name; TypeArgs = typeArgs } -> let typeArgs = diff --git a/src/Escalier.TypeChecker.Tests/Tests.fs b/src/Escalier.TypeChecker.Tests/Tests.fs index 9cc911c..d772c47 100644 --- a/src/Escalier.TypeChecker.Tests/Tests.fs +++ b/src/Escalier.TypeChecker.Tests/Tests.fs @@ -863,20 +863,29 @@ let InferTemplateLiteralTypeError () = Assert.True(Result.isError result) [] -let InferTemplateLiteralTypeErrorWithUnion () = +let InferTemplateLiteralWithUnion () = let result = result { let src = """ type Dir = `${"top" | "bottom"}-${"left" | "right"}`; - let x: Dir = "top-bottom"; + let x: Dir = "top-right"; """ - let! _, _ = inferModule src - () + let! ctx, env = inferModule src + + let! t = + expandScheme ctx env None (env.FindScheme "Dir") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal( + "`${(\"top\" | \"bottom\")}-${(\"left\" | \"right\")}`", + t.ToString() + ) } - Assert.True(Result.isError result) + printfn "result = %A" result + Assert.False(Result.isError result) [] let InferIntrinsicsBasicUsageLiterals () = @@ -989,6 +998,66 @@ let InferIntrinsicsInTemplateLiteralTypesWithUnion () = printfn "result = %A" result Assert.False(Result.isError result) +[] +let KebabTemplateLiteralType () = + let result = + result { + + let src = + """ + type Kebab = `-${T}-${U}-`; + type Foo = Kebab<"hello", "world">; + type Bar = Kebab; + """ + + let! ctx, env = inferModule src + + Assert.Empty(ctx.Report.Diagnostics) + + let! t = + expandScheme ctx env None (env.FindScheme "Foo") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("\"-hello-world-\"", t.ToString()) + + let! t = + expandScheme ctx env None (env.FindScheme "Bar") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("`-${T}-${U}-`", t.ToString()) + } + + printfn "result = %A" result + Assert.False(Result.isError result) + +[] +let MappedTypeWithTemplateLiteralKey () = + let result = + result { + + let src = + """ + type Foo = { + [`_${K}`]: T[K] for K in keyof T, + }; + type Point = {x: number, y: number}; + type Bar = Foo; + """ + + let! ctx, env = inferModule src + + Assert.Empty(ctx.Report.Diagnostics) + + let! t = + expandScheme ctx env None (env.FindScheme "Bar") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("{_x: number, _y: number}", t.ToString()) + } + + printfn "result = %A" result + Assert.False(Result.isError result) + [] let InferUnaryOperations () = let result = diff --git a/src/Escalier.TypeChecker/Unify.fs b/src/Escalier.TypeChecker/Unify.fs index 26c813e..a27a219 100644 --- a/src/Escalier.TypeChecker/Unify.fs +++ b/src/Escalier.TypeChecker/Unify.fs @@ -1224,7 +1224,11 @@ module rec Unify = // TODO: Handle the case where the type is a primitive and use a // special function to expand the type // TODO: Handle different kinds of index types, e.g. number, symbol - return! Error(TypeError.NotImplemented "TODO: expand index") + return! + Error( + TypeError.NotImplemented + $"TODO: expand index - target type = {target}" + ) | TypeKind.Condition { Check = check Extends = extends TrueType = trueType @@ -1324,6 +1328,9 @@ module rec Unify = result { match elem with | Mapped m -> + printfn $"TypeParam = {m.TypeParam.Name}" + printfn $"NameType = {m.NameType}" + match m.TypeParam.Constraint.Kind with | TypeKind.KeyOf t -> match t.Kind with @@ -1342,6 +1349,8 @@ module rec Unify = let! c = expandType ctx env ips mapping m.TypeParam.Constraint + printfn $"c = {c}" + // TODO: Document this because I don't remember why we need to // do this. match c.Kind with @@ -1350,6 +1359,13 @@ module rec Unify = types |> List.traverseResultM (fun keyType -> result { + // TODO: handle key renamining + let mutable nameMapping: Map = + Map.empty + + nameMapping <- + Map.add m.TypeParam.Name keyType nameMapping + let propName = match keyType.Kind with | TypeKind.Literal(Literal.String name) -> @@ -1384,6 +1400,21 @@ module rec Unify = | Some MappedModifier.Add -> true | Some MappedModifier.Remove -> false + let! keyType = + match m.NameType with + | Some nameType -> + expandType ctx env ips nameMapping nameType + | None -> Result.Ok keyType + + let propName = + match keyType.Kind with + | TypeKind.Literal(Literal.String name) -> + PropName.String name + | TypeKind.Literal(Literal.Number name) -> + PropName.Number name + | TypeKind.UniqueSymbol id -> PropName.Symbol id + | _ -> failwith $"Invalid key type {keyType}" + return Property { Name = propName @@ -1644,6 +1675,36 @@ module rec Unify = return { Kind = TypeKind.Tuple { Elems = elems; Immutable = immutable } Provenance = None } + | TypeKind.TemplateLiteral { Exprs = elems; Parts = quasis } -> + let! elems = elems |> List.traverseResultM (expand mapping) + + let isLiteral t = + match t.Kind with + | TypeKind.Literal _ -> true + | _ -> false + + // If all `elems` are literals, we can expand the type to a string. + // This is used for property renaming in mapped types. + if List.forall isLiteral elems then + let mutable str = "" + + for elem, quasi in (List.zip elems (List.take elems.Length quasis)) do + match elem.Kind with + | TypeKind.Literal(Literal.String s) -> str <- str + quasi + s + | TypeKind.Literal(Literal.Number n) -> + str <- str + quasi + string (n) + | TypeKind.Literal(Literal.Boolean b) -> + str <- str + quasi + string (b) + | _ -> + failwith "TODO: handle non-literal types in template literal" + + str <- str + List.last quasis + + return + { Kind = TypeKind.Literal(Literal.String str) + Provenance = None } + else + return t | _ -> // Replaces type parameters with their corresponding type arguments // TODO: do this more consistently From 2f960fa205ec979aa0ebb5c42d1456932f5c30ce Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Thu, 22 Aug 2024 07:51:51 -0400 Subject: [PATCH 2/3] handle intrinsics in template literals in expandType --- src/Escalier.TypeChecker.Tests/Tests.fs | 28 +++++++++++++++++++++- src/Escalier.TypeChecker/Unify.fs | 31 ++++++++++++++++++------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/Escalier.TypeChecker.Tests/Tests.fs b/src/Escalier.TypeChecker.Tests/Tests.fs index d772c47..e41359f 100644 --- a/src/Escalier.TypeChecker.Tests/Tests.fs +++ b/src/Escalier.TypeChecker.Tests/Tests.fs @@ -1055,7 +1055,33 @@ let MappedTypeWithTemplateLiteralKey () = Assert.Equal("{_x: number, _y: number}", t.ToString()) } - printfn "result = %A" result + Assert.False(Result.isError result) + +[] +let MappedTypeWithTemplateLiteralKeyWithIntrinsic () = + let result = + result { + + let src = + """ + type Foo = { + [`_${Uppercase}`]: T[K] for K in keyof T, + }; + type Point = {x: number, y: number}; + type Bar = Foo; + """ + + let! ctx, env = inferModule src + + Assert.Empty(ctx.Report.Diagnostics) + + let! t = + expandScheme ctx env None (env.FindScheme "Bar") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("{_X: number, _Y: number}", t.ToString()) + } + Assert.False(Result.isError result) [] diff --git a/src/Escalier.TypeChecker/Unify.fs b/src/Escalier.TypeChecker/Unify.fs index a27a219..f533d01 100644 --- a/src/Escalier.TypeChecker/Unify.fs +++ b/src/Escalier.TypeChecker/Unify.fs @@ -1328,9 +1328,6 @@ module rec Unify = result { match elem with | Mapped m -> - printfn $"TypeParam = {m.TypeParam.Name}" - printfn $"NameType = {m.NameType}" - match m.TypeParam.Constraint.Kind with | TypeKind.KeyOf t -> match t.Kind with @@ -1349,8 +1346,6 @@ module rec Unify = let! c = expandType ctx env ips mapping m.TypeParam.Constraint - printfn $"c = {c}" - // TODO: Document this because I don't remember why we need to // do this. match c.Kind with @@ -1678,14 +1673,18 @@ module rec Unify = | TypeKind.TemplateLiteral { Exprs = elems; Parts = quasis } -> let! elems = elems |> List.traverseResultM (expand mapping) - let isLiteral t = + let isLiteralOrIntrinsic t = match t.Kind with | TypeKind.Literal _ -> true + | TypeKind.IntrinsicInstance { TypeArgs = typeArgs } -> + match typeArgs with + | Some [ { Kind = TypeKind.Literal _ } ] -> true + | _ -> false | _ -> false // If all `elems` are literals, we can expand the type to a string. // This is used for property renaming in mapped types. - if List.forall isLiteral elems then + if List.forall isLiteralOrIntrinsic elems then let mutable str = "" for elem, quasi in (List.zip elems (List.take elems.Length quasis)) do @@ -1695,8 +1694,22 @@ module rec Unify = str <- str + quasi + string (n) | TypeKind.Literal(Literal.Boolean b) -> str <- str + quasi + string (b) - | _ -> - failwith "TODO: handle non-literal types in template literal" + | TypeKind.IntrinsicInstance { Name = name + TypeArgs = Some [ { Kind = TypeKind.Literal(Literal.String s) } ] } -> + match name with + | QualifiedIdent.Ident "Uppercase" -> + str <- str + quasi + s.ToUpper() + | QualifiedIdent.Ident "Lowercase" -> + str <- str + quasi + s.ToLower() + | QualifiedIdent.Ident "Capitalize" -> + str <- + str + quasi + s.[0].ToString().ToUpper() + s.[1..].ToLower() + | QualifiedIdent.Ident "Uncapitalize" -> + str <- + str + quasi + s.[0].ToString().ToLower() + s.[1..].ToUpper() + | _ -> + failwith $"Unsupported intrinsic {name} in template literal" + | _ -> () // should never happen because we pre-filtered the list str <- str + List.last quasis From 5662291bc36490c392dd70e1dded448ec30959d1 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Thu, 22 Aug 2024 08:32:51 -0400 Subject: [PATCH 3/3] handle symbol keys properly --- src/Escalier.TypeChecker.Tests/Tests.fs | 11 +- src/Escalier.TypeChecker/Unify.fs | 225 +++++++++++++----------- 2 files changed, 127 insertions(+), 109 deletions(-) diff --git a/src/Escalier.TypeChecker.Tests/Tests.fs b/src/Escalier.TypeChecker.Tests/Tests.fs index e41359f..c624db2 100644 --- a/src/Escalier.TypeChecker.Tests/Tests.fs +++ b/src/Escalier.TypeChecker.Tests/Tests.fs @@ -1055,6 +1055,7 @@ let MappedTypeWithTemplateLiteralKey () = Assert.Equal("{_x: number, _y: number}", t.ToString()) } + printfn "result = %A" result Assert.False(Result.isError result) [] @@ -1067,7 +1068,7 @@ let MappedTypeWithTemplateLiteralKeyWithIntrinsic () = type Foo = { [`_${Uppercase}`]: T[K] for K in keyof T, }; - type Point = {x: number, y: number}; + type Point = {x: number, y: number, [Symbol.iterator]: number}; type Bar = Foo; """ @@ -2030,7 +2031,7 @@ let InferMappedObjectType () = type Foo = { [K]: T[K][] for K in keyof T }; - type Bar = {a: string, b: number}; + type Bar = {a: string, b: number, [Symbol.iterator]: boolean}; type Baz = Foo; """ @@ -2043,7 +2044,11 @@ let InferMappedObjectType () = expandScheme ctx env None (env.FindScheme "Baz") Map.empty None |> Result.mapError CompileError.TypeError - Assert.Equal("{a: string[], b: number[]}", t.ToString()) + // TODO: maintain the original name of the symbol + Assert.Equal( + "{a: string[], b: number[], [Symbol(147)]: boolean[]}", + t.ToString() + ) } printfn "result = %A" result diff --git a/src/Escalier.TypeChecker/Unify.fs b/src/Escalier.TypeChecker/Unify.fs index f533d01..29bea3e 100644 --- a/src/Escalier.TypeChecker/Unify.fs +++ b/src/Escalier.TypeChecker/Unify.fs @@ -1350,73 +1350,64 @@ module rec Unify = // do this. match c.Kind with | TypeKind.Union types -> - let! elems = - types - |> List.traverseResultM (fun keyType -> - result { - // TODO: handle key renamining - let mutable nameMapping: Map = - Map.empty - - nameMapping <- - Map.add m.TypeParam.Name keyType nameMapping - - let propName = - match keyType.Kind with - | TypeKind.Literal(Literal.String name) -> - PropName.String name - | TypeKind.Literal(Literal.Number name) -> - PropName.Number name - | TypeKind.UniqueSymbol id -> PropName.Symbol id - | _ -> failwith $"Invalid key type {keyType}" - - let typeAnn = m.TypeAnn - - let folder t = - match t.Kind with - | TypeKind.TypeRef({ Name = QualifiedIdent.Ident name }) when - name = m.TypeParam.Name - -> - Some(keyType) - | _ -> None - - let typeAnn = foldType folder typeAnn - let! t = expandType ctx env ips mapping typeAnn - - let optional = - match m.Optional with - | None -> false // TODO: copy value from typeAnn if it's an index access type - | Some MappedModifier.Add -> true - | Some MappedModifier.Remove -> false - - let readonly = - match m.Readonly with - | None -> false // TODO: copy value from typeAnn if it's an index access type - | Some MappedModifier.Add -> true - | Some MappedModifier.Remove -> false - - let! keyType = - match m.NameType with - | Some nameType -> - expandType ctx env ips nameMapping nameType - | None -> Result.Ok keyType - - let propName = - match keyType.Kind with - | TypeKind.Literal(Literal.String name) -> - PropName.String name - | TypeKind.Literal(Literal.Number name) -> - PropName.Number name - | TypeKind.UniqueSymbol id -> PropName.Symbol id - | _ -> failwith $"Invalid key type {keyType}" - - return - Property - { Name = propName - Type = t - Optional = optional - Readonly = readonly } - }) + let mutable elems = [] + + for keyType in types do + let typeAnn = m.TypeAnn + + let folder t = + match t.Kind with + | TypeKind.TypeRef({ Name = QualifiedIdent.Ident name }) when + name = m.TypeParam.Name + -> + Some(keyType) + | _ -> None + + let typeAnn = foldType folder typeAnn + let! t = expandType ctx env ips mapping typeAnn + + let optional = + match m.Optional with + | None -> false // TODO: copy value from typeAnn if it's an index access type + | Some MappedModifier.Add -> true + | Some MappedModifier.Remove -> false + + let readonly = + match m.Readonly with + | None -> false // TODO: copy value from typeAnn if it's an index access type + | Some MappedModifier.Add -> true + | Some MappedModifier.Remove -> false + + let mutable nameMapping: Map = Map.empty + + nameMapping <- + Map.add m.TypeParam.Name keyType nameMapping + + let! keyType = + match m.NameType with + | Some nameType -> + expandType ctx env ips nameMapping nameType + | None -> Result.Ok keyType + + let propName = + match keyType.Kind with + | TypeKind.Literal(Literal.String name) -> + Some(PropName.String name) + | TypeKind.Literal(Literal.Number name) -> + Some(PropName.Number name) + | TypeKind.UniqueSymbol id -> Some(PropName.Symbol id) + | _ -> None + + match propName with + | Some propName -> + elems <- + elems + @ [ Property + { Name = propName + Type = t + Optional = optional + Readonly = readonly } ] + | _ -> () // Omits entries with other key types return elems | TypeKind.Literal(Literal.String key) -> @@ -1673,51 +1664,73 @@ module rec Unify = | TypeKind.TemplateLiteral { Exprs = elems; Parts = quasis } -> let! elems = elems |> List.traverseResultM (expand mapping) - let isLiteralOrIntrinsic t = + // TODO: check for other types that can't appear within template literal types + let isSymbol t = match t.Kind with - | TypeKind.Literal _ -> true - | TypeKind.IntrinsicInstance { TypeArgs = typeArgs } -> - match typeArgs with - | Some [ { Kind = TypeKind.Literal _ } ] -> true - | _ -> false + | TypeKind.UniqueSymbol _ -> true | _ -> false - // If all `elems` are literals, we can expand the type to a string. - // This is used for property renaming in mapped types. - if List.forall isLiteralOrIntrinsic elems then - let mutable str = "" - - for elem, quasi in (List.zip elems (List.take elems.Length quasis)) do - match elem.Kind with - | TypeKind.Literal(Literal.String s) -> str <- str + quasi + s - | TypeKind.Literal(Literal.Number n) -> - str <- str + quasi + string (n) - | TypeKind.Literal(Literal.Boolean b) -> - str <- str + quasi + string (b) - | TypeKind.IntrinsicInstance { Name = name - TypeArgs = Some [ { Kind = TypeKind.Literal(Literal.String s) } ] } -> - match name with - | QualifiedIdent.Ident "Uppercase" -> - str <- str + quasi + s.ToUpper() - | QualifiedIdent.Ident "Lowercase" -> - str <- str + quasi + s.ToLower() - | QualifiedIdent.Ident "Capitalize" -> - str <- - str + quasi + s.[0].ToString().ToUpper() + s.[1..].ToLower() - | QualifiedIdent.Ident "Uncapitalize" -> - str <- - str + quasi + s.[0].ToString().ToLower() + s.[1..].ToUpper() - | _ -> - failwith $"Unsupported intrinsic {name} in template literal" - | _ -> () // should never happen because we pre-filtered the list - - str <- str + List.last quasis - + if List.exists isSymbol elems then + // We don't bother reporting an error because template literal types + // are only expanded when renaming properties in mapped types. + // Returning `never` is fine because the mapped type expansion will + // filter out any keys whose type is `never`. return - { Kind = TypeKind.Literal(Literal.String str) + { Kind = TypeKind.Keyword Keyword.Never Provenance = None } else - return t + let isLiteralOrIntrinsic t = + match t.Kind with + | TypeKind.Literal _ -> true + | TypeKind.IntrinsicInstance { TypeArgs = typeArgs } -> + match typeArgs with + | Some [ { Kind = TypeKind.Literal _ } ] -> true + | _ -> false + | _ -> false + + // If all `elems` are literals, we can expand the type to a string. + // This is used for property renaming in mapped types. + if List.forall isLiteralOrIntrinsic elems then + let mutable str = "" + + for elem, quasi in + (List.zip elems (List.take elems.Length quasis)) do + match elem.Kind with + | TypeKind.Literal(Literal.String s) -> str <- str + quasi + s + | TypeKind.Literal(Literal.Number n) -> + str <- str + quasi + string (n) + | TypeKind.Literal(Literal.Boolean b) -> + str <- str + quasi + string (b) + | TypeKind.IntrinsicInstance { Name = name + TypeArgs = Some [ { Kind = TypeKind.Literal(Literal.String s) } ] } -> + match name with + | QualifiedIdent.Ident "Uppercase" -> + str <- str + quasi + s.ToUpper() + | QualifiedIdent.Ident "Lowercase" -> + str <- str + quasi + s.ToLower() + | QualifiedIdent.Ident "Capitalize" -> + str <- + str + + quasi + + s.[0].ToString().ToUpper() + + s.[1..].ToLower() + | QualifiedIdent.Ident "Uncapitalize" -> + str <- + str + + quasi + + s.[0].ToString().ToLower() + + s.[1..].ToUpper() + | _ -> + failwith $"Unsupported intrinsic {name} in template literal" + | _ -> () // should never happen because we pre-filtered the list + + str <- str + List.last quasis + + return + { Kind = TypeKind.Literal(Literal.String str) + Provenance = None } + else + return t | _ -> // Replaces type parameters with their corresponding type arguments // TODO: do this more consistently