Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement key renaming in mapped types #344

Merged
merged 3 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading