diff --git a/src/example/test.wrsc b/src/example/test.wrsc index c380073..17ed4e5 100644 --- a/src/example/test.wrsc +++ b/src/example/test.wrsc @@ -1 +1,8 @@ -let x = 92 * (3 + 4) / 2 +let a = 5419351; +let b = 1725033; + +let pi = { + a, + b, + pi: a / b +}; \ No newline at end of file diff --git a/src/frontend/ast.ts b/src/frontend/ast.ts index a9ee855..d89b2d5 100644 --- a/src/frontend/ast.ts +++ b/src/frontend/ast.ts @@ -8,6 +8,10 @@ export type NodeType = // EXPRESSIONS | "AssignmentExpression" + + // LITERALS + | "Property" + | "ObjectLiteral" | "NumericLiteral" | "Identifier" | "BinaryExpression"; @@ -75,4 +79,21 @@ export interface Identifier extends Expression { export interface NumericLiteral extends Expression { kind: "NumericLiteral"; value: number; +} + +/** + * Represents a property in the AST. + */ +export interface Property extends Expression { + kind: "Property"; + key: string; + value?: Expression; +} + +/** + * Represents an object literal in the AST. + */ +export interface ObjectLiteral extends Expression { + kind: "ObjectLiteral"; + properties: Property[]; } \ No newline at end of file diff --git a/src/frontend/lexer.ts b/src/frontend/lexer.ts index 8e82558..41a1440 100644 --- a/src/frontend/lexer.ts +++ b/src/frontend/lexer.ts @@ -11,11 +11,15 @@ export enum TokenType { Const, // Grouping / Operators + BinaryOperator, Equals, Semicolon, - OpenParentesis, - CloseParentesis, - BinaryOperator, + Comma, + Colon, + OpenParentesis, // ( + CloseParentesis, // ) + OpenBracket, // { + CloseBracket, // } // Misc EOF, @@ -74,7 +78,7 @@ export function isInteger(char: string): boolean { * @returns True if the character is a whitespace character, false otherwise. */ export function isSkipable(char: string): boolean { - return [' ', '\n', '\t'].includes(char); + return [' ', '\n', '\t', '\r'].includes(char); } /** @@ -91,12 +95,20 @@ export function tokenize(sourceCode: string): Token[] { tokens.push(token(src.shift()!, TokenType.OpenParentesis)); } else if (src[0] === ')') { tokens.push(token(src.shift()!, TokenType.CloseParentesis)); + } else if (src[0] === '{') { + tokens.push(token(src.shift()!, TokenType.OpenBracket)); + } else if (src[0] === '}') { + tokens.push(token(src.shift()!, TokenType.CloseBracket)); } else if (['+', '-', '*', '/', '%'].includes(src[0])) { tokens.push(token(src.shift()!, TokenType.BinaryOperator)); } else if (src[0] === '=') { tokens.push(token(src.shift()!, TokenType.Equals)); } else if (src[0] === ';') { tokens.push(token(src.shift()!, TokenType.Semicolon)); + } else if (src[0] === ':') { + tokens.push(token(src.shift()!, TokenType.Colon)); + } else if (src[0] === ',') { + tokens.push(token(src.shift()!, TokenType.Comma)); } else { // Handle multi-char tokens diff --git a/src/frontend/parser.ts b/src/frontend/parser.ts index 18e845b..6adfbfd 100644 --- a/src/frontend/parser.ts +++ b/src/frontend/parser.ts @@ -1,5 +1,5 @@ import { warn } from '../runtime/log'; -import { Statement, Program, Expression, BinaryExpression, NumericLiteral, Identifier, VariableDeclaration, AssignmentExpression } from './ast'; +import { Statement, Program, Expression, BinaryExpression, NumericLiteral, Identifier, VariableDeclaration, AssignmentExpression, Property, ObjectLiteral } from './ast'; import { tokenize, Token, TokenType } from './lexer'; /** @@ -133,7 +133,7 @@ export default class Parser { * @returns The parsed assignment expression. */ private parseAssignmentExpression(): Expression { - const left = this.parseAdditiveExpression(); // TODO: Switch this into ObjectExpression + const left = this.parseObjectExpression(); // TODO: Switch this into ObjectExpression if (this.at().type === TokenType.Equals) { this.eatToken(); // Advance past the '=' @@ -148,6 +148,58 @@ export default class Parser { return left; } + /** + * Parses an object literal expression and returns an AST node representing the expression. + * @returns The AST node representing the object literal expression. + * @throws If an unexpected token is encountered while parsing the expression. + */ + private parseObjectExpression(): Expression { + + if (this.at().type !== TokenType.OpenBracket) { + return this.parseAdditiveExpression(); + } + + this.eatToken(); // Advance past the '{' + const properties = new Array(); + + while (this.notEOF() && this.at().type !== TokenType.CloseBracket) { + const key = this.except(TokenType.Identifier, "Unexpected token founded inside object expression. Expected identifier").value; + + if (this.at().type === TokenType.Comma) { + this.eatToken(); + properties.push({ + kind: 'Property', + key + }); + + continue; + } else if (this.at().type === TokenType.CloseBracket) { + properties.push({ + kind: 'Property', + key + }); + break; + } + + this.except(TokenType.Colon, "Unexpected token founded inside object expression. Expected colon"); + const value = this.parseExpression(); + + properties.push({ + kind: 'Property', + key, + value + }); + + if (this.at().type !== TokenType.CloseBracket) { + this.except(TokenType.Comma, "Unexpected token founded inside object expression. Expected comma"); + } + + } + + this.except(TokenType.CloseBracket, "Unexpected token founded inside object expression. Expected closing brace"); + return { kind: "ObjectLiteral", properties } as ObjectLiteral; + } + /** * Parses an additive expression. * @returns The parsed additive expression. diff --git a/src/main.ts b/src/main.ts index e5e1d8b..511433f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,30 +1,19 @@ import { createInterface } from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; +import fs from 'fs'; // Create a readline interface for user input/output. const rl = createInterface({ input, output }); import Parser from "./frontend/parser"; import { evaluate } from './runtime/interpreter'; -import Environment from './runtime/environment'; -import { MK_NULL, MK_NUMBER, MK_BOOL } from './runtime/values'; - -// Define some initial values for the environment -const environments = { - 'x': MK_NUMBER(31), - 'true': MK_BOOL(true), - 'false': MK_BOOL(false), - 'null': MK_NULL() -}; +import { createGlobalEnvironment } from './runtime/environment'; // Define an async function for the REPL -!(async function repl() { +async function repl() { // Create a new parser and environment for each loop const parser = new Parser(); - const env = new Environment(); - - // Declare the initial environment values - Object.entries(environments).forEach(key => env.declare(...key)); + const env = createGlobalEnvironment(); // Print a welcome message console.log(`\nWraithScript REPL v0.0.1dev`); @@ -47,4 +36,42 @@ const environments = { const result = evaluate(program, env); console.log(result); } -})(); \ No newline at end of file +} + +/** + * Runs a WraithScript file. + * @param filename The path to the file to run. + */ +async function run(filename: string) { + const parser = new Parser(); + const env = createGlobalEnvironment(); + + // Read the file + const file = fs.readFileSync(filename, 'utf-8'); + + // Parse the file into an AST + const program = parser.produceAST(file); + + // Evaluate the AST and print the result + const result = evaluate(program, env); + console.log(result); + + // Exit with a status code of 0 + process.exit(0); +} + +/** + * Parses command-line arguments and runs the appropriate command. + */ +function main(): void { + if (process.argv.length === 2) { + repl(); + } else if (process.argv.length === 3) { + run(process.argv[2]); + } else { + console.log('Usage: wraithscript [filename]'); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/src/runtime/environment.ts b/src/runtime/environment.ts index 0e66b5f..2c534c0 100644 --- a/src/runtime/environment.ts +++ b/src/runtime/environment.ts @@ -1,5 +1,21 @@ import { warn } from "./log"; -import { RuntimeValue } from "./values"; +import { MK_BOOL, MK_NULL, RuntimeValue } from "./values"; + +/** + * Creates a new global environment with pre-defined values for true, false, and null. + * @returns The new global environment. + */ +export function createGlobalEnvironment() { + const env = new Environment(); + + Object.entries({ + 'true': MK_BOOL(true), + 'false': MK_BOOL(false), + 'null': MK_NULL() + }).forEach(key => env.declare(...key)); + + return env; +} /** * Represents an environment for variable and constant declarations. @@ -21,6 +37,7 @@ export default class Environment { this.constants = new Set(); this.throwErrors = throwErrors; + } /** diff --git a/src/runtime/eval/expressions.ts b/src/runtime/eval/expressions.ts index 607f10b..c248baf 100644 --- a/src/runtime/eval/expressions.ts +++ b/src/runtime/eval/expressions.ts @@ -1,8 +1,15 @@ -import { AssignmentExpression, BinaryExpression, Identifier } from "../../frontend/ast"; +import { AssignmentExpression, BinaryExpression, Identifier, ObjectLiteral } from "../../frontend/ast"; import Environment from "../environment"; import { evaluate } from "../interpreter"; -import { MK_NULL, NumberValue, RuntimeValue } from "../values"; +import { MK_NULL, NumberValue, ObjectValue, RuntimeValue } from "../values"; +/** + * Evaluates a binary expression with two numeric operands and returns a numeric value. + * @param lhs The left-hand side operand of the binary expression. + * @param rhs The right-hand side operand of the binary expression. + * @param operator The operator used in the binary expression. + * @returns The numeric value resulting from the binary expression. + */ function evaluateNumericBinaryExpression(lhs: NumberValue, rhs: NumberValue, operator: string): NumberValue { let value: number = 0; @@ -75,3 +82,22 @@ export function evaluateAssignment(node: AssignmentExpression, env: Environment, const name = (node.asignee as Identifier).symbol; return env.assign(name, evaluate(node.value, env)); } + +/** + * Evaluates an object literal expression and returns an object value. + * @param node The object literal AST node to evaluate. + * @param env The environment in which to evaluate the expression. + * @returns The object value represented by the object literal. + */ +export function evaluateObjectExpression(node: ObjectLiteral, env: Environment): RuntimeValue { + const object = { type: "object", properties: new Map() } as ObjectValue; + + for (let { key, value } of node.properties) { + + const runtimeValue = (value === undefined) ? env.lookup(key) : evaluate(value, env); + + object.properties.set(key, runtimeValue); + } + + return object; +} diff --git a/src/runtime/interpreter.ts b/src/runtime/interpreter.ts index fb84a93..aa715a9 100644 --- a/src/runtime/interpreter.ts +++ b/src/runtime/interpreter.ts @@ -1,8 +1,8 @@ import { RuntimeValue } from './values'; -import { AssignmentExpression, BinaryExpression, Identifier, NumericLiteral, Program, Statement, VariableDeclaration } from '../frontend/ast'; +import { AssignmentExpression, BinaryExpression, Identifier, NumericLiteral, ObjectLiteral, Program, Statement, VariableDeclaration } from '../frontend/ast'; import Environment from './environment'; import { evaluateProgram, evaluateVariableDeclaration } from './eval/statements'; -import { evaluateAssignment, evaluateBinaryExpression, evaluateIdentifier } from './eval/expressions'; +import { evaluateAssignment, evaluateBinaryExpression, evaluateIdentifier, evaluateObjectExpression } from './eval/expressions'; /** * Evaluates an AST node and returns a runtime value. @@ -25,6 +25,10 @@ export function evaluate(ast: Statement, env: Environment): RuntimeValue { // Evaluate an identifier return evaluateIdentifier(ast as Identifier, env); + case 'ObjectLiteral': + // Evaluate an object literal + return evaluateObjectExpression(ast as ObjectLiteral, env); + case 'AssignmentExpression': // Evaluate an assignment expression return evaluateAssignment(ast as AssignmentExpression, env); diff --git a/src/runtime/values.ts b/src/runtime/values.ts index 9fcd3a4..e1ef4ba 100644 --- a/src/runtime/values.ts +++ b/src/runtime/values.ts @@ -1,7 +1,7 @@ /** * The possible value types. */ -export type ValueType = "null" | "number" | "boolean"; +export type ValueType = "null" | "number" | "boolean" | "object"; /** * The interface for a runtime value. @@ -52,4 +52,12 @@ export interface NumberValue extends RuntimeValue { * @param value - The number value. * @returns The number value. */ -export const MK_NUMBER = (value: number = 0) => ({ type: "number", value }) as NumberValue; \ No newline at end of file +export const MK_NUMBER = (value: number = 0) => ({ type: "number", value }) as NumberValue; + +/** + * The interface for an object value. + */ +export interface ObjectValue extends RuntimeValue { + type: "object"; + properties: Map; +} \ No newline at end of file