diff --git a/languages/Inumaki/.gitignore b/languages/Inumaki/.gitignore new file mode 100644 index 0000000..3a8816c --- /dev/null +++ b/languages/Inumaki/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm-project.org/#use-with-ide +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/languages/Inumaki/.vscode/launch.json b/languages/Inumaki/.vscode/launch.json new file mode 100644 index 0000000..e0a93b9 --- /dev/null +++ b/languages/Inumaki/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File with Arguments", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": "${command:pickArgs}" + } + ] +} \ No newline at end of file diff --git a/languages/Inumaki/LICENSE b/languages/Inumaki/LICENSE new file mode 100644 index 0000000..87463cb --- /dev/null +++ b/languages/Inumaki/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 EdwardTheLegend + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/languages/Inumaki/README.md b/languages/Inumaki/README.md new file mode 100644 index 0000000..16f1559 --- /dev/null +++ b/languages/Inumaki/README.md @@ -0,0 +1,96 @@ +# Inumaki + +Programming language made for [Hack Club's langjam](https://langjam.hackclub.com) based off the speech of the Jujutsu Kaisen character [Toge Inumaki](https://jujutsu-kaisen.fandom.com/wiki/Toge_Inumaki). Though the original is of course Japanese, I'll be using the English translations of his vocabulary for the sake of my own sanity. + +# Vocabulary +These are the phrases which will be used in place of normal keywords in this language. Not all of them will be used and I'll try to use them in line with their original meaning to have some sense come out of all this. + +## Safe Words +| Word | Original use | Language use | +| --- | ---| --- | +| Salmon | Affirmation | True (Boolean) | +| Bonito Flakes | Negation | False (Boolean) | +| Kelp | Greeting | Comments | +| Mustard Leaf | Concern/Worry | Conditionals | +| Salmon Roe | "Well, well" | | +| Caviar | Expletive | Exceptions | +| Spicy Cod Roe | Motivational | | +| Tuna | Call attention | Print | +| Tuna Mayo | Do something | Function definitions | + +## Cursed Speech +| Word | Language use| +| --- | --- | +| Explode | Else | +| Twist | For loops | +| Crush | | +| Plummet | While loops | +| Stop | | +| Sleep | | +| Return | Return from function | +| Run | | +| Blast | | + +# Syntax Examples + +## Comments +``` +Kelp +``` + +## Variables +``` +Tuna Tuna +``` + +## Conditionals +``` +Mustard_Leaf Tuna Tuna { +
+} Explode { + +} +``` +There are no elifs so multiple cascading Explode..Mustard_Lead statements are required in that case. + +## For loops +``` +Twist Tuna Tuna Tuna Tuna Tuna { +
+} +``` +The increment statement usually takes a structure like `Tuna Tuna + 1` + +## While loops +``` +Plummet Tuna Tuna { +
+} +``` +The `Stop` cursed keyword may be used in future to break out of loops but until that is implemented, the condition must be made false to exit the loop. + +## Function definitions +``` +Tuna_Mayo Tuna Tuna { +
+ Return +} +``` +Parameters are optional in which case there will be `Tuna Tuna`. Return is also optional. + +# Standard Library +Tuna_Tuna for print, str and float all directly map to the python builtin functions. + +# Note on flexible keywords +In order to allow the programming language to be more chaotic and greater resembling Inumaki's speech keywords which aren't crucial to telling the parser what the current statement is do not have to be the same. For example, in a while loop + +``` +Plummet Tuna Tuna { +
+} +``` + +Once the parse sees `Plummet` it knows it's a while loop and I just need the `Tuna` to sandwich the condition as an easy way of knowing when to stop eating the condition. Therefore, in most of the places that `Tuna` has been used in the above examples, any other keyword can be used with no functional difference apart from cursedness. The exception is the assignment statement where `Tuna` is the primary indicator. + +# Cursedness +All token is this language are either cursed or not depending on wether they are one of the words in the cursed table above. Any use of these words wether necessarily in a code statement or even in a string literal will increase the interpreter's internal cursed counter. Over use of cursed speech will result in throat irritation and may cause sever damage to Inumaki's throat (an exception will be thrown). To prevent this, `Cough_Syrup` should be administered at appropriate intervals to alleviate symptoms. \ No newline at end of file diff --git a/languages/Inumaki/pdm.lock b/languages/Inumaki/pdm.lock new file mode 100644 index 0000000..b53d29c --- /dev/null +++ b/languages/Inumaki/pdm.lock @@ -0,0 +1,44 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:acd8d3f4bee34dd21872ed869a5d49f98407940284e2e09df4dc53c371d1483d" + +[[package]] +name = "argparse" +version = "1.4.0" +summary = "Python command-line parsing library" +groups = ["default"] +files = [ + {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"}, + {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"}, +] + +[[package]] +name = "ruff" +version = "0.4.8" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"}, + {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"}, + {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"}, + {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"}, + {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"}, + {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, +] diff --git a/languages/Inumaki/pyproject.toml b/languages/Inumaki/pyproject.toml new file mode 100644 index 0000000..d1e6710 --- /dev/null +++ b/languages/Inumaki/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "Inumaki" +version = "0.1.0" +description = "Default template for PDM package" +authors = [ + {name = "EdwardTheLegend", email = "40855056+EdwardTheLegend@users.noreply.github.com"}, +] +dependencies = [ + "argparse>=1.4.0", +] +requires-python = "==3.12.*" +readme = "README.md" +license = {text = "MIT"} + + +[tool.pdm] +distribution = false + +[tool.pdm.dev-dependencies] +dev = [ + "ruff>=0.4.8", +] + +[tool.ruff] +line-length = 120 \ No newline at end of file diff --git a/languages/Inumaki/samples/FizzBuzz.inu b/languages/Inumaki/samples/FizzBuzz.inu new file mode 100644 index 0000000..d12372a --- /dev/null +++ b/languages/Inumaki/samples/FizzBuzz.inu @@ -0,0 +1,16 @@ +Twist Tuna Tuna num Tuna 1 Tuna num < 101 Tuna Tuna num Tuna num + 1 Tuna { + Mustard_Leaf Tuna (num % 3 == 0) And (num % 5 == 0) Tuna { + Tuna_Tuna("FizzBuzz") + } Explode { + Mustard_Leaf Tuna num % 3 == 0 Tuna { + Tuna_Tuna("Fizz") + } Explode { + Mustard_Leaf Tuna num % 5 == 0 Tuna { + Tuna_Tuna("Buzz") + } Explode { + Tuna_Tuna(num) + Kelp Cough_Syrup <-- commented out code + } + } + } +} diff --git a/languages/Inumaki/samples/cursed_speech_overload.inu b/languages/Inumaki/samples/cursed_speech_overload.inu new file mode 100644 index 0000000..a7d422d --- /dev/null +++ b/languages/Inumaki/samples/cursed_speech_overload.inu @@ -0,0 +1,9 @@ +Tuna_Mayo Liver Tuna Tuna { + Twist Tuna Tuna Rice Tuna 0 Tuna Rice < 4 Tuna Tuna Rice Tuna Rice + 1 Tuna { + Return "Hello" + } +} + +Twist Tuna Tuna Egg Tuna 0 Tuna Egg < 100 Tuna Tuna Egg Tuna Egg + 1 Tuna { + Tuna_Tuna(Liver() + str(Egg)) +} diff --git a/languages/Inumaki/samples/hello.inu b/languages/Inumaki/samples/hello.inu new file mode 100644 index 0000000..481f673 --- /dev/null +++ b/languages/Inumaki/samples/hello.inu @@ -0,0 +1,39 @@ +Kelp <-- this indicates a comment +Kelp this file is an example of the Inumaki language features + +Kelp variables +Tuna Seaweed Tuna Salmon Kelp Salmon is true and Bonito_Flakes is false + +Kelp conditionals +Mustard_Leaf Tuna 1 == 1 And Seaweed Tuna { + Tuna_Tuna("This will print") +} Explode { + Tuna_Tuna("This will not print") +} + +Kelp for loops +Twist Tuna Tuna Rice Tuna 0 Tuna Rice < 10 Tuna Tuna Rice Tuna Rice + 1 Tuna { + Tuna_Tuna(Seaweed) +} + +Kelp while loops +Plummet Tuna Seaweed Tuna { + Tuna_Tuna("This will print") + Tuna Seaweed Tuna Bonito_Flakes +} + +Kelp functions +Tuna_Mayo Cod Tuna Eel Chicken Tuna { + Tuna_Tuna(Eel) + Tuna_Tuna(Chicken) + + Return Eel +} + +Cod("My", "Function") +Tuna Tomato Tuna Cod("Also", "Works") Kelp storing return value of a function +Tuna_Tuna(Tomato) + +Kelp builtin functions +Tuna_Tuna("Hello, World!") Kelp prints "Hello, World!" to the console +Tuna_Tuna(2*6+2-3) Kelp basic arithmetic operators (no order of operations sorry) \ No newline at end of file diff --git a/languages/Inumaki/src/inumaki/__init__.py b/languages/Inumaki/src/inumaki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/languages/Inumaki/src/inumaki/inu_ast.py b/languages/Inumaki/src/inumaki/inu_ast.py new file mode 100644 index 0000000..fea0597 --- /dev/null +++ b/languages/Inumaki/src/inumaki/inu_ast.py @@ -0,0 +1,112 @@ +class Set: + def __init__(self, name, value, cursed=0): + self.name = name + self.value = value + self.cursed = cursed + + __match_args__ = ("name", "value", "cursed") + + +class Var: + def __init__(self, name, cursed=0): + self.name = name + self.cursed = cursed + + __match_args__ = ("name", "cursed") + + +class Function: + def __init__(self, name, params, body, cursed=0): + self.name = name + self.params = params + self.body = body + self.cursed = cursed + + __match_args__ = ("name", "params", "body", "cursed") + + +class Return: + def __init__(self, value, cursed=0): + self.value = value + self.cursed = cursed + + __match_args__ = ("value", "cursed") + + +class Conditional: + def __init__(self, condition, body, else_body, cursed=0): + self.condition = condition + self.body = body + self.else_body = else_body + self.cursed = cursed + + __match_args__ = ("condition", "body", "else_body", "cursed") + + +class For: + def __init__(self, variable, condition, increment, body, cursed=0): + self.variable = variable + self.condition = condition + self.increment = increment + self.body = body + self.cursed = cursed + + __match_args__ = ("variable", "condition", "increment", "body", "cursed") + + +class While: + def __init__(self, condition, body, cursed=0): + self.condition = condition + self.body = body + self.cursed = cursed + + __match_args__ = ("condition", "body", "cursed") + + +class Call: + def __init__(self, name, args, cursed=0): + self.name = name + self.args = args + self.cursed = cursed + + __match_args__ = ("name", "args", "cursed") + + +class Get: + def __init__(self, obj, prop, cursed=0): + self.obj = obj + self.prop = prop + self.cursed = cursed + + __match_args__ = ("obj", "prop", "cursed") + + +class UnaryOp: + def __init__(self, op, right, cursed=0): + self.op = op + self.right = right + self.cursed = cursed + + __match_args__ = ("op", "right", "cursed") + + +class BinaryOp: + def __init__(self, left, op, right, cursed=0): + self.left = left + self.op = op + self.right = right + self.cursed = cursed + + __match_args__ = ("left", "op", "right", "cursed") + + +class Literal: + def __init__(self, value, cursed=0): + self.value = value + self.cursed = cursed + + __match_args__ = ("value", "cursed") + + +class CoughSyrup: + pass diff --git a/languages/Inumaki/src/inumaki/inu_interpreter.py b/languages/Inumaki/src/inumaki/inu_interpreter.py new file mode 100644 index 0000000..1e97d32 --- /dev/null +++ b/languages/Inumaki/src/inumaki/inu_interpreter.py @@ -0,0 +1,166 @@ +from inu_ast import ( + BinaryOp, + Call, + Conditional, + For, + Function, + Get, + Literal, + Return, + UnaryOp, + Var, + While, + Set, + CoughSyrup, +) + + +class Interpreter: + CursedThreshold = 100 + + class ReturnException(Exception): + def __init__(self, value): + self.value = value + + class CursedException(Exception): + def __init__(self): + super().__init__("Throat irritation from excessive cursed speech usage") + + def __init__(self, ast, scope, cursed): + self.ast = ast + self.scope = scope + self.cursed = cursed + + def run(self): + for node in self.ast: + self.execute(node) + # print(f"Current cursed count: {self.cursed}") # Debug statement + if self.cursed > Interpreter.CursedThreshold: + raise self.CursedException() + return self.scope + + def run_block(self, block, scope=None): + if scope is None: + scope = self.scope + interpreter = Interpreter(block, scope, cursed=self.cursed) + try: + self.scope = interpreter.run() + except self.ReturnException as e: + raise self.ReturnException(e.value) + finally: + self.cursed = interpreter.cursed + # print(f"Cursed count after block: {self.cursed}") # Debug statement + + def evaluate(self, node): + match node: + case Var(name, cursed): + self.cursed += cursed + # print(f"Evaluating Var: {name}, cursed: {self.cursed}") # Debug statement + return self.scope[name] + case UnaryOp(op, right): + if op == "Not": + return not self.evaluate(right) + elif op == "-": + return -self.evaluate(right) + case BinaryOp(left, op, right): + left = self.evaluate(left) + right = self.evaluate(right) + match op.value: + case "+": + return left + right + case "-": + return left - right + case "*": + return left * right + case "/": + return left / right + case "==": + return left == right + case "!=": + return left != right + case ">": + return left > right + case "<": + return left < right + case ">=": + return left >= right + case "<=": + return left <= right + case "%": + return left % right + case "And": + return left and right + case "Or": + return left or right + case _: + raise Exception(f"Unknown binary operator {op}") + case Literal(value, cursed): + self.cursed += cursed + # print(f"Evaluating Literal: {value}, cursed: {self.cursed}") # Debug statement + return value + case Call(name, args): + func = self.evaluate(name) + return func(*[self.evaluate(arg) for arg in args]) + case Get(obj, prop): + obj = self.evaluate(obj) + prop = self.evaluate(prop) + return obj[prop] + case _: + raise Exception(f"Unknown node {node}") + + def execute(self, node): + match node: + case Set(name, value, cursed): + self.cursed += cursed + # print(f"Executing Set: {name}, cursed: {self.cursed}") # Debug statement + self.scope[name.value] = self.evaluate(value) + case Function(name, params, body, cursed): + self.cursed += cursed + # print(f"Executing Function: {name}, cursed: {self.cursed}") # Debug statement + + def function(*args): + try: + self.run_block(body, {**self.scope, **{param.value: arg for param, arg in zip(params, args)}}) + except self.ReturnException as e: + return e.value + finally: + self.cursed += self.count_cursed_in_body(body) + # print(f"Cursed count after function call: {self.cursed}") # Debug statement + + self.scope[name.value] = function + case Return(value, cursed): + self.cursed += cursed + # print(f"Executing Return, cursed: {self.cursed}") # Debug statement + raise self.ReturnException(self.evaluate(value)) + case Conditional(condition, body, else_body, cursed): + self.cursed += cursed + # print(f"Executing Conditional, cursed: {self.cursed}") # Debug statement + if self.evaluate(condition): + self.run_block(body) + else: + self.run_block(else_body) + case For(variable, condition, increment, body, cursed): + self.cursed += cursed + # print(f"Executing For, cursed: {self.cursed}") # Debug statement + self.run_block([variable]) + while self.evaluate(condition): + self.run_block(body) + self.execute(increment) + case While(condition, body, cursed): + self.cursed += cursed + # print(f"Executing While, cursed: {self.cursed}") # Debug statement + while self.evaluate(condition): + self.run_block(body) + case CoughSyrup(): + self.cursed = 0 + # print(f"Executing CoughSyrup, cursed reset to: {self.cursed}") # Debug statement + case _: + self.evaluate(node) + + def count_cursed_in_body(self, body): + cursed_count = 0 + for node in body: + if hasattr(node, "cursed"): + cursed_count += node.cursed + # print(f"Counted cursed in body: {cursed_count}") # Debug statement + return cursed_count diff --git a/languages/Inumaki/src/inumaki/inu_lexer.py b/languages/Inumaki/src/inumaki/inu_lexer.py new file mode 100644 index 0000000..2eb2e30 --- /dev/null +++ b/languages/Inumaki/src/inumaki/inu_lexer.py @@ -0,0 +1,207 @@ +TOKENS = { + "LeftParen": "LeftParen", + "RightParen": "RightParen", + "LeftBrace": "LeftBrace", + "RightBrace": "RightBrace", + "LeftBracket": "LeftBracket", + "RightBracket": "RightBracket", + "Dot": "Dot", + "Comma": "Comma", + "Colon": "Colon", + "Keyword": "Keyword", + "Identifier": "Identifier", + "String": "String", + "Number": "Number", + "Boolean": "Boolean", + "Or": "Or", + "Not": "Not", + "And": "And", + "Equiv": "Equiv", + "NotEquiv": "NotEquiv", + "Gt": "Gt", + "Gte": "Gte", + "Lt": "Lt", + "Lte": "Lte", + "Plus": "Plus", + "Minus": "Minus", + "Asterisk": "Asterisk", + "Modulo": "Modulo", + "Slash": "Slash", + "Semicolon": "Semicolon", + "EOF": "EOF", + "Salmon": True, + "Bonito_Flakes": False, +} + +chars = { + "(": TOKENS["LeftParen"], + ")": TOKENS["RightParen"], + "{": TOKENS["LeftBrace"], + "}": TOKENS["RightBrace"], + "[": TOKENS["LeftBracket"], + "]": TOKENS["RightBracket"], + ".": TOKENS["Dot"], + ",": TOKENS["Comma"], + ":": TOKENS["Colon"], + "+": TOKENS["Plus"], + "-": TOKENS["Minus"], + "*": TOKENS["Asterisk"], + "/": TOKENS["Slash"], + "%": TOKENS["Modulo"], + "<": TOKENS["Lt"], + "<=": TOKENS["Lte"], + ">": TOKENS["Gt"], + ">=": TOKENS["Gte"], + "==": TOKENS["Equiv"], + ";": TOKENS["Semicolon"], +} + +KEYWORDS = [ + "Tuna", # variables + "Tuna_Mayo", # functions + "Return", + "Mustard_Leaf", # conditionals + "Explode", + "Twist", # for loop + "Plummet", # while loop + "Cough_Syrup", +] + +CURSED_WORDS = [ + "EXPLODE", + "TWIST", + "CRUSH", + "PLUMMET", + "STOP", + "SLEEP", + "RETURN", + "RUN", + "BLAST", +] + + +class Token: + def __init__(self, type, value, content, line, column): + self.type = type + self.value = value + self.content = content + self.line = line + self.column = column + + self.cursed = self.value.upper() in CURSED_WORDS + + def __str__(self): + return self.value + + +class Lexer: + def __init__(self, text): + self.text = text + self.tokens = [] + self.current = 0 + self.line = 1 + self.column = 0 + + def peek(self): + if self.current >= len(self.text): + return None + return self.text[self.current] + + def peekn(self, n): + if self.current + n >= len(self.text): + return None + return self.text[self.current + n] + + def advance(self): + if self.current >= len(self.text): + return None + self.current += 1 + self.column += 1 + return self.text[self.current - 1] + + def match(self, char): + if self.peek() == char: + return self.advance() + return False + + def match_word(self, word): + for i in range(len(word)): + if self.peekn(i) != word[i]: + return False + + for _ in range(len(word)): + self.advance() + return True + + def start_word(self, char): + word = char + column = self.column + while self.peek() and (self.peek().isalnum() or self.peek() == "_"): + word += self.advance() + + match word: + case "Or" | "Not" | "And": + self.tokens.append(Token(TOKENS[word], word, word, self.line, column)) + return + case "Kelp": + while self.peek() and self.peek() != "\n": + self.advance() + return + case "Salmon" | "Bonito_Flakes": + self.tokens.append(Token(TOKENS["Boolean"], word, word == "Salmon", self.line, column)) + return + + if word in KEYWORDS: + self.tokens.append(Token(TOKENS["Keyword"], word, word, self.line, column)) + else: + self.tokens.append(Token(TOKENS["Identifier"], word, word, self.line, column)) + + def scan_token(self): + char = self.advance() + + match char: + case "(" | ")" | "{" | "}" | "[" | "]" | "." | "," | ":" | "+" | "-" | "*" | "/" | "%" | ";": + self.tokens.append(Token(chars[char], char, char, self.line, self.column)) + case "<" | ">": + if self.match("="): + self.tokens.append(Token(chars[char + "="], char + "=", char + "=", self.line, self.column)) + else: + self.tokens.append(Token(chars[char], char, char, self.line, self.column)) + case "=": + if self.match("="): + self.tokens.append(Token(TOKENS["Equiv"], "==", "==", self.line, self.column)) + else: + raise Exception(f"Invalid character '=' at line {self.line}") + case "'" | '"': + string = "" + while self.peek() != char: + string += self.advance() + if not self.peek(): + raise Exception(f"Unterminated string at line {self.line}") + + self.advance() # closing quote + self.tokens.append(Token(TOKENS["String"], string, string, self.line, self.column)) + case " " | "\t" | "\r": + pass + case "\n": + self.line += 1 + self.column = 0 + case _: + if char.isdigit(): + number = char + while self.peek() and (self.peek().isdigit() or (self.peek() == "." and self.peekn(1).isdigit())): + number += self.advance() + + self.tokens.append(Token(TOKENS["Number"], number, float(number), self.line, self.column)) + elif char.isalpha() or char == "_": # Gather the whole word + self.start_word(char) + else: + raise Exception(f"Invalid character '{char}' at line {self.line}") + + def scan_tokens(self): + while self.peek(): + self.scan_token() + + self.tokens.append(Token(TOKENS["EOF"], "EOF", None, self.line, self.column)) + + return self.tokens diff --git a/languages/Inumaki/src/inumaki/inu_parser.py b/languages/Inumaki/src/inumaki/inu_parser.py new file mode 100644 index 0000000..07c3332 --- /dev/null +++ b/languages/Inumaki/src/inumaki/inu_parser.py @@ -0,0 +1,240 @@ +from inu_ast import ( + BinaryOp, + Call, + Conditional, + For, + Function, + Get, + Literal, + Return, + UnaryOp, + Var, + While, + Set, + CoughSyrup, +) +from inu_lexer import KEYWORDS, TOKENS, Token + + +class Parser: + def __init__(self, tokens: list[Token]): + self.tokens = tokens + self.pos = 0 + self.ast = [] + + def peek(self): + if self.pos >= len(self.tokens): + return None + return self.tokens[self.pos] + + def peekn(self, n): + if self.pos + n >= len(self.tokens): + return None + return self.tokens[self.pos + n] + + def eat(self, type): + if type in TOKENS.keys(): + if (peek_type := self.peek().type) == type: + self.pos += 1 + return self.tokens[self.pos - 1] + elif type in KEYWORDS: + if (peek_type := self.peek().value) == type: + self.pos += 1 + return self.tokens[self.pos - 1] + + raise Exception(f"Expected {type}, got {peek_type}") + + def eat_keyword(self): + if self.peek().type == TOKENS["Keyword"]: + self.pos += 1 + return self.tokens[self.pos - 1] + else: + raise Exception(f"Expected keyword, got {self.peek().type}") + + def term(self): + if self.peek().type == TOKENS["Identifier"]: + var = self.eat("Identifier") + name = Var(var.value, var.cursed) + while self.peek().type in [TOKENS["Dot"], TOKENS["LeftParen"]]: + if self.peek().type == TOKENS["Dot"]: + self.eat("Dot") + name = Get(name, self.eat("Identifier")) + else: + self.eat("LeftParen") + args = [] + while self.peek().type != TOKENS["RightParen"]: + args.append(self.expression()) + if self.peek().type == TOKENS["Comma"]: + self.eat("Comma") + self.eat("RightParen") + name = Call(name, args) + + return name + + elif self.peek().type in [TOKENS["Number"], TOKENS["Boolean"], TOKENS["String"]]: + literal = self.eat(self.peek().type) + return Literal(literal.content, literal.cursed) + elif self.peek().type == TOKENS["LeftParen"]: + self.eat("LeftParen") + expr = self.expression() + self.eat("RightParen") + return expr + elif self.peek().type == TOKENS["Minus"]: + self.eat("Minus") + return UnaryOp("-", self.term()) + elif self.peek().type == TOKENS["Not"]: + self.eat("Not") + return UnaryOp("!", self.term()) + else: + raise Exception(f"Unexpected token {self.peek().type} while processing term") + + def expression(self): + left = self.term() + + while self.peek().type in [ + TOKENS["Plus"], + TOKENS["Minus"], + TOKENS["Asterisk"], + TOKENS["Slash"], + TOKENS["Modulo"], + TOKENS["Equiv"], + TOKENS["NotEquiv"], + TOKENS["Lt"], + TOKENS["Gt"], + TOKENS["Lte"], + TOKENS["Gte"], + TOKENS["And"], + TOKENS["Or"], + ]: + op = self.eat(self.peek().type) + right = self.term() + left = BinaryOp(left, op, right) + + return left + + def parse(self): + while self.peek().type != TOKENS["EOF"]: + self.ast.append(self.parse_statement()) + return self.ast + + def parse_statement(self): + next = self.peek() + + if next.type == TOKENS["Keyword"]: + match next.value: + case "Tuna": + return self.assign_stmt() + case "Tuna_Mayo": + return self.function_stmt() + case "Return": + return self.return_stmt() + case "Mustard_Leaf": + return self.conditional_stmt() + case "Twist": + return self.for_stmt() + case "Plummet": + return self.while_stmt() + case "Cough_Syrup": + return self.cough_syrup() + case _: + raise Exception(f"Unexpected keyword {next.value}") + else: + return self.expression() + + def assign_stmt(self): + self.eat("Tuna") + name = self.eat("Identifier") + kw = self.eat_keyword() + value = self.expression() + + cursed = name.cursed + kw.cursed + + return Set(name, value, cursed) + + def function_stmt(self): + cursed = 0 + self.eat("Tuna_Mayo") + name = self.eat("Identifier") + cursed += name.cursed + cursed += self.eat_keyword().cursed + params = [] + while self.peek().type != TOKENS["Keyword"]: + params.append(self.eat("Identifier")) + cursed += params[-1].cursed + cursed += self.eat_keyword().cursed + + self.eat("LeftBrace") + body = [] + while self.peek().type != TOKENS["RightBrace"]: + body.append(self.parse_statement()) + self.eat("RightBrace") + + return Function(name, params, body, cursed) + + def return_stmt(self): + self.eat("Return") + value = self.expression() + + return Return(value, cursed=1) + + def conditional_stmt(self): + cursed = 0 + self.eat("Mustard_Leaf") + cursed += self.eat_keyword().cursed + condition = self.expression() + cursed += self.eat_keyword().cursed + self.eat("LeftBrace") + body = [] + while self.peek().type != TOKENS["RightBrace"]: + body.append(self.parse_statement()) + self.eat("RightBrace") + else_body = None + if self.peek().value == "Explode": + self.eat("Explode") + cursed += 1 + self.eat("LeftBrace") + else_body = [] + while self.peek().type != TOKENS["RightBrace"]: + else_body.append(self.parse_statement()) + self.eat("RightBrace") + + return Conditional(condition, body, else_body, cursed) + + def for_stmt(self): + self.eat("Twist") + cursed = 1 + cursed += self.eat_keyword().cursed + var = self.parse_statement() + if not isinstance(var, Set): + raise Exception("Expected variable declaration in for loop") + cursed += self.eat_keyword().cursed + condition = self.expression() + cursed += self.eat_keyword().cursed + increment = self.parse_statement() + cursed += self.eat_keyword().cursed + + self.eat("LeftBrace") + body = [] + while self.peek().type != TOKENS["RightBrace"]: + body.append(self.parse_statement()) + self.eat("RightBrace") + + return For(var, condition, increment, body, cursed) + + def while_stmt(self): + self.eat("Plummet") + cursed = 1 + cursed += self.eat_keyword().cursed + condition = self.expression() + cursed += self.eat_keyword().cursed + self.eat("LeftBrace") + body = [] + while self.peek().type != TOKENS["RightBrace"]: + body.append(self.parse_statement()) + self.eat("RightBrace") + + return While(condition, body, cursed) + + def cough_syrup(self): + self.eat("Cough_Syrup") + return CoughSyrup() diff --git a/languages/Inumaki/src/inumaki/inu_stdlib.py b/languages/Inumaki/src/inumaki/inu_stdlib.py new file mode 100644 index 0000000..5f31866 --- /dev/null +++ b/languages/Inumaki/src/inumaki/inu_stdlib.py @@ -0,0 +1 @@ +inu_stdlib = {"Tuna_Tuna": print, "str": str, "float": float} diff --git a/languages/Inumaki/src/inumaki/inumaki.py b/languages/Inumaki/src/inumaki/inumaki.py new file mode 100644 index 0000000..a35dbb6 --- /dev/null +++ b/languages/Inumaki/src/inumaki/inumaki.py @@ -0,0 +1,37 @@ +import argparse + +from inu_interpreter import Interpreter +from inu_lexer import Lexer +from inu_parser import Parser +from inu_stdlib import inu_stdlib + +parser = argparse.ArgumentParser(prog="inumaki", description="Inumkai programming language") +parser.add_argument("file", type=str, help="Inumaki source code file", nargs="?", default=None) + +args = parser.parse_args() + + +def run(text): + lexer = Lexer(text) + lexer.scan_tokens() + + parser = Parser(lexer.tokens) + parser.parse() + + interpreter = Interpreter(parser.ast, scope=inu_stdlib, cursed=0) + interpreter.run() + + +if args.file: + with open(args.file, "r") as file: + text = file.read() + + run(text) +else: + while True: + try: + text = input("inumaki> ") + except (EOFError, KeyboardInterrupt): + break + + run(text) diff --git a/languages/Inumaki/tests/__init__.py b/languages/Inumaki/tests/__init__.py new file mode 100644 index 0000000..e69de29