diff --git a/README.md b/README.md index 72be39c..3baf7f7 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Eryx is a decently fast and simple dynamically typed programming language similar to javascript/python. -> Why the name "Eryx"? -> The language was made using python which is [a family of snake](https://en.wikipedia.org/wiki/Pythonidae) and the name eryx is [a snake genus](https://en.wikipedia.org/wiki/Eryx_(snake)). +> Why the name "**Eryx**"? +>
The language was made using python which is [a family of snake](https://en.wikipedia.org/wiki/Pythonidae) and the name eryx is [a snake genus](https://en.wikipedia.org/wiki/Eryx_(snake)). ## Documentation @@ -23,7 +23,7 @@ Full documentation is available at [https://ImShyMike.github.io/Eryx](https://Im ## Online IDE -An online IDE is hosted at [https://eryx-ide.shymike.tech](https://eryx-ide.shymike.tech). It utilizes the `eryx server` command but has file I/O, input(), exit() and importing disabled (using `--no-file-io`). +An online IDE is hosted at [https://eryx-ide.shymike.tech](https://eryx-ide.shymike.tech). It utilizes the `eryx server` command but has the `os` and `file` modules and `input()` and `exit()` functions disabled (using `--no-file-io`). ## Package Index diff --git a/docs/docs/language-features.md b/docs/docs/language-features.md index 474e004..dc3d218 100644 --- a/docs/docs/language-features.md +++ b/docs/docs/language-features.md @@ -89,43 +89,96 @@ func add(x, y) { print(add(1, 2)); # Output: 3 ``` -There are also many builtin functions: +## Builtin functions + +* **print(** ... **)**: Print all values passed to it. +* **input(** text? **)**: Get user input as a string, optionally prompt with a message. +* **len(** item **)**: Get the length of a string, array, or object. +* **exit(** code? **)**: Exit the program, optionally with a status code. +* **str(** value? **)**: Convert a value to its string representation. +* **int(** value? **)**: Convert a value to an integer. +* **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. !!! note "Values" Values containing '?' are optional and '...' refers to any amount of arguments. -* **print(** ... **)**: Print all values passed to it -* **input(** text? **)**: Get user input as a string, optionally prompt with a message -* **len(** item **)**: Get the length of a string, array, or object -* **exit(** code? **)**: Exit the program, optionally with a status code -* **str(** value? **)**: Convert a value to its string representation -* **int(** value? **)**: Convert a value to an integer -* **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. -They can be imported using `import` (without ".eryx") - -Full list: - -* **time**: - * **time()**: Get the current time in seconds since the Epoch -* **math**: - * **sum(** array **)**: Get the sum of an array of numbers - * **round(** number, n? **)**: Round a number to the n'th decimal place (default 0) - * **min(** array **)**: Get the minimum value from an array of numbers - * **max(** array **)**: Get the maximum value from an array of numbers - * **random()**: Get a random number between 0 and 1 - * **pi**: The value for pi -* **file**: - * **read(** filename **)**: Read the contents of a file as a string - * **write(** filename, text **)**: Write to a file - * **append(** filename, text **)**: Append to the contents of a file -* **http**: (WIP) - * **get(** url **)**: Send a get request to a URL - * **post(** url, data **)**: Send a post request with JSON data as a string to a URL +## Builtin modules + +Builtin modules can be imported using `import` (without ".eryx") + +### math + +* **log(** x, base? **)**: Get the logarithm of a number with the specified base. +* **sqrt(** x **)**: Get the square root of a number. +* **random()**: Get a random number between 0 and 1. +* **round(** x, digits? **)**: Round a number to the specified number of digits. +* **sum(** array **)**: Get the sum of an array of numbers. +* **min(** array **)**: Get the minimum value in an array of numbers. +* **max(** array **)**: Get the maximum value in an array of numbers. +* **abs(** x **)**: Get the absolute value of a number. +* **pow(** base, exponent **)**: Get the result of raising a base to an exponent. +* **log10(** x **)**: Get the base-10 logarithm of a number. +* **sin(** x **)**: Get the sine of a number. +* **cos(** x **)**: Get the cosine of a number. +* **tan(** x **)**: Get the tangent of a number. +* **asin(** x **)**: Get the arcsine of a number. +* **acos(** x **)**: Get the arccosine of a number. +* **atan(** x **)**: Get the arctangent of a number. +* **floor(** x **)**: Get the largest integer less than or equal to a number. +* **ceil(** x **)**: Get the smallest integer greater than or equal to a number. +* **factorial(** x **)**: Get the factorial of a number. + +### file + +* **read(** filename **)**: Read the contents of a file. +* **write(** filename, content **)**: Write content to a file. +* **append(** filename, content **)**: Append content to a file. +* **exists(** filename **)**: Check if a file exists. +* **delete(** filename **)**: Delete a file. +* **copy(** source, destination **)**: Copy a file. +* **move(** source, destination **)**: Move a file. +* **list(** directory **)**: List files in a directory. +* **size(** filename **)**: Get the size of a file. + +### http + +* **get(** url **)**: Make a GET request to a URL. +* **post(** url, data **)**: Make a POST request to a URL with data. +* **put(** url, data **)**: Make a PUT request to a URL with data. +* **delete(** url **)**: Make a DELETE request to a URL. +* **urlencode(** data **)**: URL-encode a string. +* **urldecode(** data **)**: URL-decode a string. + +### time + +* **time()**: Get the current time in seconds since the epoch. +* **sleep(** seconds **)**: Sleep for a specified number of seconds. +* **format(** time **)**: Format a time value as a string. +* **timezone_offset()**: Get the timezone offset in seconds. + +### string + +* **split(** string, separator **)**: Split a string by a separator. +* **join(** array, separator **)**: Join an array of strings with a separator. +* **replace(** string, search, replace **)**: Replace occurrences of a substring in a string. +* **contains(** string, search **)**: Check if a string contains a substring. + +### array + +* **push(** array, value **)**: Add a value to the end of an array. +* **pop(** array **)**: Remove and return the last value from an array. +* **shift(** array **)**: Remove and return the first value from an array. +* **unshift(** array, value **)**: Add a value to the beginning of an array. +* **sort(** array **)**: Sort an array of numbers. + +### os + +* **cwd()**: Get the current working directory. +* **chdir(** directory **)**: Change the current working directory. +* **env(** variable? **)**: Get the value of an environment variable or all environment variables if no variable is specified. ## Classes diff --git a/eryx/__init__.py b/eryx/__init__.py index 831448b..fa6d3ec 100644 --- a/eryx/__init__.py +++ b/eryx/__init__.py @@ -1,3 +1,3 @@ """Version of the package.""" -CURRENT_VERSION = "0.3.13" +CURRENT_VERSION = "0.4.0" diff --git a/eryx/frontend/lexer.py b/eryx/frontend/lexer.py index e89e1f5..d89b7a0 100644 --- a/eryx/frontend/lexer.py +++ b/eryx/frontend/lexer.py @@ -189,7 +189,7 @@ def tokenize(source_code: str) -> list[Token]: if src[0] == "-": if len(src) > 0 and (src[1].isdigit() or src[1].isalpha() or src[1] == "_"): negative_num = True # Set negative number flag - src.pop(0) + src.pop(0) else: # If its not a negative number, its a "-" operator tokens.append(Token(src.pop(0), TokenType.BINARY_OPERATOR, current_pos)) diff --git a/eryx/frontend/syntax_highlighter.py b/eryx/frontend/syntax_highlighter.py index 514438f..3ad341b 100644 --- a/eryx/frontend/syntax_highlighter.py +++ b/eryx/frontend/syntax_highlighter.py @@ -1,5 +1,7 @@ """Module for lexer based syntax highlighting code.""" +# This file is obsolete for now (may be used in the future) + from colorama import Fore, init from eryx.frontend.lexer import TokenType, tokenize diff --git a/eryx/runtime/environment.py b/eryx/runtime/environment.py index 3503c31..27dcf49 100644 --- a/eryx/runtime/environment.py +++ b/eryx/runtime/environment.py @@ -1,9 +1,12 @@ """Environment class for storing variables (also called scope).""" import math +import os import random import sys import time +from pathlib import Path +from urllib.parse import quote, unquote import requests @@ -186,22 +189,6 @@ def _print(args: list[RuntimeValue], _: Environment) -> RuntimeValue: return NullValue() -def _sqrt(args: list[RuntimeValue], _: Environment): - if not args: - raise RuntimeError("Missing number value") - if not isinstance(args[0], NumberValue): - raise RuntimeError("Input type must be a number") - return NumberValue(args[0].value ** 0.5) - - -def _random(_: list[RuntimeValue], __: Environment): - return NumberValue(random.random()) - - -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): @@ -242,40 +229,97 @@ def _input(args: list[RuntimeValue], env: Environment) -> RuntimeValue: return StringValue(result) -def _getRequest(args: list[RuntimeValue], _: Environment) -> RuntimeValue: +def _len(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if isinstance(args[0], StringValue): + return NumberValue(len(args[0].value)) + + if isinstance(args[0], ArrayValue): + return NumberValue(len(args[0].elements)) + + if isinstance(args[0], ObjectValue): + return NumberValue(len(args[0].properties)) + + raise RuntimeError(f"Cannot get length of {args[0]}") + + +def _exit(args: list[RuntimeValue], env: Environment) -> RuntimeValue: + if env.disable_file_io: + raise RuntimeError("Exit function is disabled") + if args and isinstance(args[0], NumberValue): + sys.exit(int(args[0].value)) + sys.exit(0) + + +def _str(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) == 0: + return StringValue("") + return StringValue(get_value(args[0])) + + +def _int(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) == 0: + return NumberValue(0) + if isinstance(args[0], (StringValue, NumberValue)): + return NumberValue(int(args[0].value)) + raise RuntimeError(f"Cannot convert {args[0]} to int") + + +def _bool(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) == 0: + return BooleanValue(False) + if isinstance(args[0], (StringValue, NumberValue, BooleanValue)): + return BooleanValue(bool(args[0].value)) + if isinstance(args[0], ArrayValue): + return BooleanValue(bool(args[0].elements)) + if isinstance(args[0], ObjectValue): + return BooleanValue(bool(args[0].properties)) + raise RuntimeError(f"Cannot convert {args[0]} to bool") + + +def _array(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) == 1: + if isinstance(args[0], StringValue): + return ArrayValue([StringValue(char) for char in args[0].value]) + if isinstance(args[0], ObjectValue): + return ArrayValue(list(args[0].properties.values())) + return ArrayValue(args) + + +def _type(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + return StringValue(type(args[0]).__name__) + + +# TIME FUNCTIONS + + +def _time(_: list[RuntimeValue], __: Environment) -> RuntimeValue: + return NumberValue(time.time()) + + +def _sleep(args: list[RuntimeValue], _: Environment) -> RuntimeValue: if not args: - raise RuntimeError("Missing URL argument") - if not isinstance(args[0], StringValue): - raise RuntimeError("URL must be a string") - try: - response = requests.get(args[0].value, timeout=10) - return ObjectValue( - { - "data": StringValue(response.content.decode("utf-8")), - "status": NumberValue(response.status_code), - } - ) - except Exception as e: - raise RuntimeError(f"Error fetching URL '{args[0].value}'") from e + raise RuntimeError("Missing time argument") + if not isinstance(args[0], NumberValue): + raise RuntimeError("Time must be a number") + time.sleep(args[0].value) + return NullValue() -def _postRequest(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if len(args) < 2: - raise RuntimeError("Missing URL or data argument") - if not isinstance(args[0], StringValue): - raise RuntimeError("URL must be a string") - if not isinstance(args[1], StringValue): - raise RuntimeError("Data must be a string") - try: - response = requests.post(args[0].value, data=args[1].value, timeout=10) - return ObjectValue( - { - "data": StringValue(response.content.decode("utf-8")), - "status": NumberValue(response.status_code), - } - ) - except Exception as e: - raise RuntimeError(f"Error fetching URL '{args[0].value}'") from e +def _formatTime(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing time argument") + if not isinstance(args[0], NumberValue): + raise RuntimeError("Time must be a number") + return StringValue( + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(args[0].value)) + ) + + +def _getTimezoneOffset(_: list[RuntimeValue], __: Environment) -> RuntimeValue: + return NumberValue(time.timezone) + + +# FILE FUNCTIONS def _readFile(args: list[RuntimeValue], _: Environment) -> RuntimeValue: @@ -320,76 +364,194 @@ def _appendFile(args: list[RuntimeValue], _: Environment) -> RuntimeValue: return NullValue() -def _round(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if len(args) == 0: - return NumberValue(0) - if len(args) == 1: - if isinstance(args[0], NumberValue): - return NumberValue(round(args[0].value)) - elif len(args) == 2: - if isinstance(args[0], NumberValue) and isinstance(args[1], NumberValue): - return NumberValue(round(args[0].value, int(args[1].value))) - raise RuntimeError(f"Cannot round {args[0]}") +def _fileExists(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing filename argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Filename must be a string") + return BooleanValue(Path(args[0].value).exists()) -def _len(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if isinstance(args[0], StringValue): - return NumberValue(len(args[0].value)) +def _deleteFile(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing filename argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Filename must be a string") + try: + Path(args[0].value).unlink() + except FileNotFoundError as e: + raise RuntimeError(f"File '{args[0].value}' not found") from e + return NullValue() - if isinstance(args[0], ArrayValue): - return NumberValue(len(args[0].elements)) - if isinstance(args[0], ObjectValue): - return NumberValue(len(args[0].properties)) +def _copyFile(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing source or destination argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Source must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Destination must be a string") + try: + Path(args[0].value).write_bytes(Path(args[1].value).read_bytes()) + except FileNotFoundError as e: + raise RuntimeError(f"File '{args[0].value}' not found") from e + return NullValue() - raise RuntimeError(f"Cannot get length of {args[0]}") +def _moveFile(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing source or destination argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Source must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Destination must be a string") + try: + Path(args[0].value).replace(Path(args[1].value)) + except FileNotFoundError as e: + raise RuntimeError(f"File '{args[0].value}' not found") from e + return NullValue() -def _exit(args: list[RuntimeValue], env: Environment) -> RuntimeValue: - if env.disable_file_io: - raise RuntimeError("Exit function is disabled") - if args and isinstance(args[0], NumberValue): - sys.exit(int(args[0].value)) - sys.exit(0) +def _listFiles(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing directory argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Directory must be a string") + try: + return ArrayValue( + [StringValue(str(file)) for file in Path(args[0].value).iterdir()] + ) + except FileNotFoundError as e: + raise RuntimeError(f"Directory '{args[0].value}' not found") from e -def _str(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if len(args) == 0: - return StringValue("") - return StringValue(get_value(args[0])) +def _fileSize(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing filename argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Filename must be a string") + try: + return NumberValue(Path(args[0].value).stat().st_size) + except FileNotFoundError as e: + raise RuntimeError(f"File '{args[0].value}' not found") from e -def _int(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if len(args) == 0: - return NumberValue(0) - if isinstance(args[0], (StringValue, NumberValue)): - return NumberValue(int(args[0].value)) - raise RuntimeError(f"Cannot convert {args[0]} to int") +# HTTP FUNCTIONS -def _bool(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if len(args) == 0: - return BooleanValue(False) - if isinstance(args[0], (StringValue, NumberValue, BooleanValue)): - return BooleanValue(bool(args[0].value)) - if isinstance(args[0], ArrayValue): - return BooleanValue(bool(args[0].elements)) - if isinstance(args[0], ObjectValue): - return BooleanValue(bool(args[0].properties)) - raise RuntimeError(f"Cannot convert {args[0]} to bool") +def _getRequest(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing URL argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("URL must be a string") + try: + response = requests.get(args[0].value, timeout=10) + return ObjectValue( + { + "data": StringValue(response.content.decode("utf-8")), + "status": NumberValue(response.status_code), + } + ) + except Exception as e: + raise RuntimeError(f"Error fetching URL '{args[0].value}'") from e -def _array(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - if len(args) == 1: - if isinstance(args[0], StringValue): - return ArrayValue([StringValue(char) for char in args[0].value]) - if isinstance(args[0], ObjectValue): - return ArrayValue(list(args[0].properties.values())) - return ArrayValue(args) +def _postRequest(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing URL or data argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("URL must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Data must be a string") + try: + response = requests.post(args[0].value, data=args[1].value, timeout=10) + return ObjectValue( + { + "data": StringValue(response.content.decode("utf-8")), + "status": NumberValue(response.status_code), + } + ) + except Exception as e: + raise RuntimeError(f"Error fetching URL '{args[0].value}'") from e -def _type(args: list[RuntimeValue], _: Environment) -> RuntimeValue: - return StringValue(type(args[0]).__name__) + +def _putRequest(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing URL or data argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("URL must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Data must be a string") + try: + response = requests.put(args[0].value, data=args[1].value, timeout=10) + return ObjectValue( + { + "data": StringValue(response.content.decode("utf-8")), + "status": NumberValue(response.status_code), + } + ) + except Exception as e: + raise RuntimeError(f"Error fetching URL '{args[0].value}'") from e + + +def _deleteRequest(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing URL argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("URL must be a string") + try: + response = requests.delete(args[0].value, timeout=10) + return ObjectValue( + { + "data": StringValue(response.content.decode("utf-8")), + "status": NumberValue(response.status_code), + } + ) + except Exception as e: + raise RuntimeError(f"Error fetching URL '{args[0].value}'") from e + + +def _urlEncode(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing data argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Data must be a string") + return StringValue(quote(args[0].value)) + + +def _urlDecode(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing data argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Data must be a string") + return StringValue(unquote(args[0].value)) + + +# MATH FUNCTIONS + + +def _sqrt(args: list[RuntimeValue], _: Environment): + if not args: + raise RuntimeError("Missing number value") + if not isinstance(args[0], NumberValue): + raise RuntimeError("Input type must be a number") + return NumberValue(args[0].value ** 0.5) + + +def _random(_: list[RuntimeValue], __: Environment): + return NumberValue(random.random()) + + +def _round(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) == 0: + return NumberValue(0) + if len(args) == 1: + if isinstance(args[0], NumberValue): + return NumberValue(round(args[0].value)) + elif len(args) == 2: + if isinstance(args[0], NumberValue) and isinstance(args[1], NumberValue): + return NumberValue(round(args[0].value, int(args[1].value))) + raise RuntimeError(f"Cannot round {args[0]}") def _sum(args: list[RuntimeValue], _: Environment) -> RuntimeValue: @@ -419,12 +581,243 @@ def _max(args: list[RuntimeValue], _: Environment) -> RuntimeValue: raise RuntimeError(f"Cannot get max for {args[0]}") +def _abs(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) == 0: + return NumberValue(0) + if isinstance(args[0], NumberValue): + return NumberValue(abs(args[0].value)) + raise RuntimeError(f"Cannot get abs for {args[0]}") + + +def _pow(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing base or exponent argument") + if all(isinstance(i, NumberValue) for i in args): + return NumberValue(args[0].value ** args[1].value) # type: ignore + raise RuntimeError(f"Cannot get pow for {args}") + + +def _log(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing base or value argument") + if all(isinstance(i, NumberValue) for i in args): + return NumberValue(math.log(args[0].value, args[1].value)) # type: ignore + raise RuntimeError(f"Cannot get log for {args}") + + +def _log10(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.log10(args[0].value)) + raise RuntimeError(f"Cannot get log10 for {args[0]}") + + +def _sin(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.sin(args[0].value)) + raise RuntimeError(f"Cannot get sin for {args[0]}") + + +def _cos(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.sin(args[0].value)) + raise RuntimeError(f"Cannot get cos for {args[0]}") + + +def _tan(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.sin(args[0].value)) + raise RuntimeError(f"Cannot get tan for {args[0]}") + + +def _asin(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.asin(args[0].value)) + raise RuntimeError(f"Cannot get asin for {args[0]}") + + +def _acos(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.asin(args[0].value)) + raise RuntimeError(f"Cannot get acos for {args[0]}") + + +def _atan(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.asin(args[0].value)) + raise RuntimeError(f"Cannot get atan for {args[0]}") + + +def _floor(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.floor(args[0].value)) + raise RuntimeError(f"Cannot get floor for {args[0]}") + + +def _ceil(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.ceil(args[0].value)) + raise RuntimeError(f"Cannot get ceil for {args[0]}") + + +def _factorial(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing value argument") + if isinstance(args[0], NumberValue): + return NumberValue(math.factorial(int(args[0].value))) + raise RuntimeError(f"Cannot get factorial for {args[0]}") + + +# STRING FUNCTIONS + + +def _splitString(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing string or separator argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("String must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Separator must be a string") + return ArrayValue([StringValue(s) for s in args[0].value.split(args[1].value)]) + + +def _joinStrings(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing array or separator argument") + if not isinstance(args[0], ArrayValue): + raise RuntimeError("Array must be an array") + if not isinstance(args[1], StringValue): + raise RuntimeError("Separator must be a string") + if not all(isinstance(s, StringValue) for s in args[0].elements): + raise RuntimeError("Array must contain only strings") + return StringValue(args[1].value.join([s.value for s in args[0].elements])) # type: ignore + + +def _replaceString(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 3: + raise RuntimeError("Missing string, search or replace argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("String must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Search must be a string") + if not isinstance(args[2], StringValue): + raise RuntimeError("Replace must be a string") + return StringValue(args[0].value.replace(args[1].value, args[2].value)) + + +def _stringContains(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing string or search argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("String must be a string") + if not isinstance(args[1], StringValue): + raise RuntimeError("Search must be a string") + return BooleanValue(args[1].value in args[0].value) + + +# ARRAY FUNCTIONS + + +def _push(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing array or value argument") + if not isinstance(args[0], ArrayValue): + raise RuntimeError("First argument must be an array") + args[0].elements.append(args[1]) + return NullValue() + + +def _pop(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing array argument") + if not isinstance(args[0], ArrayValue): + raise RuntimeError("Argument must be an array") + return args[0].elements.pop() + + +def _shift(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing array argument") + if not isinstance(args[0], ArrayValue): + raise RuntimeError("Argument must be an array") + return args[0].elements.pop(0) + + +def _unshift(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if len(args) < 2: + raise RuntimeError("Missing array or value argument") + if not isinstance(args[0], ArrayValue): + raise RuntimeError("First argument must be an array") + args[0].elements.insert(0, args[1]) + return NullValue() + + +def _sort(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing array argument") + if not isinstance(args[0], ArrayValue): + raise RuntimeError("Argument must be an array") + if not all(isinstance(i, NumberValue) for i in args[0].elements): + raise RuntimeError("Array must contain only numbers") + args[0].elements.sort(key=lambda x: x.value) # type: ignore + return NullValue() + + +# OS FUNCTIONS + + +def _getCwd(_: list[RuntimeValue], __: Environment) -> RuntimeValue: + return StringValue(str(Path.cwd())) + + +def _changeDir(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + raise RuntimeError("Missing directory argument") + if not isinstance(args[0], StringValue): + raise RuntimeError("Directory must be a string") + os.chdir(args[0].value) + return NullValue() + + +def _getEnv(args: list[RuntimeValue], _: Environment) -> RuntimeValue: + if not args: + return ObjectValue( + {key: StringValue(value) for key, value in os.environ.items()} + ) + if not isinstance(args[0], StringValue): + raise RuntimeError("Variable name must be a string") + return StringValue(os.environ.get(args[0].value, "")) + + # Declare builtin modules BUILTINS["file"] = ObjectValue( { "read": NativeFunctionValue(_readFile), "write": NativeFunctionValue(_writeFile), "append": NativeFunctionValue(_appendFile), + "exists": NativeFunctionValue(_fileExists), + "delete": NativeFunctionValue(_deleteFile), + "copy": NativeFunctionValue(_copyFile), + "move": NativeFunctionValue(_moveFile), + "list": NativeFunctionValue(_listFiles), + "size": NativeFunctionValue(_fileSize), }, immutable=True, ) @@ -433,6 +826,10 @@ def _max(args: list[RuntimeValue], _: Environment) -> RuntimeValue: { "get": NativeFunctionValue(_getRequest), "post": NativeFunctionValue(_postRequest), + "put": NativeFunctionValue(_putRequest), + "delete": NativeFunctionValue(_deleteRequest), + "urlencode": NativeFunctionValue(_urlEncode), + "urldecode": NativeFunctionValue(_urlDecode), }, immutable=True, ) @@ -446,8 +843,60 @@ def _max(args: list[RuntimeValue], _: Environment) -> RuntimeValue: "pi": NumberValue(math.pi), "sqrt": NativeFunctionValue(_sqrt), "random": NativeFunctionValue(_random), + "abs": NativeFunctionValue(_abs), + "pow": NativeFunctionValue(_pow), + "log": NativeFunctionValue(_log), + "log10": NativeFunctionValue(_log10), + "sin": NativeFunctionValue(_sin), + "cos": NativeFunctionValue(_cos), + "tan": NativeFunctionValue(_tan), + "asin": NativeFunctionValue(_asin), + "acos": NativeFunctionValue(_acos), + "atan": NativeFunctionValue(_atan), + "floor": NativeFunctionValue(_floor), + "ceil": NativeFunctionValue(_ceil), + "e": NumberValue(math.e), + "factorial": NativeFunctionValue(_factorial), + }, + immutable=True, +) + +BUILTINS["time"] = ObjectValue( + { + "time": NativeFunctionValue(_time), + "sleep": NativeFunctionValue(_sleep), + "format": NativeFunctionValue(_formatTime), + "timezone_offset": NativeFunctionValue(_getTimezoneOffset), + }, + immutable=True, +) + +BUILTINS["string"] = ObjectValue( + { + "split": NativeFunctionValue(_splitString), + "join": NativeFunctionValue(_joinStrings), + "replace": NativeFunctionValue(_replaceString), + "contains": NativeFunctionValue(_stringContains), }, immutable=True, ) -BUILTINS["time"] = ObjectValue({"time": NativeFunctionValue(_time)}, immutable=True) +BUILTINS["array"] = ObjectValue( + { + "push": NativeFunctionValue(_push), + "pop": NativeFunctionValue(_pop), + "shift": NativeFunctionValue(_shift), + "unshift": NativeFunctionValue(_unshift), + "sort": NativeFunctionValue(_sort), + }, + immutable=True, +) + +BUILTINS["os"] = ObjectValue( + { + "cwd": NativeFunctionValue(_getCwd), + "chdir": NativeFunctionValue(_changeDir), + "env": NativeFunctionValue(_getEnv), + }, + immutable=True, +) diff --git a/eryx/runtime/interpreter.py b/eryx/runtime/interpreter.py index 94d2178..f054617 100644 --- a/eryx/runtime/interpreter.py +++ b/eryx/runtime/interpreter.py @@ -200,8 +200,8 @@ def eval_import_statement( module_name = import_statement.module if module_name in BUILTINS: - if module_name in ("file") and environment.disable_file_io: - raise RuntimeError("File I/O is disabled, unable to import 'file'.") + if module_name in ("file", "os") and environment.disable_file_io: + raise RuntimeError(f"File I/O is disabled, unable to import '{module_name}'.") module = BUILTINS.get(module_name) if module: @@ -471,7 +471,9 @@ def eval_member_expression( member: MemberExpression, environment: Environment ) -> RuntimeValue: """Evaluate a member expression.""" - object_value = evaluate(member.object, environment) # if ur reading this, you are silly :3 + object_value = evaluate( + member.object, environment + ) # if ur reading this, you are silly :3 if isinstance(object_value, (ObjectValue, ClassValue, EnumValue)): if member.computed: @@ -706,7 +708,8 @@ def eval_call_expression( if arg_names: if len(arg_names) != len(arguments): raise RuntimeError( - f"Expected {len(arg_names)} arguments, got {len(arguments)}." + f"Expected {len(arg_names)} arguments, got {len(arguments)}. " \ + f"({', '.join(arg_names)})" ) new_args = dict(zip(arg_names, arguments)) return ObjectValue(properties=new_args | methods)