Skip to content

Commit

Permalink
Merge all readonly types (#340)
Browse files Browse the repository at this point in the history
* Merge readonly types with their mutable versions

* treate more interfaces as 'readonly'
  • Loading branch information
kevinbarabash committed Aug 17, 2024
1 parent 9e88903 commit 5c115c4
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 42 deletions.
44 changes: 22 additions & 22 deletions src/Escalier.Compiler/Prelude.fs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ module Prelude =
let libs =
[ "lib.es5.d.ts"
"lib.es2015.core.d.ts"
"lib.es2015.collection.d.ts"
"lib.es2015.symbol.d.ts"
"lib.es2015.symbol.wellknown.d.ts"
"lib.es2015.iterable.d.ts"
Expand All @@ -534,28 +535,27 @@ module Prelude =
let! env, _ = inferLib ctx newEnv fullPath
newEnv <- env

// TODO: look for more (Readonly)Foo pairs once we parse lib.es6.d.ts and
// future versions of the JavaScript standard library type defs
match
newEnv.TryFindScheme "ReadonlyArray", newEnv.TryFindScheme "Array"
with
| Some(readonlyArray), Some(array) ->
// TODO: Merge ReadonlyFoo and Foo as part Escalier.Interop.Migrate
let merged = QualifiedGraph.mergeType readonlyArray.Type array.Type
newEnv <- newEnv.AddScheme "Array" { array with Type = merged }

// TODO: for type definitions using Array and ReadonlyArray we need to
// make sure that params are marked with `mut` appropriately and all
// references to ReadonlyArray must be replaced with Array

// TODO: make it so that we can only use `ReadonlyArray` from .d.ts files
// newEnv <-
// { newEnv with
// Namespace =
// { newEnv.Namespace with
// Schemes = newEnv.Namespace.Schemes.Remove "ReadonlyArray" } }
()
| _ -> ()
// TODO: handle schemes within namespaces
let readonlySchemes =
newEnv.Namespace.Schemes
|> Map.filter (fun k _ ->
(k.StartsWith "Readonly" || k.EndsWith "ReadOnly")
&& k <> "Readonly")

for KeyValue(readonlyName, readonlyScheme) in readonlySchemes do
let name =
readonlyName.Replace("Readonly", "").Replace("ReadOnly", "")

match newEnv.TryFindScheme name with
| Some(scheme) ->
let merged =
QualifiedGraph.mergeType readonlyScheme.Type scheme.Type

// TODO: track which TypeScript interface decls each of the properties
// come from in the merged type.
newEnv <- newEnv.AddScheme name { scheme with Type = merged }
()
| _ -> ()

let result = Result.Ok(ctx, newEnv)
memoizedEnvAndCtx <- memoizedEnvAndCtx.Add(baseDir, result)
Expand Down
4 changes: 2 additions & 2 deletions src/Escalier.Interop.Tests/Migrate.fs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ let ParseAndInferInterface () =
Assert.Type(
env,
"Foo",
"{bar fn (self: Self) -> number, baz fn (self: Self, mut x: string) -> boolean, get qux fn () -> string, set qux fn (mut x: string) -> undefined, ...}"
"{bar fn (mut self: Self) -> number, baz fn (mut self: Self, mut x: string) -> boolean, get qux fn () -> string, set qux fn (mut x: string) -> undefined, ...}"
)
}

Expand Down Expand Up @@ -195,7 +195,7 @@ let ParseAndInferUnorderedTypeParams () =
Assert.Type(
env,
"MyObjectConstructor",
"{freeze fn <T: {[idx]+?: U | null | undefined | object for idx in string, ...}, U: string | bigint | number | boolean | symbol>(self: Self, mut o: T) -> Readonly<T>, ...}"
"{freeze fn <T: {[idx]+?: U | null | undefined | object for idx in string, ...}, U: string | bigint | number | boolean | symbol>(mut self: Self, mut o: T) -> Readonly<T>, ...}"
)
}

Expand Down
71 changes: 53 additions & 18 deletions src/Escalier.Interop/Migrate.fs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ module rec Migrate =

expr

let makeSelfFuncParam () : FuncParam =
let makeSelfFuncParam (isMutable: bool) : FuncParam =
let ident =
{ Name = "self"
IsMut = false
IsMut = isMutable
Assertion = None }

let pattern: Syntax.Pattern =
Expand Down Expand Up @@ -126,7 +126,10 @@ module rec Migrate =

self

let migrateTypeElement (elem: TsTypeElement) : Syntax.ObjTypeAnnElem =
let migrateTypeElement
(isMutable: bool)
(elem: TsTypeElement)
: Syntax.ObjTypeAnnElem =
match elem with
| TsCallSignatureDecl { Params = fnParams
TypeAnn = typeAnn
Expand All @@ -143,8 +146,8 @@ module rec Migrate =

let f: FuncSig =
{ TypeParams = typeParams
Self = Some(makeSelfFuncParam ())
ParamList = List.map migrateFnParam fnParams
Self = Some(makeSelfFuncParam isMutable)
ParamList = List.map (migrateFnParam isMutable) fnParams
ReturnType = Some retType
Throws = None
IsAsync = false }
Expand All @@ -165,8 +168,8 @@ module rec Migrate =

let f: FuncSig =
{ TypeParams = typeParams
Self = Some(makeSelfFuncParam ())
ParamList = List.map migrateFnParam fnParams
Self = Some(makeSelfFuncParam isMutable)
ParamList = List.map (migrateFnParam isMutable) fnParams
ReturnType = Some retType
Throws = None
IsAsync = false }
Expand Down Expand Up @@ -209,8 +212,8 @@ module rec Migrate =

let f: FuncSig =
{ TypeParams = typeParams
Self = Some(makeSelfFuncParam ())
ParamList = List.map migrateFnParam fnParams
Self = Some(makeSelfFuncParam isMutable)
ParamList = List.map (migrateFnParam isMutable) fnParams
ReturnType = Some retType
Throws = None
IsAsync = false }
Expand Down Expand Up @@ -269,7 +272,8 @@ module rec Migrate =
Param = fnParam
Optional = _optional
Computed = _computed } ->
let fnParam = migrateFnParam fnParam
// TODO: warn if there's a setter on a Readonly interface
let fnParam = migrateFnParam isMutable fnParam

let undefined: Syntax.TypeAnn =
{ Kind = Syntax.Keyword KeywordTypeAnn.Undefined
Expand Down Expand Up @@ -317,6 +321,9 @@ module rec Migrate =
TypeRef.TypeArgs = None }
|> TypeRef
| TsType.TsFnOrConstructorType tsFnOrConstructorType ->
// Assumes the entire object is mutable
let isMutable = true

match tsFnOrConstructorType with
| TsFnType f ->
// TsFnType is shorthand for an object with only a callable signature
Expand All @@ -326,7 +333,8 @@ module rec Migrate =
List.map migrateTypeParam tpd.Params)
f.TypeParams

let paramList: list<FuncParam> = List.map migrateFnParam f.Params
let paramList: list<FuncParam> =
List.map (migrateFnParam isMutable) f.Params

let fnType: FuncSig =
{ TypeParams = typeParams
Expand All @@ -345,7 +353,7 @@ module rec Migrate =
List.map migrateTypeParam tpd.Params)
f.TypeParams

let paramList = List.map migrateFnParam f.Params
let paramList = List.map (migrateFnParam isMutable) f.Params

let fnType: FuncSig =
{ TypeParams = typeParams
Expand Down Expand Up @@ -376,7 +384,11 @@ module rec Migrate =

TypeAnnKind.Typeof name
| TsType.TsTypeLit { Members = members } ->
let elems: list<ObjTypeAnnElem> = List.map migrateTypeElement members
// Assumes that all methods on object types are mutable
let isMutable = true

let elems: list<ObjTypeAnnElem> =
List.map (migrateTypeElement isMutable) members

TypeAnnKind.Object
{ Elems = elems
Expand Down Expand Up @@ -507,21 +519,27 @@ module rec Migrate =
Default = Option.map migrateType typeParam.Default
Span = DUMMY_SPAN }

let migrateFnParam (fnParam: TypeScript.TsFnParam) : Syntax.FuncParam =
{ Pattern = migrateFnParamPattern fnParam.Pat
let migrateFnParam
(isMutable: bool)
(fnParam: TypeScript.TsFnParam)
: Syntax.FuncParam =
{ Pattern = migrateFnParamPattern isMutable fnParam.Pat
TypeAnn =
match fnParam.TypeAnn with
| Some typeAnn -> Some(migrateTypeAnn typeAnn)
| None -> failwith "all function parameters must have a type annotation"
Optional = fnParam.Optional }

let migrateFnParamPattern (pat: TypeScript.TsFnParamPat) : Pattern =
let migrateFnParamPattern
(isMutable: bool)
(pat: TypeScript.TsFnParamPat)
: Pattern =
let kind =
match pat with
| TsFnParamPat.Ident { Id = ident } ->
PatternKind.Ident
{ Name = ident.Name
IsMut = true
IsMut = isMutable
Assertion = None }
| TsFnParamPat.Object { Props = props } ->
let elems: list<ObjPatElem> =
Expand Down Expand Up @@ -950,7 +968,24 @@ module rec Migrate =
(fun (tpd: TsTypeParamDecl) -> List.map migrateTypeParam tpd.Params)
typeParams

let elems = List.map migrateTypeElement body.Body
let isReadonly =
ident.Name.StartsWith "Readonly"
|| ident.Name.EndsWith "ReadOnly"
|| (List.contains
ident.Name
[ "Boolean"
"Number"
"String"
"Symbol"
"BigInt"
// TODO: Track what namespace we're inside of if any
// TODO: Track what node_module we're inside of it any
// TODO: Maintain a full qualified list of interfaces that are
// known to be "readonly" like `Intl.NumberFormatOptions`
"NumberFormat" ])

let isMutable = not isReadonly
let elems = List.map (migrateTypeElement isMutable) body.Body

let extends: option<list<TypeRef>> =
match extends with
Expand Down
2 changes: 2 additions & 0 deletions src/Escalier.TypeChecker/Infer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3740,6 +3740,8 @@ module rec Infer =
| TypeKind.Object { Elems = existingElems },
TypeKind.Object { Elems = newElems } ->
// TODO: remove duplicates
// TODO: track which TypeScript interface decls each of the properties
// come from in the merged type.
let mergedElems = existingElems @ newElems

let kind =
Expand Down

0 comments on commit 5c115c4

Please sign in to comment.