Skip to content

Commit

Permalink
logo + favicon + parser error handling +
Browse files Browse the repository at this point in the history
  • Loading branch information
ImShyMike committed Dec 19, 2024
1 parent 704634c commit 5c49f83
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 75 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Eryx
# [![Eryx](https://github.com/ImShyMike/Eryx/blob/main/assets/eryx_small.png)][pypi_url]
[![Build Status](https://github.com/ImShyMike/Eryx/actions/workflows/python-package.yml/badge.svg)](https://github.com/ImShyMike/Eryx/actions/workflows/python-package.yml)
[![License](https://img.shields.io/pypi/l/Eryx)](https://github.com/ImShyMike/Eryx/blob/main/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/Eryx)][pypi_url]
Expand Down Expand Up @@ -31,10 +31,10 @@ python -m eryx
```

The list of subcommands is:
- repl => Start the [REPL](https://wikipedia.org/wiki/REPL)
- run => Run an Eryx file
- playground => Start the web playground
- test => Run the test suite
* **repl**: Start the [REPL](https://wikipedia.org/wiki/REPL)
* **run**: Run an Eryx file
* **playground**: Start the web playground
* **test**: Run the test suite

## Documentation
Coming soon...
Expand Down
Binary file added assets/eryx.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/eryx_small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 12 additions & 8 deletions eryx/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Eryx entry point and Command Line Interface (CLI) module."""

import argparse
import os

import pytest
from colorama import init
Expand Down Expand Up @@ -74,14 +75,17 @@ def main():
if args.command == "repl":
start_repl(log_ast=args.ast, log_result=args.result, log_tokens=args.tokenize)
elif args.command == "run":
with open(args.filepath, "r", encoding="utf8") as file:
source_code = file.read()
run_code(
source_code,
log_ast=args.ast,
log_result=args.result,
log_tokens=args.tokenize,
)
try:
with open(args.filepath, "r", encoding="utf8") as file:
source_code = file.read()
run_code(
source_code,
log_ast=args.ast,
log_result=args.result,
log_tokens=args.tokenize,
)
except Exception as e: # pylint: disable=broad-except
print(f"eryx: can't open file '{args.filepath}': [Errno {e.args[0]}] {e.args[1]}")
elif args.command == "playground":
start_playground(args.host or "0.0.0.0", port=args.port or 80)
elif args.command == "test":
Expand Down
57 changes: 19 additions & 38 deletions eryx/frontend/lexer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Lexer for the fronted."""

import sys
from enum import Enum, auto
from typing import Any, Union

from colorama import Fore, init

from eryx.utils.errors import syntax_error

init(autoreset=True)


Expand Down Expand Up @@ -78,38 +79,23 @@ def is_skipable(char: str) -> bool:
) # Skip spaces, newlines, tabs, and carriage returns


def position_to_line_column(source_code: str, position: int) -> tuple[int, int]:
"""Convert a position to a line and column number."""
# Get the substring up to the given position
substring = source_code[:position]

# Count the number of newline characters to determine the line
line = substring.count("\n") + 1

# Find the column by looking for the last newline
last_newline_pos = substring.rfind("\n")
column = position - last_newline_pos if last_newline_pos != -1 else position + 1

return (line, column)


def get_line_string(source_code: str, line: int) -> str:
"""Get the line string from the source code."""
lines = source_code.split("\n")

return lines[line - 1]


def tokenize(source_code: str) -> list[Token]:
"""Tokenize the source code."""
tokens = []
source_size = len(source_code)
src = list(source_code)
comment = False

while len(src) > 0:
negative_num = False
current_pos = source_size - len(src)

if comment:
if src[0] in ("\n", "\r", ";"):
comment = False
src.pop(0)
continue

single_char_tokens = {
"(": TokenType.OPEN_PAREN,
")": TokenType.CLOSE_PAREN,
Expand All @@ -133,6 +119,12 @@ def tokenize(source_code: str) -> list[Token]:
tokens.append(Token(token, single_char_tokens[token], current_pos))
continue

# Check for comments
if src[0] == "#":
comment = True
src.pop(0)
continue

# If its not a single character token, check for negative numbers
if src[0] == "-":
if len(src) > 0 and src[1].isdigit():
Expand Down Expand Up @@ -249,22 +241,11 @@ def tokenize(source_code: str) -> list[Token]:

else:
# If this is reached, its an unknown character
current_line, current_col = position_to_line_column(
source_code, current_pos
)
line = get_line_string(source_code, current_line)
current_line_str = str(current_line).rjust(3)
print(f"\n{Fore.CYAN}{current_line_str}:{Fore.WHITE} {line}")
print(
Fore.YELLOW
+ "^".rjust(current_col + len(current_line_str) + 2)
+ Fore.WHITE
)
print(
f"{Fore.RED}SyntaxError{Fore.WHITE}: Unknown character found in source "
f"'{Fore.MAGENTA}{src.pop(0)}{Fore.WHITE}'"
syntax_error(
source_code,
current_pos,
f"Unknown character found in source '{Fore.MAGENTA}{src.pop(0)}{Fore.WHITE}'",
)
sys.exit(1)

tokens.append(Token("EOF", TokenType.EOF, source_size - len(src)))

Expand Down
28 changes: 20 additions & 8 deletions eryx/frontend/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
)
from eryx.frontend.lexer import Token, TokenType, tokenize

from eryx.utils.errors import syntax_error

class Parser:
"""Parser class."""

def __init__(self) -> None:
self.source_code = ""
self.tokens = []

def not_eof(self) -> bool:
Expand All @@ -48,10 +50,7 @@ def assert_next(self, token_type: TokenType, error: str) -> Token:
"""Assert that the next token is of a certain type and return it."""
token = self.next()
if token.type != token_type:
raise RuntimeError(
f"Parser error on position {token.position}: "
f"\n{error} {token} - Expected: {token_type}"
)
syntax_error(self.source_code, token.position, error)
return token

def parse_additive_expression(self) -> Expression:
Expand Down Expand Up @@ -120,7 +119,11 @@ def parse_member_expression(self) -> Expression:
proprty = self.parse_primary_expression() # Identifier

if not isinstance(proprty, Identifier):
raise RuntimeError("Expected an identifier as a property.")
syntax_error(
self.source_code,
self.at().position,
"Expected an identifier as a property.",
)
else:
computed = True
proprty = self.parse_expression()
Expand Down Expand Up @@ -186,7 +189,7 @@ def parse_primary_expression(self) -> Expression:
) # Skip the close parenthesis
return expression
case _:
raise RuntimeError(f"Unexpected token: {token}")
syntax_error(self.source_code, token.position, "Unexpected token.")

def parse_assignment_expression(self) -> Expression:
"""Parse an assignment expression."""
Expand Down Expand Up @@ -319,7 +322,11 @@ def parse_function_declaration(self) -> Statement:
parameters = []
for argument in arguments:
if not isinstance(argument, Identifier):
raise RuntimeError("Function arguments must be identifiers.")
syntax_error(
self.source_code,
self.at().position,
"Function arguments must be identifiers.",
)
parameters.append(argument.symbol)

self.assert_next(TokenType.OPEN_BRACE, "Expected an opening brace.")
Expand All @@ -345,7 +352,11 @@ def parse_variable_declaration(self) -> Statement:
if self.at().type == TokenType.SEMICOLON:
self.next() # Skip the semicolon
if is_constant:
raise RuntimeError("Constant declaration must have an initial value.")
syntax_error(
self.source_code,
self.at().position,
"Constant declaration must have an initial value.",
)

return VariableDeclaration(is_constant, Identifier(identifier))

Expand Down Expand Up @@ -381,6 +392,7 @@ def parse_statement(self) -> Statement:

def produce_ast(self, source_code: str) -> Program:
"""Produce an abstract syntax tree (AST) from source code."""
self.source_code = source_code
self.tokens = tokenize(source_code)
program = Program(body=[])

Expand Down
8 changes: 7 additions & 1 deletion eryx/playground/playground.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Web UI for Eryx."""
"""Web playground for Eryx."""

import io
import json
Expand Down Expand Up @@ -208,6 +208,12 @@ def static_route(path):
return app.send_static_file(path)


@app.route("/favicon.ico")
def favicon():
"""Serve the favicon."""
return app.send_static_file("favicon.ico")


def start_playground(host="0.0.0.0", port=80):
"""Start the web playground."""
app.run(host=host, port=port, debug=False, use_reloader=False)
Expand Down
Binary file added eryx/playground/static/eryx.ico
Binary file not shown.
31 changes: 18 additions & 13 deletions eryx/playground/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Eryx Playground</title>
<meta name="description" content="Online runtime for Eryx">
<meta name="description" content="Online playground for the Eryx programming language">
<meta property="og:title" content="Eryx Playground">
<meta property="og:description" content="Online runtime for Eryx">
<meta property="og:url" content="https://lang.shymike.tech/">
<meta property="og:description" content="Online playground for the Eryx programming language">
<meta property="og:type" content="website">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="static/style.css">
Expand Down Expand Up @@ -48,15 +47,19 @@ <h3>Control Structures</h3>
<ul>
<li>If/Else Statements</li>
<ul>
<h3>Comments</h3>
<ul>
<li>Single Line Comments</li>
</ul>
<h3>Example</h3>
<pre>
const timenow = time();

func add (x, y) {
let result = x + y;

func sub (a, b) {
a - b
func sub(a, b) {
return a - b;
}

let result2 = sub(y, x);
Expand All @@ -66,33 +69,35 @@ <h3>Example</h3>
y
};

let five = 10 - 5;
let five;
five = 10 - 5;
const neg_num = -1000;
let float = 69.420;
const str = "Strings work!!!!";
let flt = 69.420;
const string = "Strings work!!!!";

let obj = {
five,
neg_num,
float,
flt,
result2,
timenow,
result,
object: obj2,
str
string,
arr: [1, 2, 3, 4, 5]
};

obj
return obj;
}

print(add(5, 7))

func makeAdder(offset) {
func add(x, y) {
x + y + offset
return x + y + offset;
}

add
return add;
}

let adder = makeAdder(10);
Expand Down
3 changes: 1 addition & 2 deletions eryx/runtime/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,13 @@ def eval_call_expression(
function_argument, arguments[i], False
)

result = NullValue()
# Evaluate the function body statement by statement
for statement in func.body:
if isinstance(statement, ReturnStatement):
return evaluate(statement, function_environment)
evaluate(statement, function_environment)

return result
return NullValue()

raise RuntimeError("Cannot call a non-function value.")

Expand Down
42 changes: 42 additions & 0 deletions eryx/utils/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Stuff for handling errors."""

import sys
from colorama import Fore

def position_to_line_column(source_code: str, position: int) -> tuple[int, int]:
"""Convert a position to a line and column number."""
# Get the substring up to the given position
substring = source_code[:position]

# Count the number of newline characters to determine the line
line = substring.count("\n") + 1

# Find the column by looking for the last newline
last_newline_pos = substring.rfind("\n")
column = position - last_newline_pos if last_newline_pos != -1 else position + 1

return (line, column)


def get_line_strings(source_code: str, line: int) -> str:
"""Get the line string from the source code."""
lines = source_code.split("\n")

return lines[line - 10: line]

def syntax_error(source_code: str, pos: int, error_message: str) -> None:
"""Handle a syntax error."""
current_line, current_col = position_to_line_column(
source_code, pos
)
lines = get_line_strings(source_code, current_line)
print()
for n, line in enumerate(lines):
print(f"{Fore.CYAN}{str(current_line - (len(lines) - n) + 1).rjust(3)}:{Fore.WHITE} {line}")
print(
Fore.YELLOW
+ "^".rjust(current_col + len(str(current_line).rjust(3)) + 2)
+ Fore.WHITE
)
print(f"{Fore.RED}SyntaxError{Fore.WHITE}: {error_message}")
sys.exit(1)

0 comments on commit 5c49f83

Please sign in to comment.