From df576b2d6520b356a7d79054c6d83f066637ec13 Mon Sep 17 00:00:00 2001 From: Mikkel RINGAUD Date: Wed, 13 Dec 2023 17:01:51 +0100 Subject: [PATCH] feat: add support for `si/sinon` --- examples/conditions.fr | 11 ++++++ src/ast/index.ts | 73 +++++++++++++++++++++++++++++++--------- src/ast/nodes.ts | 10 ++++++ src/interpreter/index.ts | 19 ++++++++++- src/lexer/index.ts | 8 ++++- src/lexer/tokens.ts | 22 ++++++++++++ src/symbols/visitor.ts | 31 +++++++++++++++-- 7 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 examples/conditions.fr diff --git a/examples/conditions.fr b/examples/conditions.fr new file mode 100644 index 0000000..25cd42b --- /dev/null +++ b/examples/conditions.fr @@ -0,0 +1,11 @@ +programme Conditions +début + avec ma_condition : booléen + + ma_condition <- vrai + + si + ma_condition + alors afficher "c'est vrai !\n" + fin si +fin Conditions \ No newline at end of file diff --git a/src/ast/index.ts b/src/ast/index.ts index 06f8a0b..d021b8a 100644 --- a/src/ast/index.ts +++ b/src/ast/index.ts @@ -24,7 +24,7 @@ import { NotToken } from "../lexer/tokens"; -import { IntegerNumber, BinaryOperation, UnaryOperation, type AST, Compound, NoOp, Variable, Assign, Program, Type, VariableDeclaration, RealNumber, StringConstant, GlobalScope, Procedure, ArgumentVariable, ProcedureCall, CharConstant, BooleanConstant } from "./nodes"; +import { IntegerNumber, BinaryOperation, UnaryOperation, type AST, Compound, NoOp, Variable, Assign, Program, Type, VariableDeclaration, RealNumber, StringConstant, GlobalScope, Procedure, ArgumentVariable, ProcedureCall, CharConstant, BooleanConstant, If } from "./nodes"; export class Parser { /** Current token instance. */ @@ -318,30 +318,31 @@ export class Parser { return root; } - /** statement_list : statement | statement \n statement_list */ - private statement_list (): Array { - const node = this.statement(); - const results = [node]; + /** + * statement_list : (statement)* + * + * Handles statements until it reaches an END token or an + * ELSE token (if it's inside an IF statement -> `insideIf === true`). + */ + private statement_list (insideIf = false): Array { + const results = []; // After the first statement, we expect the token to be a line break. // So we can loop on every statement until we reach the end of the program. + const isStatementsEnd = () => this.current_token?.type === TokenType.END || ( + insideIf && this.current_token?.type === TokenType.ELSE + ); + while (!isStatementsEnd()) { + // Eat every newline token between statements. + this.skip_newlines(); - while (this.current_token?.type === TokenType.LINE_BREAK) { - // Eat the line break and move to next token. - this.eat(TokenType.LINE_BREAK); - - // @ts-expect-error : We moved to next token. - // Check if the next token is the end of the program. - if (this.current_token?.type === TokenType.END) break; + // Check if the next token is the end of the statement list. + if (isStatementsEnd()) break; results.push(this.statement()); } - if (this.current_token?.type !== TokenType.END) { - throw new Error(`Token attendu: END\nToken actuel: ${this.current_token?.type}`); - } - return results; } @@ -370,6 +371,10 @@ export class Parser { } } + else if (this.current_token?.type === TokenType.IF) { + return this.if_statement(); + } + return this.empty(); } @@ -427,6 +432,37 @@ export class Parser { return node; } + /** + * if_statement : SI expr ALORS statement_list (SINON statement_list)? FIN SI + */ + private if_statement (): If { + this.eat(TokenType.IF); + const condition = this.expr(); + + // Possible to add a newline before the `then` token. + this.skip_newlines(); + this.eat(TokenType.THEN); + + const main_statements = this.statement_list(true); + let else_statements: AST[] | undefined; + + if (this.current_token?.type === TokenType.ELSE) { + this.eat(TokenType.ELSE); + else_statements = this.statement_list(); + } + + const node = new If( + condition, + main_statements, + else_statements + ); + + this.eat(TokenType.END); + this.eat(TokenType.IF); + + return node; + } + /** * Handles the variable node. * variable: ID @@ -451,6 +487,7 @@ export class Parser { * Such as unary operations, numbers, parenthesis, etc. */ private factor (): BinaryOperation | IntegerNumber | RealNumber | UnaryOperation | Variable | CharConstant | StringConstant | BooleanConstant { + this.skip_newlines(); const token = this.current_token!; switch (token.type) { @@ -479,6 +516,10 @@ export class Parser { case TokenType.LPAREN: { this.eat(TokenType.LPAREN); const node = this.expr(); + + // Possible to add a newline before the closing parenthesis. + this.skip_newlines(); + this.eat(TokenType.RPAREN); return node; } diff --git a/src/ast/nodes.ts b/src/ast/nodes.ts index 8b46090..4bd3495 100644 --- a/src/ast/nodes.ts +++ b/src/ast/nodes.ts @@ -87,6 +87,16 @@ export class Program { ) {} } +export class If { + public type = "If"; + + constructor ( + public condition: BinaryOperation | IntegerNumber | UnaryOperation | Variable | CharConstant | StringConstant | BooleanConstant, + public main_statements: AST[] = [], + public else_statements: AST[] = [] + ) {} +} + export class Procedure { public type = "Procedure"; public args: VariableDeclaration[] = []; diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 8bd9fae..7943786 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -1,4 +1,4 @@ -import { AST, Assign, BinaryOperation, BooleanConstant, CharConstant, Compound, GlobalScope, IntegerNumber, ProcedureCall, Program, RealNumber, StringConstant, UnaryOperation, Variable } from "../ast/nodes"; +import { AST, Assign, BinaryOperation, BooleanConstant, CharConstant, Compound, GlobalScope, If, IntegerNumber, ProcedureCall, Program, RealNumber, StringConstant, UnaryOperation, Variable } from "../ast/nodes"; import { TypeBooleanOperationError, TypeOperationError } from "../errors/math"; import { TokenType } from "../lexer/tokens"; import { builtinProcedures } from "./builtins"; @@ -24,6 +24,8 @@ class Interpreter { return this.visitUnaryOperation(node as UnaryOperation); case "Compound": return this.visitCompound(node as Compound); + case "If": + return this.visitIf(node as If); case "Assign": return this.visitAssign(node as Assign); case "Variable": @@ -285,6 +287,21 @@ class Interpreter { } } + private async visitIf (node: If): Promise { + const condition = await this.visit(node.condition); + + if (condition) { + for (const statement of node.main_statements) { + await this.visit(statement); + } + } + else if (!condition && node.else_statements.length > 0) { + for (const statement of node.else_statements) { + await this.visit(statement); + } + } + } + private async visitAssign (node: Assign): Promise { const variableName = node.left.value; const newVariableValue = await this.visit(node.right); diff --git a/src/lexer/index.ts b/src/lexer/index.ts index 40c778d..6b699c2 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -37,7 +37,10 @@ import { GreaterThanToken, EqualToken, NotToken, - NotEqualToken + NotEqualToken, + IfToken, + ThenToken, + ElseToken } from "./tokens"; const RESERVED_KEYWORDS = { @@ -47,6 +50,9 @@ const RESERVED_KEYWORDS = { "début": new BeginToken(), "avec": new VariableDeclarationBlockToken(), "fin": new EndToken(), + "si": new IfToken(), + "sinon": new ElseToken(), + "alors": new ThenToken(), // Operator. "mod": new ModToken(), diff --git a/src/lexer/tokens.ts b/src/lexer/tokens.ts index b37e179..e1b0b60 100644 --- a/src/lexer/tokens.ts +++ b/src/lexer/tokens.ts @@ -11,6 +11,10 @@ export enum TokenType { MOD = "MOD", NOT = "NOT", + IF = "IF", + ELSE = "ELSE", + THEN = "THEN", + EQUAL = "EQUAL", NOT_EQUAL = "NOT_EQUAL", LESS_THAN = "LOWER_THAN", @@ -52,6 +56,24 @@ class BaseToken { ) {} } +export class IfToken extends BaseToken { + constructor () { + super(TokenType.IF, "si"); + } +} + +export class ElseToken extends BaseToken { + constructor () { + super(TokenType.ELSE, "sinon"); + } +} + +export class ThenToken extends BaseToken { + constructor () { + super(TokenType.THEN, "alors"); + } +} + export class IntegerToken extends BaseToken { constructor () { super(TokenType.INTEGER, "entier"); diff --git a/src/symbols/visitor.ts b/src/symbols/visitor.ts index 0255fb4..a564242 100644 --- a/src/symbols/visitor.ts +++ b/src/symbols/visitor.ts @@ -1,4 +1,4 @@ -import { AST, Assign, BinaryOperation, Compound, GlobalScope, Procedure, ProcedureCall, Program, StringConstant, UnaryOperation, Variable, VariableDeclaration } from "../ast/nodes"; +import { AST, Assign, BinaryOperation, Compound, GlobalScope, If, Procedure, ProcedureCall, Program, StringConstant, UnaryOperation, Variable, VariableDeclaration } from "../ast/nodes"; import { TypeOperationError, TypeOperationVariableError } from "../errors/math"; import { UndeclaredVariableTypeError } from "../errors/variables"; import { TokenType } from "../lexer/tokens"; @@ -31,6 +31,8 @@ class SemanticAnalyzer { return this.visitUnaryOperation(node); case "Compound": return this.visitCompound(node); + case "If": + return this.visitIf(node); case "VariableDeclaration": return this.visitVariableDeclaration(node); case "Assign": @@ -163,13 +165,24 @@ class SemanticAnalyzer { break; } + /** - * When it's a "+" operation, we can have - * operations with every numbers but also strings. + * Conditional operators can be used on anything. + * + * A "+" operation can have + * operations with anything except booleans. */ + case TokenType.EQUAL: + case TokenType.GREATER_THAN: + case TokenType.GREATER_THAN_OR_EQUAL: + case TokenType.LESS_THAN: + case TokenType.LESS_THAN_OR_EQUAL: case TokenType.PLUS: this.visit(node.left); this.visit(node.right); + break; + default: + throw new Error("Erreur de syntaxe.\ndebug: unknown binary operation."); } } @@ -189,6 +202,18 @@ class SemanticAnalyzer { } } + private visitIf (node: If): void { + this.visit(node.condition); + + for (const statement of node.main_statements) { + this.visit(statement); + } + + for (const statement of node.else_statements) { + this.visit(statement); + } + } + private visitVariableDeclaration (node: VariableDeclaration): void { const type_name = node.type_node.value; const type_symbol: BuiltinTypeSymbol | undefined = this.current_scope!.lookup(type_name);