From 1107c1501a8cdef41796dcd37884984c9688f21b Mon Sep 17 00:00:00 2001 From: imteekay Date: Mon, 17 Jul 2023 19:13:14 -0300 Subject: [PATCH 01/23] Some ideas for object types --- draft.md => draft/index.md | 0 draft/object-types.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename draft.md => draft/index.md (100%) create mode 100644 draft/object-types.md diff --git a/draft.md b/draft/index.md similarity index 100% rename from draft.md rename to draft/index.md diff --git a/draft/object-types.md b/draft/object-types.md new file mode 100644 index 0000000..e69de29 From 446d0f380306042dfaa76c6c595c4c72bb5eeca9 Mon Sep 17 00:00:00 2001 From: imteekay Date: Mon, 17 Jul 2023 19:16:25 -0300 Subject: [PATCH 02/23] Add types --- src/types.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/types.ts b/src/types.ts index 246fe29..3e76c69 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,8 @@ export enum Token { Comma = 'Comma', Whitespace = 'Whitespace', String = 'String', + OpenBrace = 'OpenBrace', + CloseBrace = 'CloseBrace', Unknown = 'Unknown', BOF = 'BOF', EOF = 'EOF', @@ -34,6 +36,7 @@ export enum Node { Var, Let, TypeAlias, + TypeLiteral, StringLiteral, EmptyStatement, VariableStatement, @@ -128,6 +131,23 @@ export type TypeAlias = Location & { typename: Identifier; }; +type TypeReference = Location & { + name: Identifier; +}; + +type PropertySignature = Location & { + name: Identifier; + type: Identifier | TypeReference; +}; + +// Added it to member as it's possible to have other types of members. e.g. method signature +type Member = PropertySignature; + +export type TypeLiteral = Location & { + kind: Node.TypeLiteral; + members: Member[]; +}; + export type EmptyStatement = { kind: Node.EmptyStatement; }; From de8582bcf88e385909c3a7a9f9b844fc6ab5b6b0 Mon Sep 17 00:00:00 2001 From: imteekay Date: Mon, 17 Jul 2023 19:16:35 -0300 Subject: [PATCH 03/23] Package lock --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 432cd4f..b7673b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "mini-typescript", "version": "1.0.0", "license": "MIT", "devDependencies": { From 273828e0ecd54639462b5167d9cd0848a6904786 Mon Sep 17 00:00:00 2001 From: imteekay Date: Mon, 17 Jul 2023 19:17:10 -0300 Subject: [PATCH 04/23] Add open and close brace tokens for the lexer --- src/lex.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lex.ts b/src/lex.ts index b091099..000905f 100644 --- a/src/lex.ts +++ b/src/lex.ts @@ -59,6 +59,12 @@ export function lex(s: string): Lexer { case ',': token = Token.Comma; break; + case '{': + token = Token.OpenBrace; + break; + case '}': + token = Token.CloseBrace; + break; default: token = Token.Unknown; break; From 4e1c4048dc644dd1b650d0f84664ee942482a68c Mon Sep 17 00:00:00 2001 From: imteekay Date: Wed, 2 Aug 2023 08:30:14 -0300 Subject: [PATCH 05/23] Design: add examples and AST types --- draft/object-types.md | 156 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/draft/object-types.md b/draft/object-types.md index e69de29..7ab2f43 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -0,0 +1,156 @@ +# Object Types + +Example + +```ts +type A = string; +type B = number; +type C = { + a: string; + b: number; +}; + +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; +}; +``` + +A type alias can be + +- A literal type (string, number) +- An object type with type literals +- An object type with type literals and identifiers +- An object type with type literals, identifiers, and other object types + +## Type Alias: A literal type (string, number) + +```ts +type A = string; +type B = number; +``` + +AST: + +```ts +{ + kind: TypeAliasDeclaration, + name: Identifier, + type: string | number +} +``` + +## Type Alias: An object type with type literals + +```ts +type C = { + a: string; + b: number; +}; +``` + +AST: + +```ts +{ + kind: TypeAliasDeclaration, + name: Identifier, + type: { + kind: TypeLiteral, + members: { + kind: PropertySignature + name: Identifier, + type: string | number + }[] + } +} +``` + +## Type Alias: An object type with type literals and identifiers + +```ts +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; +}; +``` + +AST: + +```ts +// TypeAliasDeclaration +{ + kind: TypeAliasDeclaration, + name: Identifier, + type: { + kind: TypeLiteral, + members: { + kind: PropertySignature + name: Identifier, + type: string | number | TypeReference + }[] + } +} + +// TypeReference +{ + kind: TypeReference + typename: Identifier +} +``` + +## Type Alias: An object type with type literals, identifiers, and other object types + +```ts +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; + f: { + foo: string; + bar: number; + }; +}; +``` + +AST: + +```ts +// TypeAliasDeclaration +{ + kind: TypeAliasDeclaration, + name: Identifier, + type: { + kind: TypeLiteral, + members: { + kind: PropertySignature + name: Identifier, + type: string | number | TypeReference | TypeLiteral + }[] + } +} + +// TypeReference +{ + kind: TypeReference + typename: Identifier +} + +// TypeLiteral +{ + kind: TypeLiteral, + members: { + kind: PropertySignature + name: Identifier, + type: string | number | TypeReference | TypeLiteral + }[] +} +``` From 5d218ebd899585d01bf4384d493e05b8a415a195 Mon Sep 17 00:00:00 2001 From: imteekay Date: Wed, 2 Aug 2023 08:38:13 -0300 Subject: [PATCH 06/23] Alias `typename` can be Identifier or TypeLiteral --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index d44ce9b..94a7c57 100644 --- a/src/types.ts +++ b/src/types.ts @@ -127,7 +127,7 @@ export type Let = Location & { export type TypeAlias = Location & { kind: Node.TypeAlias; name: Identifier; - typename: Identifier; + typename: Identifier | TypeLiteral; }; type TypeReference = Location & { From ed7e40df7b33be16053632d8bac1d4915872e693 Mon Sep 17 00:00:00 2001 From: imteekay Date: Thu, 3 Aug 2023 09:07:46 -0300 Subject: [PATCH 07/23] Parse TypeLiteral --- src/parse.ts | 36 +++++++++++++++++++++++++++++++++--- src/types.ts | 6 +----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index de5b84f..72b3ef0 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -10,6 +10,7 @@ import { VariableDeclaration, NodeFlags, VariableStatement, + TypeLiteral, } from './types'; import { error } from './error'; @@ -19,7 +20,7 @@ export function parse(lexer: Lexer): Module { function parseModule(): Module { return { - statements: parseStatements( + statements: parseList( parseStatement, () => tryParseToken(Token.Semicolon), () => lexer.token() !== Token.EOF, @@ -68,6 +69,33 @@ export function parse(lexer: Lexer): Module { return { kind: Node.Identifier, text: '(missing)', pos: e.pos }; } + function parseMember() { + const pos = lexer.pos(); + const name = parseIdentifier(); + parseExpected(Token.Colon); + const typename = tryParseToken(Token.OpenBrace) + ? parseTypeLiteral() + : parseIdentifier(); + + return { + name, + typename, + pos, + }; + } + + function parseTypeLiteral(): TypeLiteral { + return { + kind: Node.TypeLiteral, + members: parseList( + parseMember, + () => tryParseToken(Token.Semicolon), + () => tryParseToken(Token.CloseBrace), + ), + pos: lexer.pos(), + }; + } + function parseStatement(): Statement { const pos = lexer.pos(); @@ -78,7 +106,9 @@ export function parse(lexer: Lexer): Module { } else if (tryParseToken(Token.Type)) { const name = parseIdentifier(); parseExpected(Token.Equals); - const typename = parseIdentifier(); + const typename = tryParseToken(Token.OpenBrace) + ? parseTypeLiteral() + : parseIdentifier(); return { kind: Node.TypeAlias, name, typename, pos }; } else if (tryParseToken(Token.Semicolon)) { return { kind: Node.EmptyStatement }; @@ -139,7 +169,7 @@ export function parse(lexer: Lexer): Module { } } - function parseStatements( + function parseList( element: () => T, terminator: () => boolean, peek: () => boolean, diff --git a/src/types.ts b/src/types.ts index 94a7c57..3982230 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,13 +130,9 @@ export type TypeAlias = Location & { typename: Identifier | TypeLiteral; }; -type TypeReference = Location & { - name: Identifier; -}; - type PropertySignature = Location & { name: Identifier; - type: Identifier | TypeReference; + typename: Identifier | TypeLiteral; }; // Added it to member as it's possible to have other types of members. e.g. method signature From 11b65418090d986c9676adfbceebaa7260306a99 Mon Sep 17 00:00:00 2001 From: imteekay Date: Sat, 5 Aug 2023 14:50:16 -0300 Subject: [PATCH 08/23] Continue parsing type literal members while the current token is not a CloseBrace token --- src/parse.ts | 5 +++-- src/types.ts | 2 +- tests/objectTypes.ts | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 tests/objectTypes.ts diff --git a/src/parse.ts b/src/parse.ts index 72b3ef0..9899806 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -11,6 +11,7 @@ import { NodeFlags, VariableStatement, TypeLiteral, + Member, } from './types'; import { error } from './error'; @@ -69,7 +70,7 @@ export function parse(lexer: Lexer): Module { return { kind: Node.Identifier, text: '(missing)', pos: e.pos }; } - function parseMember() { + function parseMember(): Member { const pos = lexer.pos(); const name = parseIdentifier(); parseExpected(Token.Colon); @@ -90,7 +91,7 @@ export function parse(lexer: Lexer): Module { members: parseList( parseMember, () => tryParseToken(Token.Semicolon), - () => tryParseToken(Token.CloseBrace), + () => !tryParseToken(Token.CloseBrace), ), pos: lexer.pos(), }; diff --git a/src/types.ts b/src/types.ts index 3982230..f54a254 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,7 +136,7 @@ type PropertySignature = Location & { }; // Added it to member as it's possible to have other types of members. e.g. method signature -type Member = PropertySignature; +export type Member = PropertySignature; export type TypeLiteral = Location & { kind: Node.TypeLiteral; diff --git a/tests/objectTypes.ts b/tests/objectTypes.ts new file mode 100644 index 0000000..8aab0ec --- /dev/null +++ b/tests/objectTypes.ts @@ -0,0 +1,6 @@ +type A = string; +type B = number; +type C = { + a: string; + b: number; +}; From 78f274e2754664d2351a315f6d19063a2d399906 Mon Sep 17 00:00:00 2001 From: imteekay Date: Sat, 5 Aug 2023 14:52:16 -0300 Subject: [PATCH 09/23] Adding an example on nested object type literal --- tests/objectTypes.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/objectTypes.ts b/tests/objectTypes.ts index 8aab0ec..ac3de44 100644 --- a/tests/objectTypes.ts +++ b/tests/objectTypes.ts @@ -4,3 +4,15 @@ type C = { a: string; b: number; }; + +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; + f: { + foo: string; + bar: number; + }; +}; \ No newline at end of file From a73ec99576f4c10073357645ffb11d99a1fe5866 Mon Sep 17 00:00:00 2001 From: imteekay Date: Tue, 8 Aug 2023 08:45:49 -0300 Subject: [PATCH 10/23] Better symbol naming and add comment --- src/check.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/check.ts b/src/check.ts index afb14e8..84597f9 100644 --- a/src/check.ts +++ b/src/check.ts @@ -92,15 +92,16 @@ export function check(module: Module) { function checkVariableDeclaration(declaration: VariableDeclaration) { const initType = checkExpression(declaration.init); - const symbol = resolve( + const varSymbol = resolve( module.locals, declaration.name.text, SymbolFlags.FunctionScopedVariable, ); - if (symbol && declaration !== symbol.valueDeclaration) { + // handle subsequent variable declarations types — generate an error if it has type mismatches + if (varSymbol && declaration !== varSymbol.valueDeclaration) { const valueDeclarationType = checkVariableDeclarationType( - symbol.valueDeclaration!, + varSymbol.valueDeclaration!, ); const type = declaration.typename From cb4b245e48508c3f9313775e0f36bdaa839f3ccb Mon Sep 17 00:00:00 2001 From: TK Date: Mon, 28 Aug 2023 09:03:12 -0300 Subject: [PATCH 11/23] Parse object literals --- src/parse.ts | 32 +++++++++++++++++++++++++++++++- src/types.ts | 20 +++++++++++++++++++- tests/objectTypes.ts | 19 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 39ccb16..c2a1a87 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -11,6 +11,9 @@ import { VariableStatement, TypeLiteral, Member, + ObjectLiteralExpression, + PropertyAssignment, + IdentifierOrLiteral, } from './types'; import { error } from './error'; @@ -38,7 +41,7 @@ export function parse(lexer: Lexer): Module { return e; } - function parseIdentifierOrLiteral(): Expression { + function parseIdentifierOrLiteral(): IdentifierOrLiteral { const pos = lexer.pos(); if (tryParseToken(Token.Identifier)) { return { kind: Node.Identifier, text: lexer.text(), pos }; @@ -51,6 +54,8 @@ export function parse(lexer: Lexer): Module { pos, isSingleQuote: lexer.isSingleQuote(), }; + } else if (tryParseToken(Token.OpenBrace)) { + return parseObjectLiteral(); } error( pos, @@ -69,6 +74,31 @@ export function parse(lexer: Lexer): Module { return { kind: Node.Identifier, text: '(missing)', pos: e.pos }; } + function parseProperty(): PropertyAssignment { + const pos = lexer.pos(); + const name = parseIdentifier(); + parseExpected(Token.Colon); + const init = parseIdentifierOrLiteral(); + + return { + name, + init, + pos, + }; + } + + function parseObjectLiteral(): ObjectLiteralExpression { + return { + kind: Node.ObjectLiteralExpression, + properties: parseList( + parseProperty, + () => tryParseToken(Token.Comma), + () => !tryParseToken(Token.CloseBrace), + ), + pos: lexer.pos(), + }; + } + function parseMember(): Member { const pos = lexer.pos(); const name = parseIdentifier(); diff --git a/src/types.ts b/src/types.ts index da2941d..cfdeb5d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,7 @@ export enum Node { VariableStatement, VariableDeclarationList, VariableDeclaration, + ObjectLiteralExpression, } export type Error = { @@ -57,7 +58,14 @@ export type Expression = | Identifier | NumericLiteral | Assignment - | StringLiteral; + | StringLiteral + | ObjectLiteralExpression; + +export type IdentifierOrLiteral = + | Identifier + | StringLiteral + | NumericLiteral + | ObjectLiteralExpression; export type Identifier = Location & { kind: Node.Identifier; @@ -81,6 +89,16 @@ export type Assignment = Location & { value: Expression; }; +export type PropertyAssignment = Location & { + name: Identifier | StringLiteral | NumericLiteral; + init: IdentifierOrLiteral; +}; + +export type ObjectLiteralExpression = Location & { + kind: Node.ObjectLiteralExpression; + properties: PropertyAssignment[]; +}; + export type Statement = | ExpressionStatement | TypeAlias diff --git a/tests/objectTypes.ts b/tests/objectTypes.ts index ac3de44..6279fdd 100644 --- a/tests/objectTypes.ts +++ b/tests/objectTypes.ts @@ -15,4 +15,23 @@ type D = { foo: string; bar: number; }; +}; + +var a: A = 'string'; +var b: B = 11; +var c: C = { + a: a, + b: b +}; + +var d: D = { + a: a, + b: b, + c: a, + d: b, + e: c, + f: { + foo: 'string', + bar: 10 + } }; \ No newline at end of file From c7fb42a1af2a341a35e69fe370f10535477eed6d Mon Sep 17 00:00:00 2001 From: TK Date: Mon, 28 Aug 2023 09:08:59 -0300 Subject: [PATCH 12/23] Add AST example for object literals --- draft/object-types.md | 399 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) diff --git a/draft/object-types.md b/draft/object-types.md index 7ab2f43..997d023 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -154,3 +154,402 @@ AST: }[] } ``` + +## Object Literals + +```ts +type A = string; +type B = number; +type C = { + a: string; + b: number; +}; + +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; + f: { + foo: string; + bar: number; + }; +}; + +var a: A = 'string'; +var b: B = 11; +var c: C = { + a: a, + b: b, +}; + +var d: D = { + a: a, + b: b, + c: a, + d: b, + e: c, + f: { + foo: 'string', + bar: 10, + }, +}; +``` + +It produces this AST + +```ts +{ + "statements": [ + { + "kind": "TypeAlias", + "name": { + "kind": "Identifier", + "text": "A" + }, + "typename": { + "kind": "Identifier", + "text": "string" + } + }, + { + "kind": "TypeAlias", + "name": { + "kind": "Identifier", + "text": "B" + }, + "typename": { + "kind": "Identifier", + "text": "number" + } + }, + { + "kind": "TypeAlias", + "name": { + "kind": "Identifier", + "text": "C" + }, + "typename": { + "kind": "TypeLiteral", + "members": { + "0": { + "name": { + "kind": "Identifier", + "text": "a" + }, + "typename": { + "kind": "Identifier", + "text": "string" + } + }, + "1": { + "name": { + "kind": "Identifier", + "text": "b" + }, + "typename": { + "kind": "Identifier", + "text": "number" + } + } + } + } + }, + { + "kind": "TypeAlias", + "name": { + "kind": "Identifier", + "text": "D" + }, + "typename": { + "kind": "TypeLiteral", + "members": { + "0": { + "name": { + "kind": "Identifier", + "text": "a" + }, + "typename": { + "kind": "Identifier", + "text": "string" + } + }, + "1": { + "name": { + "kind": "Identifier", + "text": "b" + }, + "typename": { + "kind": "Identifier", + "text": "number" + } + }, + "2": { + "name": { + "kind": "Identifier", + "text": "c" + }, + "typename": { + "kind": "Identifier", + "text": "A" + } + }, + "3": { + "name": { + "kind": "Identifier", + "text": "d" + }, + "typename": { + "kind": "Identifier", + "text": "B" + } + }, + "4": { + "name": { + "kind": "Identifier", + "text": "e" + }, + "typename": { + "kind": "Identifier", + "text": "C" + } + }, + "5": { + "name": { + "kind": "Identifier", + "text": "f" + }, + "typename": { + "kind": "TypeLiteral", + "members": { + "0": { + "name": { + "kind": "Identifier", + "text": "foo" + }, + "typename": { + "kind": "Identifier", + "text": "string" + } + }, + "1": { + "name": { + "kind": "Identifier", + "text": "bar" + }, + "typename": { + "kind": "Identifier", + "text": "number" + } + } + } + } + } + } + } + }, + { + "kind": "VariableStatement", + "declarationList": { + "kind": "VariableDeclarationList", + "declarations": { + "0": { + "kind": "VariableDeclaration", + "name": { + "kind": "Identifier", + "text": "a" + }, + "typename": { + "kind": "Identifier", + "text": "A" + }, + "init": { + "kind": "StringLiteral", + "value": "string", + "isSingleQuote": true + } + } + }, + "flags": 0 + } + }, + { + "kind": "VariableStatement", + "declarationList": { + "kind": "VariableDeclarationList", + "declarations": { + "0": { + "kind": "VariableDeclaration", + "name": { + "kind": "Identifier", + "text": "b" + }, + "typename": { + "kind": "Identifier", + "text": "B" + }, + "init": { + "kind": "NumericLiteral", + "value": 11 + } + } + }, + "flags": 0 + } + }, + { + "kind": "VariableStatement", + "declarationList": { + "kind": "VariableDeclarationList", + "declarations": { + "0": { + "kind": "VariableDeclaration", + "name": { + "kind": "Identifier", + "text": "c" + }, + "typename": { + "kind": "Identifier", + "text": "C" + }, + "init": { + "kind": "ObjectLiteralExpression", + "properties": { + "0": { + "name": { + "kind": "Identifier", + "text": "a" + }, + "init": { + "kind": "Identifier", + "text": "a" + } + }, + "1": { + "name": { + "kind": "Identifier", + "text": "b" + }, + "init": { + "kind": "Identifier", + "text": "b" + } + } + } + } + } + }, + "flags": 0 + } + }, + { + "kind": "VariableStatement", + "declarationList": { + "kind": "VariableDeclarationList", + "declarations": { + "0": { + "kind": "VariableDeclaration", + "name": { + "kind": "Identifier", + "text": "d" + }, + "typename": { + "kind": "Identifier", + "text": "D" + }, + "init": { + "kind": "ObjectLiteralExpression", + "properties": { + "0": { + "name": { + "kind": "Identifier", + "text": "a" + }, + "init": { + "kind": "Identifier", + "text": "a" + } + }, + "1": { + "name": { + "kind": "Identifier", + "text": "b" + }, + "init": { + "kind": "Identifier", + "text": "b" + } + }, + "2": { + "name": { + "kind": "Identifier", + "text": "c" + }, + "init": { + "kind": "Identifier", + "text": "a" + } + }, + "3": { + "name": { + "kind": "Identifier", + "text": "d" + }, + "init": { + "kind": "Identifier", + "text": "b" + } + }, + "4": { + "name": { + "kind": "Identifier", + "text": "e" + }, + "init": { + "kind": "Identifier", + "text": "c" + } + }, + "5": { + "name": { + "kind": "Identifier", + "text": "f" + }, + "init": { + "kind": "ObjectLiteralExpression", + "properties": { + "0": { + "name": { + "kind": "Identifier", + "text": "foo" + }, + "init": { + "kind": "StringLiteral", + "value": "string", + "isSingleQuote": true + } + }, + "1": { + "name": { + "kind": "Identifier", + "text": "bar" + }, + "init": { + "kind": "NumericLiteral", + "value": 10 + } + } + } + } + } + } + } + } + }, + "flags": 0 + } + } + ] +} +``` From d5500c637ed3fd20ef6692fae6ff59d2d1a29c3d Mon Sep 17 00:00:00 2001 From: TK Date: Mon, 28 Aug 2023 09:43:22 -0300 Subject: [PATCH 13/23] Add type checking section --- draft/object-types.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/draft/object-types.md b/draft/object-types.md index 997d023..cb24e84 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -553,3 +553,28 @@ It produces this AST ] } ``` + +## Type Checking + +Two way type checking: + +1. going from the value and checking the type definition +2. going from the type definition and checking the value properties + +Any type mismatch should generate this error + +``` +Type {X} is not assignable to {Y} +``` + +Every property was right but it's missing some from the type definition + +``` +Type '{ a: string; b: number; }' is missing the following properties from type 'D': c, d, e, f +``` + +Go through the list of properties, for each, check if it's defined in the type definition. If it's not, it should generate the type error + +``` +Object literal may only specify known properties, and 'g' does not exist in type 'D'. +``` From db2f9d35f4cf1d9ede82738716e87cbfe4348a7c Mon Sep 17 00:00:00 2001 From: TK Date: Tue, 29 Aug 2023 11:05:19 -0300 Subject: [PATCH 14/23] Create object types --- draft/object-types.md | 42 ++++++++++++++++++++++++++++++---- src/check.ts | 53 ++++++++++++++++++++++++++++++++++++++----- src/parse.ts | 1 + src/types.ts | 19 ++++++++++++++-- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/draft/object-types.md b/draft/object-types.md index cb24e84..737f43d 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -561,20 +561,54 @@ Two way type checking: 1. going from the value and checking the type definition 2. going from the type definition and checking the value properties -Any type mismatch should generate this error +Check phases + +1. Any property type mismatch should generate this error ``` Type {X} is not assignable to {Y} ``` -Every property was right but it's missing some from the type definition +2. Go through the list of properties. For each one, check if it's defined in the type definition. If it's not, it should generate the type error + +``` +Object literal may only specify known properties, and 'g' does not exist in type 'D'. +``` + +3. Every property was right but it's missing some from the type definition ``` Type '{ a: string; b: number; }' is missing the following properties from type 'D': c, d, e, f ``` -Go through the list of properties, for each, check if it's defined in the type definition. If it's not, it should generate the type error +### Object type structure +```ts +type ObjecType = { + flags: ObjectFlag; + members: Record; +}; ``` -Object literal may only specify known properties, and 'g' does not exist in type 'D'. + +A module can have many different object types: + +```ts +type ObjectTypes = { + [key: string]: ObjectType; +}; +``` + +So we can use it like: + +```ts +const objectsTypes: ObjectTypes = { + A: { + flags: ObjectFlag, + members: { + a: number, + }, + }, +}; + +objectTypes.get('A').members.get('a'); ``` diff --git a/src/check.ts b/src/check.ts index 84597f9..05d2a4b 100644 --- a/src/check.ts +++ b/src/check.ts @@ -9,21 +9,35 @@ import { VariableDeclaration, SymbolFlags, Symbol, + TypeLiteral, + TypeFlags, + Member, + PropertySignature, + TypeTable, } from './types'; import { error } from './error'; import { resolve } from './bind'; -const stringType: Type = { id: 'string' }; -const numberType: Type = { id: 'number' }; -const errorType: Type = { id: 'error' }; -const empty: Type = { id: 'empty' }; -const anyType: Type = { id: 'any' }; +const stringType: Type = { id: 'string', flags: TypeFlags.Any }; +const numberType: Type = { id: 'number', flags: TypeFlags.NumericLiteral }; +const errorType: Type = { id: 'error', flags: TypeFlags.Any }; +const empty: Type = { id: 'empty', flags: TypeFlags.Any }; +const anyType: Type = { id: 'any', flags: TypeFlags.Any }; + +function createObjectType(members: TypeTable): Type { + return { + id: 'object', + flags: TypeFlags.Object, + members, + }; +} function typeToString(type: Type) { return type.id; } export function check(module: Module) { + const objectTypes = new Map(); return module.statements.map(checkStatement); function checkStatement(statement: Statement): Type { @@ -31,7 +45,7 @@ export function check(module: Module) { case Node.ExpressionStatement: return checkExpression(statement.expr); case Node.TypeAlias: - return checkType(statement.typename); + return checkTypeAlias(statement); case Node.VariableStatement: statement.declarationList.declarations.forEach( checkVariableDeclaration, @@ -120,6 +134,7 @@ export function check(module: Module) { } const type = checkType(declaration.typename); + if (type !== initType && type !== errorType) error( declaration.init.pos, @@ -127,6 +142,7 @@ export function check(module: Module) { initType, )}' to variable with declared type '${typeToString(type)}'.`, ); + return type; } @@ -136,6 +152,12 @@ export function check(module: Module) { : checkExpression(declaration.init); } + function checkTypeAlias(statement: TypeAlias | PropertySignature) { + return statement.typename.kind === Node.TypeLiteral + ? checkObjecType(statement) + : checkType(statement.typename); + } + function checkType(name: Identifier): Type { switch (name.text) { case 'string': @@ -158,6 +180,25 @@ export function check(module: Module) { } } + function checkObjecType(statement: TypeAlias | PropertySignature) { + objectTypes.set( + statement.name.text, + createObjectType( + checkMemberTypes((statement.typename as TypeLiteral).members), + ), + ); + + return objectTypes.get(statement.name.text) as Type; + } + + function checkMemberTypes(members: Member[]) { + const membersTable = new Map(); + members.forEach((member) => + membersTable.set(member.name.text, checkTypeAlias(member)), + ); + return membersTable; + } + function handleSubsequentVariableDeclarationsTypes( declaration: VariableDeclaration, valueDeclarationType: Type, diff --git a/src/parse.ts b/src/parse.ts index c2a1a87..ffee4b3 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -108,6 +108,7 @@ export function parse(lexer: Lexer): Module { : parseIdentifier(); return { + kind: Node.PropertySignature, name, typename, pos, diff --git a/src/types.ts b/src/types.ts index cfdeb5d..f246939 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,7 @@ export enum Node { VariableStatement, VariableDeclarationList, VariableDeclaration, + PropertySignature, ObjectLiteralExpression, } @@ -134,7 +135,8 @@ export type TypeAlias = Location & { typename: Identifier | TypeLiteral; }; -type PropertySignature = Location & { +export type PropertySignature = Location & { + kind: Node.PropertySignature; name: Identifier; typename: Identifier | TypeLiteral; }; @@ -166,7 +168,13 @@ export type Module = { statements: Statement[]; }; -export type Type = { id: string }; +export type TypeTable = Map; + +export type Type = { + id: string; + flags: TypeFlags; + members?: TypeTable; +}; export enum CharCodes { b = 98, @@ -195,3 +203,10 @@ export const enum NodeFlags { None = 0, Let = 1 << 0, } + +export const enum TypeFlags { + Any = 1 << 0, + StringLiteral = 1 << 1, + NumericLiteral = 1 << 2, + Object = 1 << 3, +} From a3cede9020ba1864cf4fc19a01559f851d0b2d1e Mon Sep 17 00:00:00 2001 From: TK Date: Wed, 30 Aug 2023 11:22:04 -0300 Subject: [PATCH 15/23] Add script to test the checker --- package.json | 1 + src/testChecker.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/testChecker.ts diff --git a/package.json b/package.json index 1a30754..6ef0302 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test:file": "tsc && node built/testFile.js", "test:parser": "ts-node ./src/testParser", "test:binder": "ts-node ./src/testBinder", + "test:checker": "ts-node ./src/testChecker", "accept": "mv baselines/local/* baselines/reference/", "mtsc": "node built/index.js" }, diff --git a/src/testChecker.ts b/src/testChecker.ts new file mode 100644 index 0000000..5096454 --- /dev/null +++ b/src/testChecker.ts @@ -0,0 +1,23 @@ +import * as fs from 'fs'; +import { parse } from './parse'; +import { lex } from './lex'; +import { bind } from './bind'; +import { check } from './check'; +import { errors } from './error'; + +const args = process.argv.slice(2); +const file = args[0]; + +if (!file) { + console.log('Missing test file'); + process.exit(); +} + +const tree = parse(lex(fs.readFileSync('tests/' + file, 'utf8'))); + +bind(tree); +check(tree); + +for (let [key, value] of errors.entries()) { + console.log(key, value); +} From 0521eb1a9a47b2dbed5f475f81c4ef9806ce7ded Mon Sep 17 00:00:00 2001 From: TK Date: Wed, 30 Aug 2023 11:22:17 -0300 Subject: [PATCH 16/23] Add more tests --- tests/{objectTypes.ts => objectLiteral.ts} | 14 ++++++-------- tests/objectTypeAlias.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) rename tests/{objectTypes.ts => objectLiteral.ts} (76%) create mode 100644 tests/objectTypeAlias.ts diff --git a/tests/objectTypes.ts b/tests/objectLiteral.ts similarity index 76% rename from tests/objectTypes.ts rename to tests/objectLiteral.ts index 6279fdd..994a1fc 100644 --- a/tests/objectTypes.ts +++ b/tests/objectLiteral.ts @@ -17,18 +17,16 @@ type D = { }; }; -var a: A = 'string'; -var b: B = 11; var c: C = { - a: a, - b: b + a: 'a', + b: 1 }; var d: D = { - a: a, - b: b, - c: a, - d: b, + a: 'a', + b: 1, + c: 'a', + d: 1, e: c, f: { foo: 'string', diff --git a/tests/objectTypeAlias.ts b/tests/objectTypeAlias.ts new file mode 100644 index 0000000..ac3de44 --- /dev/null +++ b/tests/objectTypeAlias.ts @@ -0,0 +1,18 @@ +type A = string; +type B = number; +type C = { + a: string; + b: number; +}; + +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; + f: { + foo: string; + bar: number; + }; +}; \ No newline at end of file From 8ce53573be7ad0a9881f60636ca9a81ff75ae9a9 Mon Sep 17 00:00:00 2001 From: TK Date: Wed, 30 Aug 2023 11:41:54 -0300 Subject: [PATCH 17/23] Check object literals --- src/check.ts | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/check.ts b/src/check.ts index 05d2a4b..19e7bf7 100644 --- a/src/check.ts +++ b/src/check.ts @@ -14,6 +14,8 @@ import { Member, PropertySignature, TypeTable, + ObjectLiteralExpression, + PropertyAssignment, } from './types'; import { error } from './error'; import { resolve } from './bind'; @@ -45,7 +47,7 @@ export function check(module: Module) { case Node.ExpressionStatement: return checkExpression(statement.expr); case Node.TypeAlias: - return checkTypeAlias(statement); + return checkIdentifierOrObjectType(statement); case Node.VariableStatement: statement.declarationList.declarations.forEach( checkVariableDeclaration, @@ -93,6 +95,8 @@ export function check(module: Module) { )}' to variable of type '${typeToString(t)}'.`, ); return t; + case Node.ObjectLiteralExpression: + return createObjectType(checkPropertyTypes(expression.properties)); } } @@ -152,12 +156,6 @@ export function check(module: Module) { : checkExpression(declaration.init); } - function checkTypeAlias(statement: TypeAlias | PropertySignature) { - return statement.typename.kind === Node.TypeLiteral - ? checkObjecType(statement) - : checkType(statement.typename); - } - function checkType(name: Identifier): Type { switch (name.text) { case 'string': @@ -167,12 +165,10 @@ export function check(module: Module) { default: const symbol = resolve(module.locals, name.text, SymbolFlags.Type); if (symbol) { - return checkType( - ( - symbol.declarations.find( - (d) => d.kind === Node.TypeAlias, - ) as TypeAlias - ).typename, + return checkIdentifierOrObjectType( + symbol.declarations.find( + (d) => d.kind === Node.TypeAlias, + ) as TypeAlias, ); } error(name.pos, 'Could not resolve type ' + name.text); @@ -194,11 +190,34 @@ export function check(module: Module) { function checkMemberTypes(members: Member[]) { const membersTable = new Map(); members.forEach((member) => - membersTable.set(member.name.text, checkTypeAlias(member)), + membersTable.set(member.name.text, checkIdentifierOrObjectType(member)), ); return membersTable; } + function checkPropertyTypes(properties: PropertyAssignment[]) { + const membersTable = new Map(); + properties.forEach((property) => + membersTable.set( + 'text' in property.name + ? property.name.text + : property.name.value.toString(), + checkIdentifierOrObjectType(property), + ), + ); + return membersTable; + } + + function checkIdentifierOrObjectType( + statement: TypeAlias | PropertySignature | PropertyAssignment, + ) { + return 'typename' in statement + ? statement.typename.kind === Node.TypeLiteral + ? checkObjecType(statement) + : checkType(statement.typename) + : checkExpression(statement.init); + } + function handleSubsequentVariableDeclarationsTypes( declaration: VariableDeclaration, valueDeclarationType: Type, From c0860c0316b3a12d8d0b221c5a57d0fa0685eae5 Mon Sep 17 00:00:00 2001 From: TK Date: Wed, 30 Aug 2023 11:42:57 -0300 Subject: [PATCH 18/23] Clean up --- src/check.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/check.ts b/src/check.ts index 19e7bf7..e11b420 100644 --- a/src/check.ts +++ b/src/check.ts @@ -14,7 +14,6 @@ import { Member, PropertySignature, TypeTable, - ObjectLiteralExpression, PropertyAssignment, } from './types'; import { error } from './error'; From ed131cd65a3e6927a285be303dd7ce71054d8d01 Mon Sep 17 00:00:00 2001 From: TK Date: Wed, 30 Aug 2023 11:44:26 -0300 Subject: [PATCH 19/23] Rename function for clarity --- src/check.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/check.ts b/src/check.ts index e11b420..fce3170 100644 --- a/src/check.ts +++ b/src/check.ts @@ -46,7 +46,7 @@ export function check(module: Module) { case Node.ExpressionStatement: return checkExpression(statement.expr); case Node.TypeAlias: - return checkIdentifierOrObjectType(statement); + return checkTypeIdentifierOrObjectType(statement); case Node.VariableStatement: statement.declarationList.declarations.forEach( checkVariableDeclaration, @@ -164,7 +164,7 @@ export function check(module: Module) { default: const symbol = resolve(module.locals, name.text, SymbolFlags.Type); if (symbol) { - return checkIdentifierOrObjectType( + return checkTypeIdentifierOrObjectType( symbol.declarations.find( (d) => d.kind === Node.TypeAlias, ) as TypeAlias, @@ -189,7 +189,10 @@ export function check(module: Module) { function checkMemberTypes(members: Member[]) { const membersTable = new Map(); members.forEach((member) => - membersTable.set(member.name.text, checkIdentifierOrObjectType(member)), + membersTable.set( + member.name.text, + checkTypeIdentifierOrObjectType(member), + ), ); return membersTable; } @@ -201,13 +204,13 @@ export function check(module: Module) { 'text' in property.name ? property.name.text : property.name.value.toString(), - checkIdentifierOrObjectType(property), + checkTypeIdentifierOrObjectType(property), ), ); return membersTable; } - function checkIdentifierOrObjectType( + function checkTypeIdentifierOrObjectType( statement: TypeAlias | PropertySignature | PropertyAssignment, ) { return 'typename' in statement From d80238f11e532ed4269d079bf471b1429b835792 Mon Sep 17 00:00:00 2001 From: TK Date: Mon, 1 Jan 2024 15:05:11 -0300 Subject: [PATCH 20/23] * --- draft/object-types.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/draft/object-types.md b/draft/object-types.md index 737f43d..f0db613 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -16,6 +16,10 @@ type D = { c: A; d: B; e: C; + f: { + foo: string; + bar: number; + }; }; ``` From 77b25d41ef95c75aa9196328fc7215567a77f375 Mon Sep 17 00:00:00 2001 From: TK Date: Sat, 6 Jan 2024 22:51:02 -0300 Subject: [PATCH 21/23] Rename from `TypeAliasDeclaration` to `TypeAlias` --- draft/object-types.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/draft/object-types.md b/draft/object-types.md index f0db613..9d39a19 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -41,7 +41,7 @@ AST: ```ts { - kind: TypeAliasDeclaration, + kind: TypeAlias, name: Identifier, type: string | number } @@ -60,7 +60,7 @@ AST: ```ts { - kind: TypeAliasDeclaration, + kind: TypeAlias, name: Identifier, type: { kind: TypeLiteral, @@ -88,9 +88,9 @@ type D = { AST: ```ts -// TypeAliasDeclaration +// TypeAlias { - kind: TypeAliasDeclaration, + kind: TypeAlias, name: Identifier, type: { kind: TypeLiteral, @@ -128,9 +128,9 @@ type D = { AST: ```ts -// TypeAliasDeclaration +// TypeAlias { - kind: TypeAliasDeclaration, + kind: TypeAlias, name: Identifier, type: { kind: TypeLiteral, From a3cbd8da775a52d79dc8e3d3df63e8b73549d49b Mon Sep 17 00:00:00 2001 From: TK Date: Sun, 7 Jan 2024 17:35:38 -0300 Subject: [PATCH 22/23] Add more information to the draft --- draft/object-types.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/draft/object-types.md b/draft/object-types.md index 9d39a19..aaf14a5 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -563,12 +563,16 @@ It produces this AST Two way type checking: 1. going from the value and checking the type definition + easier to find if the object literal has attributes that are not in the type definition 2. going from the type definition and checking the value properties + easier to find what's missing in the object literal to match the type definition Check phases 1. Any property type mismatch should generate this error +- If they don't share the same type. e.g. `number` and `object` + ``` Type {X} is not assignable to {Y} ``` From ddc02921b9af0f6a8041558bc7e9989274b3c35c Mon Sep 17 00:00:00 2001 From: TK Date: Tue, 23 Jan 2024 09:00:16 -0300 Subject: [PATCH 23/23] Check unassignable types and properties --- draft/object-types.md | 9 ++- src/check.ts | 88 +++++++++++++++++++++++++++++- tests/objectLiteral.ts | 14 +++-- tests/unassignableObjectLiteral.ts | 59 ++++++++++++++++++++ 4 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 tests/unassignableObjectLiteral.ts diff --git a/draft/object-types.md b/draft/object-types.md index aaf14a5..3104f7b 100644 --- a/draft/object-types.md +++ b/draft/object-types.md @@ -569,15 +569,18 @@ Two way type checking: Check phases -1. Any property type mismatch should generate this error +1. Any property type mismatch should generate this error. If they don't share the same type. e.g. `number` and `object` -- If they don't share the same type. e.g. `number` and `object` +- [x] Generate error ``` Type {X} is not assignable to {Y} ``` -2. Go through the list of properties. For each one, check if it's defined in the type definition. If it's not, it should generate the type error +2. Go through the list of properties. For each one, check if it's defined in the type definition. If it's not, it should generate the type error. This error only appears if rule number one passes the checker. + +- [x] Generate error +- [ ] Generate error only if there're no rule number one errors ``` Object literal may only specify known properties, and 'g' does not exist in type 'D'. diff --git a/src/check.ts b/src/check.ts index fce3170..3b37ba7 100644 --- a/src/check.ts +++ b/src/check.ts @@ -138,17 +138,61 @@ export function check(module: Module) { const type = checkType(declaration.typename); - if (type !== initType && type !== errorType) + handleUnassignableTypes(declaration, initType, type); + + if (initType.id === 'object') { + // Handle property type mismatch and only known property errors + handlePropertyTypeMismatch(declaration, initType, type); + // Handle missing properties error + + return type; + } + + if (type !== initType && type !== errorType) { error( declaration.init.pos, `Cannot assign initialiser of type '${typeToString( initType, )}' to variable with declared type '${typeToString(type)}'.`, ); + } return type; } + function handlePropertyTypeMismatch( + declaration: VariableDeclaration, + initType: Type, + type: Type, + ) { + let hasUnassignablePropertyTypes = false; + let undefinedPropertyName; + + for (const [propertyName, propertyType] of initType.members as TypeTable) { + const typePropertyType = type.members?.get(propertyName); + + if (typePropertyType) { + hasUnassignablePropertyTypes ||= handleUnassignablePropertyTypes( + declaration, + propertyType, + typePropertyType, + propertyName, + ); + } else { + undefinedPropertyName ||= propertyName; + } + } + + if (!hasUnassignablePropertyTypes && undefinedPropertyName) { + error( + declaration.init.pos, + `Object literal may only specify known properties, and '${undefinedPropertyName}' does not exist in type '${declaration.typename?.text}'.`, + ); + } + + return hasUnassignablePropertyTypes; + } + function checkVariableDeclarationType(declaration: VariableDeclaration) { return declaration.typename ? checkType(declaration.typename) @@ -220,6 +264,48 @@ export function check(module: Module) { : checkExpression(statement.init); } + function handleUnassignableTypes( + declaration: VariableDeclaration, + initType: Type, + type: Type, + ) { + if (initType.id !== type.id) { + error( + declaration.init.pos, + `Type '${typeToString( + initType, + )}' is not assignable to type '${typeToString(type)}'.`, + ); + } + } + + function handleUnassignablePropertyTypes( + declaration: VariableDeclaration, + initType: Type, + type: Type, + propertyName: string, + ): boolean { + if (initType.id !== type.id) { + error( + declaration.init.pos, + `Type '${typeToString( + initType, + )}' is not assignable to type '${typeToString( + type, + )}'. The expected type comes from property '${propertyName}' which is declared here on type '${ + declaration.typename?.text + }'`, + ); + return true; + } + + if (initType.id === type.id && initType.id === 'object') { + return handlePropertyTypeMismatch(declaration, initType, type); + } + + return false; + } + function handleSubsequentVariableDeclarationsTypes( declaration: VariableDeclaration, valueDeclarationType: Type, diff --git a/tests/objectLiteral.ts b/tests/objectLiteral.ts index 994a1fc..a414e1c 100644 --- a/tests/objectLiteral.ts +++ b/tests/objectLiteral.ts @@ -17,16 +17,18 @@ type D = { }; }; +var a: A = 'string'; +var b: B = 10; var c: C = { - a: 'a', - b: 1 + a: a, + b: b }; var d: D = { - a: 'a', - b: 1, - c: 'a', - d: 1, + a: a, + b: b, + c: a, + d: b, e: c, f: { foo: 'string', diff --git a/tests/unassignableObjectLiteral.ts b/tests/unassignableObjectLiteral.ts new file mode 100644 index 0000000..c67090d --- /dev/null +++ b/tests/unassignableObjectLiteral.ts @@ -0,0 +1,59 @@ +type A = string; +type B = number; +type C = { + a: string; + b: number; +}; + +type D = { + a: string; + b: number; + c: A; + d: B; + e: C; + f: { + foo: string; + bar: number; + }; +}; + +var a: D = 'string'; +var b: D = 1; +var c: D = { + a: 1 +} + +var d: D = { + f: { + foo: 123 + } +} + +type E = { + a: { + foo: { + bar: number + } + } +} + +var e: E = { + a: { + foo: { + bar: '123' + } + } +} + +type F = { + a: number +} + +var f: F = { + b: 123 +} + +var g: F = { + a: '123', + b: 123 +}