Skip to content

Commit

Permalink
for loops + del keyword + tests suite update + version bump
Browse files Browse the repository at this point in the history
  • Loading branch information
ImShyMike committed Jan 18, 2025
1 parent 3e57123 commit f2b3f06
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 20 deletions.
17 changes: 16 additions & 1 deletion docs/docs/language-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ var = 100; # Redefining a variable's value does not need a semicolon
!!! note "Semicolon usage"
All variable declarations **must** end in a semicolon (`;`)

## Variable deletion
Variables can be deleted with the `del` keyword:
```sh linenums="1"
const var = 1; # Declare a constant variable
del var; # Delete it so it can be redeclared
```

## Value types
All currently suppoted value types are:

Expand Down Expand Up @@ -85,6 +92,7 @@ There are also many builtin functions:
* **bool(** value? **)**: Convert a value to a boolean
* **array(** ... or string **)**: Create a new array from the given values or turn a string into an array
* **type(** value **)**: Get the type of the given value
* **range(** start, end?, step? **)**: Generates an array from start to end with step
There are also many builtin modules:
Expand Down Expand Up @@ -157,7 +165,10 @@ if (1 == 1) {

### Loops

Infinite loops and while loops are supported.
Infinite loops, while loops and for loops are supported.

!!! note "For Loops"
For loops can only iterate over arrays.

```rust linenums="1"
loop {
Expand All @@ -171,6 +182,10 @@ while (value < threshold) {

print(value);
}

for i in range(5) {
print(i)
}
```

Supported keywords are:
Expand Down
2 changes: 1 addition & 1 deletion eryx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version of the package."""

CURRENT_VERSION = "0.3.8"
CURRENT_VERSION = "0.3.9"
16 changes: 16 additions & 0 deletions eryx/frontend/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,27 @@ class WhileStatement(Statement):
body: list[Statement]


@dataclass()
class ForStatement(Statement):
"""For statement class."""

variable: Expression
iterator: Expression
body: list[Statement]


@dataclass()
class BreakLiteral(Expression):
"""Break literal class."""


@dataclass()
class DelStatement(Statement):
"""Del statement class."""

identifier: Expression


@dataclass()
class ContinueLiteral(Expression):
"""Continue literal class."""
7 changes: 7 additions & 0 deletions eryx/frontend/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class TokenType(Enum):

LOOP = auto()
WHILE = auto()
FOR = auto()
IN = auto()
BREAK = auto()
CONTINUE = auto()

Expand All @@ -46,6 +48,8 @@ class TokenType(Enum):

EQUALS = auto()

DEL = auto()

COMMA = auto()
COLON = auto()
SEMICOLON = auto()
Expand Down Expand Up @@ -80,8 +84,11 @@ def __repr__(self) -> str:
"as": TokenType.AS,
"loop": TokenType.LOOP,
"while": TokenType.WHILE,
"for": TokenType.FOR,
"in": TokenType.IN,
"break": TokenType.BREAK,
"continue": TokenType.CONTINUE,
"del": TokenType.DEL,
}


Expand Down
46 changes: 46 additions & 0 deletions eryx/frontend/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
BreakLiteral,
CallExpression,
ContinueLiteral,
DelStatement,
Expression,
ForStatement,
FunctionDeclaration,
Identifier,
IfStatement,
Expand Down Expand Up @@ -537,6 +539,46 @@ def parse_loop_statement(self) -> Statement:

return LoopStatement(body)

def parse_for_statement(self) -> Statement:
"""Parse a for statement."""
self.next() # Skip the for keyword

variable = self.assert_next(
TokenType.IDENTIFIER, "Expected an identifier for the loop variable."
)

self.assert_next(TokenType.IN, "Expected the 'in' keyword for the for loop.")

iterator = self.parse_expression()

self.assert_next(
TokenType.OPEN_BRACE, "Expected opening brace for the for statement."
)

body = []
while self.not_eof() and self.at().type != TokenType.CLOSE_BRACE:
statement = self.parse_statement()
if statement != Expression():
body.append(statement)

self.assert_next(
TokenType.CLOSE_BRACE, "Expected closing brace for the for statement."
)

return ForStatement(
Identifier(variable.value), iterator, body
)

def parse_del_statement(self) -> Statement:
"""Parse a del statement."""
self.next() # Skip the del keyword

variable = self.assert_next(
TokenType.IDENTIFIER, "Expected an identifier after the del keyword."
)

return DelStatement(Identifier(variable.value))

def parse_while_statement(self) -> Statement:
"""Parse a while statement."""
self.next() # Skip the while keyword
Expand Down Expand Up @@ -590,6 +632,10 @@ def parse_statement(self) -> Statement:
return self.parse_while_statement()
case TokenType.LOOP:
return self.parse_loop_statement()
case TokenType.FOR:
return self.parse_for_statement()
case TokenType.DEL:
return self.parse_del_statement()
case _:
return self.parse_expression()

Expand Down
38 changes: 38 additions & 0 deletions eryx/runtime/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ def resolve(self, variable_name: str) -> "Environment":
# If it does not exist in the parent scope, raise an exception
raise RuntimeError(f'Variable "{variable_name}" not found in scope')

def delete_variable(self, variable_name: str) -> None:
"""Delete a variable from the current scope."""
if variable_name in self.variables:
if variable_name in self.constants:
del self.constants[self.constants.index(variable_name)]
del self.variables[variable_name]
else:
raise RuntimeError(f'Variable "{variable_name}" not found in scope')

def setup_scope(self) -> None:
"""Setup the global scope."""
# Declare global variables
Expand All @@ -103,6 +112,7 @@ def setup_scope(self) -> None:
self.declare_variable("bool", NativeFunctionValue(_bool), True)
self.declare_variable("array", NativeFunctionValue(_array), True)
self.declare_variable("type", NativeFunctionValue(_type), True)
self.declare_variable("range", NativeFunctionValue(_range), True)


def get_value(value: RuntimeValue, inside_array: bool = False) -> str:
Expand Down Expand Up @@ -177,6 +187,34 @@ def _time(_: list[RuntimeValue], __: Environment) -> RuntimeValue:
return NumberValue(time.time())


def _range(args: list[RuntimeValue], _: Environment) -> RuntimeValue:
if len(args) == 1:
if isinstance(args[0], NumberValue):
return ArrayValue([NumberValue(i) for i in range(int(args[0].value))])
if len(args) == 2:
if all(isinstance(i, NumberValue) for i in args):
return ArrayValue(
[NumberValue(i) for i in range(
int(args[0].value), # type: ignore
int(args[1].value), # type: ignore
)
]
)
if len(args) == 3:
if all(isinstance(i, NumberValue) for i in args):
return ArrayValue(
[
NumberValue(i)
for i in range(
int(args[0].value), # type: ignore
int(args[1].value), # type: ignore
int(args[2].value), # type: ignore
)
]
)
raise RuntimeError(f"Cannot create range with {args}")


def _input(args: list[RuntimeValue], env: Environment) -> RuntimeValue:
if env.disable_file_io:
raise RuntimeError("Input function is disabled")
Expand Down
56 changes: 55 additions & 1 deletion eryx/runtime/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
BreakLiteral,
CallExpression,
ContinueLiteral,
DelStatement,
ForStatement,
FunctionDeclaration,
Identifier,
IfStatement,
Expand Down Expand Up @@ -224,6 +226,18 @@ def eval_import_statement(
return NullValue()


def eval_del_statement(
del_statement: DelStatement, environment: Environment
) -> RuntimeValue:
"""Evaluate a del statement."""
if not isinstance(del_statement.identifier, Identifier):
raise RuntimeError("Expected an identifier (variable) to delete.")

environment.delete_variable(del_statement.identifier.symbol)

return NullValue()


def eval_loop_statement(
loop_statement: LoopStatement, environment: Environment
) -> RuntimeValue:
Expand All @@ -241,6 +255,28 @@ def eval_loop_statement(
return NullValue()


def eval_for_statement(
for_statement: ForStatement, environment: Environment
) -> RuntimeValue:
"""Evaluate a for statement."""
if not isinstance(for_statement.variable, Identifier):
raise RuntimeError("Expected an identifier as a variable.")
try:
iterator = evaluate(for_statement.iterator, environment)
if not isinstance(iterator, ArrayValue):
raise RuntimeError("Expected an array as an iterator.")
for element in iterator.elements:
environment.declare_variable(for_statement.variable.symbol, element, False, True)
try:
for statement in for_statement.body:
evaluate(statement, environment)
except ContinueException:
pass
except BreakException:
pass

return NullValue()

def eval_while_statement(
while_statement: WhileStatement, environment: Environment
) -> RuntimeValue:
Expand Down Expand Up @@ -387,7 +423,21 @@ def eval_member_expression(
else NullValue()
)

raise RuntimeError("Expected a computed property for an array (number).")
raise RuntimeError("Expected a computed property for an array: string[number].")

if isinstance(object_value, StringValue):
if member.computed:
property_value = evaluate(member.property, environment)
if not isinstance(property_value, NumberValue):
raise RuntimeError("Expected a number as an index.")

return (
StringValue(object_value.value[int(property_value.value)])
if len(object_value.value) > int(property_value.value)
else NullValue()
)

raise RuntimeError("Expected a computed property for a string: string[number].")

raise RuntimeError("Expected an object or array.")

Expand Down Expand Up @@ -631,10 +681,14 @@ def evaluate(ast_node: Statement | None, environment: Environment) -> RuntimeVal
return eval_object_expression(ast_node, environment)
case IfStatement():
return eval_if_statement(ast_node, environment)
case DelStatement():
return eval_del_statement(ast_node, environment)
case LoopStatement():
return eval_loop_statement(ast_node, environment)
case WhileStatement():
return eval_while_statement(ast_node, environment)
case ForStatement():
return eval_for_statement(ast_node, environment)
case BreakLiteral():
raise BreakException()
case ContinueLiteral():
Expand Down
Loading

0 comments on commit f2b3f06

Please sign in to comment.