diff --git a/.gitignore b/.gitignore index b7faf40..02f7256 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,95 @@ coverage.xml *.py.cover .hypothesis/ .pytest_cache/ + +# Selenium compiler outputs +*.c +*.out +# Executables (no extension on macOS) +seleniumc +hello +factorial +float +bool +char +for + +# 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 +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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. +#P ipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__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/ cover/ # Translations diff --git a/README.md b/README.md index 0a8323d..510f4d9 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ Selenium is a small esoteric language with a lunar / poetic surface and a strict - semicolon-terminated statements - functions - variables and constants -- `if` / `else` -- `while` +- `eclipse` / `shadow` for if / else +- `tide` for while +- `orbit` for for - `return` - `whisper` for printing - explicit `cast(type, expr)` conversions @@ -30,6 +31,15 @@ eclipse (moon < tide) { whisper tide; }; +tide (moon < 10) { + whisper moon; + moon = moon + 1; +}; + +orbit (wax int i = 0; i < 5; i = i + 1) { + whisper i; +}; + whisper add(moon, tide); ``` diff --git a/examples/bool.sel b/examples/bool.sel new file mode 100644 index 0000000..9d9d4f9 --- /dev/null +++ b/examples/bool.sel @@ -0,0 +1,13 @@ +seal bool t = true; +seal bool f = false; +whisper t; +whisper f; + +wax bool and = t && f; +whisper and; + +wax bool or = t || f; +whisper or; + +wax bool not = !f; +whisper not; \ No newline at end of file diff --git a/examples/char.sel b/examples/char.sel new file mode 100644 index 0000000..04a0093 --- /dev/null +++ b/examples/char.sel @@ -0,0 +1,5 @@ +seal char c = 'A'; +whisper c; + +wax int i = cast(int, c); +whisper i; \ No newline at end of file diff --git a/examples/float.sel b/examples/float.sel new file mode 100644 index 0000000..1483e98 --- /dev/null +++ b/examples/float.sel @@ -0,0 +1,5 @@ +wax float pi = 3.14159; +whisper pi; + +wax float y = cast(float, 5); +whisper y; \ No newline at end of file diff --git a/examples/for.sel b/examples/for.sel new file mode 100644 index 0000000..38da95b --- /dev/null +++ b/examples/for.sel @@ -0,0 +1,3 @@ +orbit (wax int i = 0; i < 5; i = i + 1) { + whisper i; +}; \ No newline at end of file diff --git a/selenium/ast.py b/selenium/ast.py index 3388cb4..5fdc7c9 100644 --- a/selenium/ast.py +++ b/selenium/ast.py @@ -60,6 +60,14 @@ class WhileStmt: body: Block +@dataclass(slots=True) +class ForStmt: + init: Optional["Stmt"] + condition: "Expr" + increment: Optional["Stmt"] + body: Block + + @dataclass(slots=True) class ReturnStmt: value: Optional["Expr"] @@ -112,5 +120,5 @@ class Cast: Expr = Union[Literal, VarRef, Unary, Binary, Call, Cast] -Stmt = Union[VarDecl, Assign, IfStmt, WhileStmt, ReturnStmt, PrintStmt, ExprStmt, Block] -TopLevel = Union[VarDecl, FunctionDecl, Assign, IfStmt, WhileStmt, ReturnStmt, PrintStmt, ExprStmt, Block] +Stmt = Union[VarDecl, Assign, IfStmt, WhileStmt, ForStmt, ReturnStmt, PrintStmt, ExprStmt, Block] +TopLevel = Union[VarDecl, FunctionDecl, Assign, IfStmt, WhileStmt, ForStmt, ReturnStmt, PrintStmt, ExprStmt, Block] diff --git a/selenium/codegen_c.py b/selenium/codegen_c.py index d4f9246..405caca 100644 --- a/selenium/codegen_c.py +++ b/selenium/codegen_c.py @@ -11,6 +11,7 @@ from .ast import ( Cast, Expr, ExprStmt, + ForStmt, FunctionDecl, IfStmt, Literal, @@ -48,11 +49,15 @@ class CCodeGenerator: self.indent = 0 self._emit_prelude() functions = [item for item in self.program.items if isinstance(item, FunctionDecl)] - others = [item for item in self.program.items if not isinstance(item, FunctionDecl)] + globals_ = [item for item in self.program.items if isinstance(item, VarDecl)] + statements = [item for item in self.program.items if not isinstance(item, (FunctionDecl, VarDecl))] + for var in globals_: + self._emit_global_var(var) + self._writeline("") for fn in functions: self._emit_function(fn) self._writeline("") - self._emit_main(others) + self._emit_main(statements) return "\n".join(self.lines) + "\n" def _emit_prelude(self) -> None: @@ -98,6 +103,8 @@ class CCodeGenerator: self._emit_if(item) elif isinstance(item, WhileStmt): self._emit_while(item) + elif isinstance(item, ForStmt): + self._emit_for(item) elif isinstance(item, ReturnStmt): if item.value is None: self._writeline("return;") @@ -145,10 +152,35 @@ class CCodeGenerator: self._emit_statements(stmt.body.statements) self.indent -= 1 self._writeline("}") + def _emit_for(self, stmt: ForStmt) -> None: + init_str = "" + if stmt.init is not None: + if isinstance(stmt.init, VarDecl): + init_str = f"{self._c_type(stmt.init.type.name)} {stmt.init.name} = {self._expr(stmt.init.value)}" + elif isinstance(stmt.init, Assign): + init_str = f"{stmt.init.name} = {self._expr(stmt.init.value)}" + elif isinstance(stmt.init, ExprStmt): + init_str = self._expr(stmt.init.expr) + cond_str = self._expr(stmt.condition) if stmt.condition else "" + inc_str = "" + if stmt.increment is not None: + if isinstance(stmt.increment, Assign): + inc_str = f"{stmt.increment.name} = {self._expr(stmt.increment.value)}" + elif isinstance(stmt.increment, ExprStmt): + inc_str = self._expr(stmt.increment.expr) + self._writeline(f"for ({init_str}; {cond_str}; {inc_str}) {{") + self.indent += 1 + self._emit_statements(stmt.body.statements) + self.indent -= 1 + self._writeline("}") + def _emit_global_var(self, decl: VarDecl) -> None: + ctype = self._c_type(decl.type.name) + qualifier = "const " if not decl.mutable and not ctype.startswith("const ") else "" + self._writeline(f"{qualifier}{ctype} {decl.name} = {self._expr(decl.value)};") def _emit_vardecl(self, decl: VarDecl) -> None: ctype = self._c_type(decl.type.name) - qualifier = "const " if not decl.mutable else "" + qualifier = "const " if not decl.mutable and not ctype.startswith("const ") else "" self._writeline(f"{qualifier}{ctype} {decl.name} = {self._expr(decl.value)};") def _emit_print(self, expr: Expr) -> None: diff --git a/selenium/lexer.py b/selenium/lexer.py index 841411e..00e0ccc 100644 --- a/selenium/lexer.py +++ b/selenium/lexer.py @@ -23,6 +23,7 @@ KEYWORDS = { "eclipse": "ECLIPSE", "shadow": "SHADOW", "tide": "TIDE", + "orbit": "ORBIT", "whisper": "WHISPER", "return": "RETURN", "cast": "CAST", diff --git a/selenium/parser.py b/selenium/parser.py index e21c3f3..f6aa688 100644 --- a/selenium/parser.py +++ b/selenium/parser.py @@ -10,6 +10,7 @@ from .ast import ( Cast, Expr, ExprStmt, + ForStmt, FunctionDecl, IfStmt, Literal, @@ -77,6 +78,15 @@ class Parser: self._consume(";", "Expected ';' after function body") return FunctionDecl(name, params, return_type, body) + def _for_init(self) -> Optional[Stmt]: + if self._match("WAX"): + return self._var_decl_no_semi(mutable=True) + if self._match("SEAL"): + return self._var_decl_no_semi(mutable=False) + if not self._check(";"): + return self._statement() + return None + def _var_decl(self, mutable: bool) -> VarDecl: var_type = self._type_ref() name = self._consume("IDENT", "Expected variable name").value @@ -85,6 +95,13 @@ class Parser: self._consume(";", "Expected ';' after declaration") return VarDecl(mutable, var_type, name, value) + def _var_decl_no_semi(self, mutable: bool) -> VarDecl: + var_type = self._type_ref() + name = self._consume("IDENT", "Expected variable name").value + self._consume("=", "Expected '=' in declaration") + value = self._expression() + return VarDecl(mutable, var_type, name, value) + def _statement(self) -> Stmt: if self._match("ECLIPSE"): self._consume("(", "Expected '(' after eclipse") @@ -105,6 +122,26 @@ class Parser: self._consume(";", "Expected ';' after while statement") return WhileStmt(cond, body) + if self._match("ORBIT"): + self._consume("(", "Expected '(' after orbit") + init = self._for_init() + self._consume(";", "Expected ';' after init") + cond = self._expression() + self._consume(";", "Expected ';' after condition") + increment = None + if not self._check(")"): + if self._check("IDENT") and self._check_next("="): + name = self._advance().value + self._advance() # = + value = self._expression() + increment = Assign(name, value) + else: + increment = ExprStmt(self._expression()) + self._consume(")", "Expected ')' after increment") + body = self._block() + self._consume(";", "Expected ';' after for statement") + return ForStmt(init, cond, increment, body) + if self._match("RETURN"): if self._check(";"): self._advance() diff --git a/selenium/sema.py b/selenium/sema.py index 9f8231a..876babb 100644 --- a/selenium/sema.py +++ b/selenium/sema.py @@ -11,6 +11,7 @@ from .ast import ( Cast, Expr, ExprStmt, + ForStmt, FunctionDecl, IfStmt, Literal, @@ -168,6 +169,17 @@ class SemanticAnalyzer: self._analyze_block(stmt.body, scope, in_function) return + if isinstance(stmt, ForStmt): + for_scope = Scope(scope) + if stmt.init is not None: + self._analyze_stmt(stmt.init, for_scope, in_function) + cond_type = self._infer_expr(stmt.condition, for_scope) + self._require_type(cond_type, "bool", "For condition must be bool") + if stmt.increment is not None: + self._analyze_stmt(stmt.increment, for_scope, in_function) + self._analyze_block(stmt.body, for_scope, in_function) + return + if isinstance(stmt, ReturnStmt): if not in_function: raise SemanticError("Return is only allowed inside a function")