Skip to content

Commit

Permalink
Implement key renaming in mapped types (#344)
Browse files Browse the repository at this point in the history
* Implement key renaming in mapped types

* handle intrinsics in template literals in expandType

* handle symbol keys properly
  • Loading branch information
kevinbarabash committed Aug 22, 2024
1 parent 54bfd8e commit ea2b19b
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 54 deletions.
9 changes: 8 additions & 1 deletion src/Escalier.Data/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
114 changes: 107 additions & 7 deletions src/Escalier.TypeChecker.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -863,20 +863,29 @@ let InferTemplateLiteralTypeError () =
Assert.True(Result.isError result)

[<Fact>]
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)

[<Fact>]
let InferIntrinsicsBasicUsageLiterals () =
Expand Down Expand Up @@ -989,6 +998,93 @@ let InferIntrinsicsInTemplateLiteralTypesWithUnion () =
printfn "result = %A" result
Assert.False(Result.isError result)

[<Fact>]
let KebabTemplateLiteralType () =
let result =
result {

let src =
"""
type Kebab<T: string, U: string> = `-${T}-${U}-`;
type Foo = Kebab<"hello", "world">;
type Bar = Kebab<string, number>;
"""

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)

[<Fact>]
let MappedTypeWithTemplateLiteralKey () =
let result =
result {

let src =
"""
type Foo<T> = {
[`_${K}`]: T[K] for K in keyof T,
};
type Point = {x: number, y: number};
type Bar = Foo<Point>;
"""

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)

[<Fact>]
let MappedTypeWithTemplateLiteralKeyWithIntrinsic () =
let result =
result {

let src =
"""
type Foo<T> = {
[`_${Uppercase<K>}`]: T[K] for K in keyof T,
};
type Point = {x: number, y: number, [Symbol.iterator]: number};
type Bar = Foo<Point>;
"""

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)

[<Fact>]
let InferUnaryOperations () =
let result =
Expand Down Expand Up @@ -1935,7 +2031,7 @@ let InferMappedObjectType () =
type Foo<T> = {
[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<Bar>;
"""

Expand All @@ -1948,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
Expand Down
179 changes: 133 additions & 46 deletions src/Escalier.TypeChecker/Unify.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1346,51 +1350,64 @@ module rec Unify =
// do this.
match c.Kind with
| TypeKind.Union types ->
let! elems =
types
|> List.traverseResultM (fun keyType ->
result {
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

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<string, Type> = 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) ->
Expand Down Expand Up @@ -1644,6 +1661,76 @@ 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)

// TODO: check for other types that can't appear within template literal types
let isSymbol t =
match t.Kind with
| TypeKind.UniqueSymbol _ -> true
| _ -> false

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.Keyword Keyword.Never
Provenance = None }
else
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
Expand Down

0 comments on commit ea2b19b

Please sign in to comment.