diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..72446f4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/examples/expression/README.md b/examples/expression/README.md new file mode 100644 index 0000000..dfcb70b --- /dev/null +++ b/examples/expression/README.md @@ -0,0 +1,131 @@ +# A handwritten parser + +This package contains a handwritten parser for a simple expression language. +The language supports: + +- Variables declarations with the types `number` and `string` +- Variable Assignments +- Arithmetic expressions +- Print statements +- Expressions, like basic arithmetic operations, string concatenation, variable references, literals and parentheses + +## How does it work? + +Parsing is a linear process that takes a string of text and produces a tree-like structure that represents the structure of the text. + +```mermaid +flowchart LR + A[Lexer] --> B[Parser] + CC@{shape: brace-r, label: "Typir is applied here"} --> C + B --> C[Type System] + C --> D[Validator] + + style C fill:#f9f,stroke:#333,stroke-width:4px +``` + +The following sections describe each step in the process. + +### Lexer + +**Input**: A string of text + +**Output**: A list of tokens + +**Task**: Splits the text to tokens and classifies each token. + +```mermaid +flowchart LR + subgraph Text + A["variable = 123"] + end + A --> B[Lexer] + B --> Tokens + subgraph Tokens + T1[variable:ID] + T2[=:ASSIGN] + T3[123:NUMBER] + end +``` + +### Parser + +**Input**: A list of tokens + +**Output**: An Abstract Syntax Tree (AST) + +**Task**: Takes token and arranges them as a tree. + +```mermaid +flowchart LR + subgraph Tokens + T1[variable:ID] + T2[=:ASSIGN] + T3[123:NUMBER] + end + + Tokens --> D[Parser] + subgraph AST + EE1[variable] + EE2[=] + EE3[123] + EE2 --> EE1 + EE2 --> EE3 + end + D --> AST +``` + +### Type system + +**Input**: An AST + +**Output**: A typed AST + +**Task**: Assigns types to the nodes of the AST. + +```mermaid +flowchart LR + subgraph AST + EE1[variable] + EE2[=] + EE3[123] + EE2 --> EE1 + EE2 --> EE3 + end + FF@{shape: brace-r, label: "described by Typir"} --> F + AST --> F[Type System] + F --> AST2 + subgraph AST2["Typed AST"] + FF1[variable:STRING] + FF2[=] + FF3[123:NUMBER] + FF2 --> FF1 + FF2 --> FF3 + end + + style F fill:#f9f,stroke:#333,stroke-width:4px +``` + +### Validator + +**Input**: A typed AST + +**Output**: a list of errors + +**Task**: Checks if the AST is valid. + +```mermaid +flowchart LR + subgraph AST["Typed AST"] + FF1[variable:STRING] + FF2[=] + FF3[123:NUMBER] + FF2 --> FF1 + FF2 --> FF3 + end + AST --> H[Validator] + H --> Errors + subgraph Errors + I1["Variable got wrong type assigned"] + end + style I1 fill:#fdd,stroke:#333,stroke-width:4px +``` diff --git a/examples/expression/package.json b/examples/expression/package.json new file mode 100644 index 0000000..392d423 --- /dev/null +++ b/examples/expression/package.json @@ -0,0 +1,30 @@ +{ + "name": "typir-example-expression", + "displayName": "expression", + "version": "0.0.2", + "private": true, + "description": "", + "author": { + "name": "TypeFox", + "url": "https://www.typefox.io" + }, + "license": "MIT", + "type": "module", + "engines": { + "vscode": "^1.67.0" + }, + "volta": { + "node": "18.20.4", + "npm": "10.7.0" + }, + "scripts": { + "build": "tsc -b tsconfig.json", + "clean": "shx rm -rf out", + "lint": "eslint src --ext ts", + "test": "vitest", + "watch": "concurrently -n tsc -c blue \"tsc -b tsconfig.json --watch\"" + }, + "dependencies": { + "typir": "0.1.2" + } +} diff --git a/examples/expression/src/ast.ts b/examples/expression/src/ast.ts new file mode 100644 index 0000000..dcdc217 --- /dev/null +++ b/examples/expression/src/ast.ts @@ -0,0 +1,141 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ +export interface BinaryExpression { + type: 'binary'; + left: Expression; + right: Expression; + op: '+'|'-'|'/'|'*'|'%'; +} + +export function isBinaryExpression(node: unknown): node is BinaryExpression { + return isAstNode(node) && node.type === 'binary'; +} + +export interface UnaryExpression { + type: 'unary'; + operand: Expression; + op: '+'|'-'; +} + +export function isUnaryExpression(node: unknown): node is UnaryExpression { + return isAstNode(node) && node.type === 'unary'; +} + +export interface VariableUsage { + type: 'variable-usage'; + ref: VariableDeclaration; +} + + +export function isVariableUsage(node: unknown): node is VariableUsage { + return isAstNode(node) && node.type === 'variable-usage'; +} + + +export interface Numeric { + type: 'numeric'; + value: number; +} + +export function isNumeric(node: unknown): node is Numeric { + return isAstNode(node) && node.type === 'numeric'; +} + +export interface CharString { + type: 'string'; + value: string; +} + +export function isCharString(node: unknown): node is CharString { + return isAstNode(node) && node.type === 'string'; +} + +export type Expression = UnaryExpression | BinaryExpression | VariableUsage | Numeric | CharString; + +export interface VariableDeclaration { + type: 'variable-declaration'; + name: string; + value: Expression; +} + +export function isVariableDeclaration(node: unknown): node is VariableDeclaration { + return isAstNode(node) && node.type === 'variable-declaration'; +} + +export interface Assignment { + type: 'assignment'; + variable: VariableDeclaration; + value: Expression; +} + +export function isAssignment(node: unknown): node is Assignment { + return isAstNode(node) && node.type === 'assignment'; +} + + +export interface Printout { + type: 'printout'; + value: Expression; +} + +export function isPrintout(node: unknown): node is Printout { + return isAstNode(node) && node.type === 'printout'; +} + +export type Statement = VariableDeclaration | Printout | Assignment; + +export type Model = Statement[]; + +export type Node = Expression | Printout | VariableDeclaration | Assignment; + +export function isAstNode(node: unknown): node is Node { + return Object.getOwnPropertyNames(node).includes('type') && ['variable-usage', 'unary', 'binary', 'numeric', 'string', 'printout', 'variable-declaration', 'assignment'].includes((node as Node).type); +} + +export namespace AST { + export function variable(name: string, value: Expression): VariableDeclaration { + return { type: 'variable-declaration', name, value }; + } + export function assignment(variable: VariableDeclaration, value: Expression): Assignment { + return { type: 'assignment', variable, value }; + } + export function printout(value: Expression): Printout { + return { type: 'printout', value }; + } + export function num(value: number): Numeric { + return { + type: 'numeric', + value + }; + } + export function string(value: string): CharString { + return { + type: 'string', + value + }; + } + export function binary(left: Expression, op: BinaryExpression['op'], right: Expression): BinaryExpression { + return { + type: 'binary', + left, + op, + right + }; + } + export function unary(op: UnaryExpression['op'], operand: Expression): UnaryExpression { + return { + type: 'unary', + op, + operand + }; + } + export function useVariable(variable: VariableDeclaration): VariableUsage { + return { + ref: variable, + type: 'variable-usage' + }; + } +} diff --git a/examples/expression/src/lexer.ts b/examples/expression/src/lexer.ts new file mode 100644 index 0000000..bfffe59 --- /dev/null +++ b/examples/expression/src/lexer.ts @@ -0,0 +1,68 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +/** + * This is a table of all token type definitions required to parse the language. + * The order is important! The last token type ERROR is meant to catch all bad input. + */ +const TokenDefinitions = { + WS: /\s+/, + VAR: /VAR/, + PRINT: /PRINT/, + LPAREN: /\(/, + RPAREN: /\)/, + ASSIGN: /=/, + SEMICOLON: /;/, + ID: /[A-Z_][A-Z_0-9]*/, + NUM: /[0-9]+/, + STRING: /"([^"\\]|\\["\\])*"/, + ADD_OP: /\+|-/, + MUL_OP: /\*|\/|%/, + ERROR: /./ +} satisfies Record; + +export type TokenType = keyof typeof TokenDefinitions; + +export type Token = { + type: TokenType; + content: string; +}; + +/** + * A tokenizer (or lexer) takes a string, analyzes it and returns tokens representing the split text. + * Each token is meant as a piece of text with a special token type or character class. + * @param text + */ +export function* tokenize(text: string): Generator { + let position = 0; + const definitions = stickyfy(TokenDefinitions); + while(position < text.length) { + for (const [type, regexp] of Object.entries(definitions)) { + regexp.lastIndex = position; + const match = regexp.exec(text); + if(match) { + const content = match[0]; + position += content.length; + yield { + type: type as TokenType, + content + }; + break; + } + } + } +} + +/** + * Stickify helps to transform the token type RegExps to become `sticky` (y) and `case-insensitive` (i). + * Sticky means that we can set the offset where the RegExp has to start matching. + */ +function stickyfy(definitions: typeof TokenDefinitions) { + return Object.fromEntries( + Object.entries(definitions) + .map(([name, regexp]) => [name, new RegExp(regexp, 'yi')]) + ) as typeof TokenDefinitions; +} diff --git a/examples/expression/src/parser.ts b/examples/expression/src/parser.ts new file mode 100644 index 0000000..a5bb7be --- /dev/null +++ b/examples/expression/src/parser.ts @@ -0,0 +1,227 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { Expression, BinaryExpression, UnaryExpression, VariableUsage, Numeric, CharString, VariableDeclaration, Printout, Model as AST, Statement, Assignment } from './ast.js'; +import { Token, tokenize, TokenType } from './lexer.js'; + +/** + * A parser receives a stream of tokens, analyzes it and returns an abstract syntax tree. + */ +export class Parser { + private tokens: Token[]; + private tokenIndex: number; + private symbols: Record; + /** skips tokens of a given type, normally you want to skip whitespace and comments */ + private skip(...tokenTypes: TokenType[]) { + while(this.tokenIndex < this.tokens.length && tokenTypes.includes(this.tokens[this.tokenIndex].type)) { + this.tokenIndex++; + } + } + /** checks if the current token has the given type */ + private canConsume(tokenType: TokenType): boolean { + this.skip('WS'); + return this.tokens[this.tokenIndex].type === tokenType; + } + + /** + * Assumes that the current token is of given type and moves the lookahead one token forward. + * If the assumption is wrong, throw an error. + */ + private consume(tokenType: TokenType): Token { + this.skip('WS'); + const lookahead = this.tokens[this.tokenIndex]; + if(lookahead.type !== tokenType) { + throw new Error(`Expected ${tokenType} but got ${lookahead.type}!`); + } + this.tokenIndex++; + return lookahead; + } + + /** + * EXPRESSION ::= ADDITIVE + * @returns + */ + private expression(): Expression { + return this.additive(); + } + + /** + * ADDITIVE ::= (MULTIPLICATIVE ADD_OP)* MULTIPLICATIVE + * @returns + */ + private additive(): Expression { + let left = this.multiplicative(); + while(this.canConsume('ADD_OP')) { + const op = this.consume('ADD_OP').content as '+'|'-'; + const right = this.multiplicative(); + left = { + type: 'binary', + left, + right, + op + } as BinaryExpression; + } + return left; + } + + /** + * MULTIPLICATIVE ::= (UNARY ADD_OP)* UNARY + * @returns + */ + private multiplicative(): Expression { + let left = this.unary(); + while(this.canConsume('MUL_OP')) { + const op = this.consume('MUL_OP').content as '/'|'*'|'%'; + const right = this.unary(); + left = { + type: 'binary', + left, + right, + op + } as BinaryExpression; + } + return left; + } + + /** + * UNARY ::= ADD_OP UNARY | PRIMARY + * @returns + */ + private unary(): Expression { + if(this.canConsume('ADD_OP')) { + const op = this.consume('ADD_OP').content as '+'|'-'; + const operand = this.unary(); + return { + type: 'unary', + operand, + op + } as UnaryExpression; + } else { + return this.primary(); + } + } + + /** + * PRIMARY ::= LPAREN EXPRESSION RPAREN + * | ID + * | NUM + * | STRING + * @returns + */ + private primary(): Expression { + if(this.canConsume('LPAREN')) { + this.consume('LPAREN'); + const result = this.expression(); + this.consume('RPAREN'); + return result; + } else if(this.canConsume('ID')) { + const token = this.consume('ID'); + const symbol = this.symbols[token.content]; + if(!symbol) { + throw new Error(`Unknown symbol '${token.content}'!`); + } + return { + type: 'variable-usage', + ref: symbol + } as VariableUsage; + } else if(this.canConsume('NUM')) { + return { + type: 'numeric', + value: parseInt(this.consume('NUM').content, 10) + } as Numeric; + } else if(this.canConsume('STRING')) { + const literal = this.consume('STRING').content; + return { + type: 'string', + value: literal.substring(1, literal.length-1).replace(/\\"/g, '"').replace(/\\\\/g, '\\') + } as CharString; + } else { + throw new Error("Don't know how to continue..."); + } + } + + /** + * VARIABLE_DECLARATION ::= VAR ID ASSIGN EXPRESSION SEMICOLON + * @returns + */ + private variableDeclaration(): VariableDeclaration { + this.consume('VAR'); + const name = this.consume('ID').content; + this.consume('ASSIGN'); + const value = this.expression(); + this.consume('SEMICOLON'); + return { + type: 'variable-declaration', + name, + value + }; + } + + /** + * PRINTOUT ::= PRINT EXPRESSION SEMICOLON + * @returns + */ + private printout(): Printout { + this.consume('PRINT'); + const value = this.expression(); + this.consume('SEMICOLON'); + return { + type: 'printout', + value + }; + } + + /** + * STATEMENT ::= PRINTOUT | VARIABLE_DECLARATION | ASSIGNMENT + * @returns + */ + private statement(): Statement { + if(this.canConsume('VAR')) { + const variable = this.variableDeclaration(); + this.symbols[variable.name] = variable; + return variable; + } else if(this.canConsume('PRINT')) { + return this.printout(); + } else if(this.canConsume('ID')) { + return this.assignment(); + } + throw new Error(`Unexpected token '${this.tokens[this.tokenIndex].type}'.`); + } + + /** + * ASSIGNMENT ::= ID ASSIGN EXPRESSION SEMICOLON + * @returns + */ + private assignment(): Assignment { + const name = this.consume('ID').content; + const variable = this.symbols[name]; + this.consume('ASSIGN'); + const value = this.expression(); + this.consume('SEMICOLON'); + return { + type: 'assignment', + variable, + value, + }; + } + + /** + * Get some text, tokenize it with the tokenizer and then consume the statement parser rule one by one + * PROGRAM ::= STATEMENT* + * @param text + * @returns + */ + parse(text: string): AST { + this.tokens = [...tokenize(text)]; + this.tokenIndex = 0; + this.symbols = {}; + const result: AST = []; + while(this.tokenIndex < this.tokens.length) { + result.push(this.statement()); + } + return result; + } +} diff --git a/examples/expression/src/type-system.ts b/examples/expression/src/type-system.ts new file mode 100644 index 0000000..f0eba0f --- /dev/null +++ b/examples/expression/src/type-system.ts @@ -0,0 +1,82 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ +import { createTypirServices, InferenceRuleNotApplicable, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, NO_PARAMETER_NAME } from 'typir'; +import { BinaryExpression, isAssignment, isBinaryExpression, isCharString, isNumeric, isPrintout, isUnaryExpression, isVariableDeclaration, isVariableUsage, UnaryExpression } from './ast.js'; + +export function initializeTypir() { + const typir = createTypirServices(); + const typeNumber = typir.factory.Primitives.create({ + primitiveName: 'number', inferenceRules: isNumeric + }); + const typeString = typir.factory.Primitives.create({ + primitiveName: 'string', inferenceRules: isCharString + }); + const typeVoid = typir.factory.Primitives.create({ primitiveName: 'void' }); + + const binaryInferenceRule: InferOperatorWithMultipleOperands = { + filter: isBinaryExpression, + matching: (node: BinaryExpression, name: string) => node.op === name, + operands: (node: BinaryExpression) => [node.left, node.right], + }; + for (const operator of ['+', '-', '/', '*', '%']) { + typir.factory.Operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); + } + typir.factory.Operators.createBinary({ name: '+', signature: { left: typeString, right: typeString, return: typeString }, inferenceRule: binaryInferenceRule }); + + const unaryInferenceRule: InferOperatorWithSingleOperand = { + filter: isUnaryExpression, + matching: (node: UnaryExpression, name: string) => node.op === name, + operand: (node: UnaryExpression, _name: string) => node.operand, + }; + typir.factory.Operators.createUnary({ name: '+', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + typir.factory.Operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + + typir.factory.Functions.create({ + functionName: 'print', + inputParameters: [{ + name: 'input', + type: typeString, + }], + outputParameter: { name: NO_PARAMETER_NAME, type: typeVoid }, + inferenceRuleForCalls: { + filter: isPrintout, + matching: () => true, + inputArguments: (node) => [node.value], + } + }); + + typir.Conversion.markAsConvertible(typeNumber, typeString, 'IMPLICIT_EXPLICIT'); + + typir.Inference.addInferenceRule((languageNode) => { + if (isVariableDeclaration(languageNode)) { + return languageNode.value; + } else if (isVariableUsage(languageNode)) { + return languageNode.ref; + } + return InferenceRuleNotApplicable; + }); + + typir.validation.Collector.addValidationRule((node) => { + if(isAssignment(node)) { + const left = typir.Inference.inferType(node.variable); + const right = typir.Inference.inferType(node.value); + if(!Array.isArray(left) && !Array.isArray(right)) { + const problem = typir.Assignability.getAssignabilityProblem(right, left); + if(problem) { + return [{ + $problem: 'ValidationProblem', + languageNode: node, + message: `'${right.getName()}' is not assignable to '${left.getName()}'.`, + severity: 'error', + }]; + } + } + } + return []; + }); + + return typir; +} diff --git a/examples/expression/src/validator.ts b/examples/expression/src/validator.ts new file mode 100644 index 0000000..a36c7d7 --- /dev/null +++ b/examples/expression/src/validator.ts @@ -0,0 +1,33 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ +import { TypirServices } from 'typir'; +import { Expression, Model } from './ast.js'; + +export function validate(typir: TypirServices, model: Model, accept: (message: string) => void) { + function runValidator(languageNode: unknown) { + typir.validation.Collector.validate(languageNode).forEach(m => accept(m.message)); + } + function visitExpression(expr: Expression) { + switch(expr.type) { + case 'binary': + visitExpression(expr.left); + visitExpression(expr.right); + break; + case 'unary': + visitExpression(expr.operand); + break; + case 'variable-usage': + case 'numeric': + case 'string': + break; + } + runValidator(expr); + } + for (const statement of model) { + visitExpression(statement.value); + runValidator(statement); + } +} diff --git a/examples/expression/test/lexer.test.ts b/examples/expression/test/lexer.test.ts new file mode 100644 index 0000000..a3937a2 --- /dev/null +++ b/examples/expression/test/lexer.test.ts @@ -0,0 +1,23 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ +import { describe, expect, test } from 'vitest'; +import { tokenize, TokenType } from '../src/lexer.js'; + +function expectTokenTypes(text: string, ...expecteds: TokenType[]) { + const actuals = [...tokenize(text)].map(t => t.type); + expect(actuals).toEqual(expecteds); +} + +describe('Tokenizer', () => { + test('tokenize', () => { + expectTokenTypes('VAR A = 1;', 'VAR', 'WS', 'ID', 'WS', 'ASSIGN', 'WS', 'NUM', 'SEMICOLON'); + expectTokenTypes('PRINT 1;', 'PRINT', 'WS', 'NUM', 'SEMICOLON'); + expectTokenTypes('PRINT(A);', 'PRINT', 'LPAREN', 'ID', 'RPAREN', 'SEMICOLON'); + expectTokenTypes('PRINT 1+2*3;', 'PRINT', 'WS', 'NUM', 'ADD_OP', 'NUM', 'MUL_OP', 'NUM', 'SEMICOLON'); + expectTokenTypes('PRINT --1;', 'PRINT', 'WS', 'ADD_OP', 'ADD_OP', 'NUM', 'SEMICOLON'); + expectTokenTypes('PRINT "Hello, \\"User\\"!";', 'PRINT', 'WS', 'STRING', 'SEMICOLON'); + }); +}); diff --git a/examples/expression/test/parser.test.ts b/examples/expression/test/parser.test.ts new file mode 100644 index 0000000..88d3818 --- /dev/null +++ b/examples/expression/test/parser.test.ts @@ -0,0 +1,38 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ +import { describe, expect, test } from 'vitest'; +import { AST, Model } from '../src/ast.js'; +import { Parser } from '../src/parser.js'; + +describe('Parser', () => { + test('parse', () => { + expectAST('VAR A = 1;', [AST.variable('A', AST.num(1))]); + expectAST('PRINT 1;', [AST.printout(AST.num(1))]); + expectError('PRINT(A);', "Unknown symbol 'A'!"); + expectAST('PRINT 1+2*3;', [AST.printout(AST.binary(AST.num(1), '+', AST.binary(AST.num(2), '*', AST.num(3))))]); + expectAST('PRINT --1;', [AST.printout(AST.unary('-', AST.unary('-', AST.num(1))))]); + expectAST('PRINT "Hello, \\"User\\"!";', [AST.printout(AST.string('Hello, "User"!'))]); + const variableA = AST.variable('A', AST.num(1)); + expectAST('VAR A = 1; PRINT(A);', [variableA, AST.printout(AST.useVariable(variableA))]); + const variableB = AST.variable('B', AST.num(1)); + expectAST('VAR B = 1; B = 2; PRINT B;', [ + variableB, + AST.assignment(variableB, AST.num(2)), + AST.printout(AST.useVariable(variableB)) + ]); + }); +}); + +function expectAST(text: string, expected: Model) { + const actual = new Parser().parse(text); + expect(actual).toEqual(expected); +} + +function expectError(text: string, message: string) { + expect(()=> { + new Parser().parse(text); + }).toThrow(message); +} diff --git a/examples/expression/test/validator.test.ts b/examples/expression/test/validator.test.ts new file mode 100644 index 0000000..fc3c409 --- /dev/null +++ b/examples/expression/test/validator.test.ts @@ -0,0 +1,34 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ +import { describe, expect, test } from 'vitest'; +import { Parser } from '../src/parser.js'; +import { initializeTypir } from '../src/type-system.js'; +import { validate } from '../src/validator.js'; + +const typir = initializeTypir(); + +describe('Validator', () => { + test('Positives', () => { + expectValidationMessages('VAR X = 1+2+3; PRINT X;'); + expectValidationMessages('PRINT 1+2+3;'); + expectValidationMessages('PRINT "Hallo!";'); + expectValidationMessages('PRINT "Hallo!"+"Welt!";'); + expectValidationMessages('VAR X = "Hallo!"; X = 123;'); //coercion rule applies! + }); + test('Negatives', () => { + expectValidationMessages('VAR X = 1; X = "hallo";', "'string' is not assignable to 'number'."); + expectValidationMessages('PRINT "1"-"2";', 'The given operands for the overloaded function \'-\' match the expected types only partially.'); + expectValidationMessages('PRINT 123-"hallo";', 'The given operands for the overloaded function \'-\' match the expected types only partially.'); + }); +}); + + +function expectValidationMessages(text: string, ...messages: string[]) { + const model = new Parser().parse(text); + const actual: string[] = []; + validate(typir, model, m => actual.push(m)); + expect(actual).toStrictEqual(messages); +} diff --git a/examples/expression/tsconfig.json b/examples/expression/tsconfig.json new file mode 100644 index 0000000..77a3cac --- /dev/null +++ b/examples/expression/tsconfig.json @@ -0,0 +1,12 @@ +// this file is required for VSCode to work properly +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + }, + "include": [ + "src/**/*", + "test/**/*" + ] +} diff --git a/examples/expression/tsconfig.src.json b/examples/expression/tsconfig.src.json new file mode 100644 index 0000000..8347682 --- /dev/null +++ b/examples/expression/tsconfig.src.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "out", + "lib": ["ESNext", "WebWorker"] + }, + "references": [{ + "path": "../../packages/typir/tsconfig.src.json" + }], + "include": [ + "src/**/*" + ] +} diff --git a/examples/expression/tsconfig.test.json b/examples/expression/tsconfig.test.json new file mode 100644 index 0000000..0ab1f70 --- /dev/null +++ b/examples/expression/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "test" + }, + "references": [{ + "path": "./tsconfig.src.json" + }], + "include": [ + "test/**/*", + ] +} diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 0c9e440..bbd5c97 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -185,7 +185,6 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { } } - export function createOxTypirModule(langiumServices: LangiumSharedCoreServices): Module { return { // specific configurations for OX diff --git a/package-lock.json b/package-lock.json index c6a6d19..0481f46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "workspaces": [ "packages/typir", "packages/typir-langium", + "examples/expression", "examples/ox", - "examples/lox" + "examples/lox", + "examples/expression" ], "devDependencies": { "@types/node": "~18.19.55", @@ -34,6 +36,17 @@ "npm": ">= 9.5.0" } }, + "examples/expression": { + "name": "typir-example-expression", + "version": "0.0.2", + "license": "MIT", + "dependencies": { + "typir": "0.1.2" + }, + "engines": { + "vscode": "^1.67.0" + } + }, "examples/lox": { "name": "typir-example-lox", "version": "0.1.2", @@ -3312,6 +3325,10 @@ "resolved": "packages/typir", "link": true }, + "node_modules/typir-example-expression": { + "resolved": "examples/expression", + "link": true + }, "node_modules/typir-example-lox": { "resolved": "examples/lox", "link": true diff --git a/package.json b/package.json index ad67155..5bc39a7 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "packages/typir", "packages/typir-langium", "examples/ox", - "examples/lox" + "examples/lox", + "examples/expression" ] } diff --git a/scripts/update-version.js b/scripts/update-version.js index 2bd0acd..dea336d 100644 --- a/scripts/update-version.js +++ b/scripts/update-version.js @@ -9,6 +9,7 @@ async function runUpdate() { await Promise.all([ replaceAll('typir', true, versions), replaceAll('typir-langium', true, versions), + replaceAll('expression', false, versions), replaceAll('ox', false, versions), replaceAll('lox', false, versions), ]); diff --git a/tsconfig.build.json b/tsconfig.build.json index a2cd4bf..8265b43 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,5 +9,7 @@ { "path": "examples/lox/tsconfig.test.json" }, { "path": "examples/ox/tsconfig.src.json" }, { "path": "examples/ox/tsconfig.test.json" }, + { "path": "examples/expression/tsconfig.src.json" }, + { "path": "examples/expression/tsconfig.test.json" }, ] }