Skip to content

Object Types #1

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

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1107c15
Some ideas for object types
imteekay Jul 17, 2023
446d0f3
Add types
imteekay Jul 17, 2023
de8582b
Package lock
imteekay Jul 17, 2023
273828e
Add open and close brace tokens for the lexer
imteekay Jul 17, 2023
eb0d405
Merge branch 'master' into object-types
imteekay Aug 2, 2023
4e1c404
Design: add examples and AST types
imteekay Aug 2, 2023
5d218eb
Alias `typename` can be Identifier or TypeLiteral
imteekay Aug 2, 2023
ed7e40d
Parse TypeLiteral
imteekay Aug 3, 2023
283a4dd
Merge branch 'master' into object-types
imteekay Aug 5, 2023
11b6541
Continue parsing type literal members while the current token is not …
imteekay Aug 5, 2023
78f274e
Adding an example on nested object type literal
imteekay Aug 5, 2023
9fe27f8
Merge branch 'master' into object-types
imteekay Aug 5, 2023
6361c8a
Merge branch 'master' into object-types
imteekay Aug 6, 2023
a73ec99
Better symbol naming and add comment
imteekay Aug 8, 2023
976fd0e
Merge branch 'master' into object-types
imteekay Aug 28, 2023
cb4b245
Parse object literals
imteekay Aug 28, 2023
c7fb42a
Add AST example for object literals
imteekay Aug 28, 2023
d5500c6
Add type checking section
imteekay Aug 28, 2023
db2f9d3
Create object types
imteekay Aug 29, 2023
a3cede9
Add script to test the checker
imteekay Aug 30, 2023
0521eb1
Add more tests
imteekay Aug 30, 2023
8ce5357
Check object literals
imteekay Aug 30, 2023
c0860c0
Clean up
imteekay Aug 30, 2023
ed131cd
Rename function for clarity
imteekay Aug 30, 2023
d80238f
*
imteekay Jan 1, 2024
7785118
Merge branch 'master' into object-types
imteekay Jan 1, 2024
77b25d4
Rename from `TypeAliasDeclaration` to `TypeAlias`
imteekay Jan 7, 2024
a3cbd8d
Add more information to the draft
imteekay Jan 7, 2024
ddc0292
Check unassignable types and properties
imteekay Jan 23, 2024
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
File renamed without changes.
625 changes: 625 additions & 0 deletions draft/object-types.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
181 changes: 165 additions & 16 deletions src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,44 @@ import {
VariableDeclaration,
SymbolFlags,
Symbol,
TypeLiteral,
TypeFlags,
Member,
PropertySignature,
TypeTable,
PropertyAssignment,
} 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<string, Type>();
return module.statements.map(checkStatement);

function checkStatement(statement: Statement): Type {
switch (statement.kind) {
case Node.ExpressionStatement:
return checkExpression(statement.expr);
case Node.TypeAlias:
return checkType(statement.typename);
return checkTypeIdentifierOrObjectType(statement);
case Node.VariableStatement:
statement.declarationList.declarations.forEach(
checkVariableDeclaration,
Expand Down Expand Up @@ -79,6 +94,8 @@ export function check(module: Module) {
)}' to variable of type '${typeToString(t)}'.`,
);
return t;
case Node.ObjectLiteralExpression:
return createObjectType(checkPropertyTypes(expression.properties));
}
}

Expand All @@ -92,15 +109,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
Expand All @@ -119,16 +137,62 @@ 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)
Expand All @@ -144,19 +208,104 @@ 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 checkTypeIdentifierOrObjectType(
symbol.declarations.find(
(d) => d.kind === Node.TypeAlias,
) as TypeAlias,
);
}
error(name.pos, 'Could not resolve type ' + name.text);
return errorType;
}
}

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<string, Type>();
members.forEach((member) =>
membersTable.set(
member.name.text,
checkTypeIdentifierOrObjectType(member),
),
);
return membersTable;
}

function checkPropertyTypes(properties: PropertyAssignment[]) {
const membersTable = new Map<string, Type>();
properties.forEach((property) =>
membersTable.set(
'text' in property.name
? property.name.text
: property.name.value.toString(),
checkTypeIdentifierOrObjectType(property),
),
);
return membersTable;
}

function checkTypeIdentifierOrObjectType(
statement: TypeAlias | PropertySignature | PropertyAssignment,
) {
return 'typename' in statement
? statement.typename.kind === Node.TypeLiteral
? checkObjecType(statement)
: checkType(statement.typename)
: 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,
Expand Down
6 changes: 6 additions & 0 deletions src/lex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
70 changes: 66 additions & 4 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
VariableDeclaration,
NodeFlags,
VariableStatement,
TypeLiteral,
Member,
ObjectLiteralExpression,
PropertyAssignment,
IdentifierOrLiteral,
} from './types';
import { error } from './error';

Expand All @@ -18,7 +23,7 @@ export function parse(lexer: Lexer): Module {

function parseModule(): Module {
return {
statements: parseStatements(
statements: parseList(
parseStatement,
() => tryParseToken(Token.Semicolon),
() => lexer.token() !== Token.EOF,
Expand All @@ -36,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 };
Expand All @@ -49,6 +54,8 @@ export function parse(lexer: Lexer): Module {
pos,
isSingleQuote: lexer.isSingleQuote(),
};
} else if (tryParseToken(Token.OpenBrace)) {
return parseObjectLiteral();
}
error(
pos,
Expand All @@ -67,6 +74,59 @@ 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();
parseExpected(Token.Colon);
const typename = tryParseToken(Token.OpenBrace)
? parseTypeLiteral()
: parseIdentifier();

return {
kind: Node.PropertySignature,
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();

Expand All @@ -77,7 +137,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 };
Expand Down Expand Up @@ -138,7 +200,7 @@ export function parse(lexer: Lexer): Module {
}
}

function parseStatements<T>(
function parseList<T>(
element: () => T,
terminator: () => boolean,
peek: () => boolean,
Expand Down
Loading