From 399a75601d59caeea1ece1e777afcdecd33f3b65 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Mon, 6 Apr 2026 19:12:59 +0100 Subject: [PATCH] init: add initial version files --- README.md | 56 ++++++++ examples/factorial.sel | 9 ++ examples/hello.sel | 9 ++ pyproject.toml | 13 ++ selenium/__init__.py | 9 ++ selenium/ast.py | 116 ++++++++++++++++ selenium/codegen_c.py | 227 +++++++++++++++++++++++++++++++ selenium/lexer.py | 208 +++++++++++++++++++++++++++++ selenium/main.py | 45 +++++++ selenium/parser.py | 281 +++++++++++++++++++++++++++++++++++++++ selenium/runtime.c | 8 ++ selenium/sema.py | 295 +++++++++++++++++++++++++++++++++++++++++ 12 files changed, 1276 insertions(+) create mode 100644 README.md create mode 100644 examples/factorial.sel create mode 100644 examples/hello.sel create mode 100644 pyproject.toml create mode 100644 selenium/__init__.py create mode 100644 selenium/ast.py create mode 100644 selenium/codegen_c.py create mode 100644 selenium/lexer.py create mode 100644 selenium/main.py create mode 100644 selenium/parser.py create mode 100644 selenium/runtime.c create mode 100644 selenium/sema.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a8323d --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Selenium compiler + +Selenium is a small esoteric language with a lunar / poetic surface and a strict, C-like core. + +## Features + +- strongly typed +- semicolon-terminated statements +- functions +- variables and constants +- `if` / `else` +- `while` +- `return` +- `whisper` for printing +- explicit `cast(type, expr)` conversions + +## Syntax sketch + +```selenium +wax int moon = 3; +seal int tide = 8; + +ritual add(int a, int b) -> int { + return a + b; +}; + +eclipse (moon < tide) { + whisper moon; +} shadow { + whisper tide; +}; + +whisper add(moon, tide); +``` + +## Build a C file + +```bash +python -m selenium.main examples/hello.sel -o out.c +gcc out.c -o out +./out +``` + +Or install it as a script: + +```bash +pip install -e . +seleniumc examples/hello.sel -o out.c +``` + +## Notes + +- The compiler is intentionally strict. +- No implicit type coercion. +- Top-level `wax`, `seal`, and statements are emitted into `main`. +- Function definitions become normal C functions. diff --git a/examples/factorial.sel b/examples/factorial.sel new file mode 100644 index 0000000..990a63b --- /dev/null +++ b/examples/factorial.sel @@ -0,0 +1,9 @@ +ritual fact(int n) -> int { + eclipse (n <= 1) { + return 1; + } shadow { + return n * fact(n - 1); + }; +}; + +whisper fact(5); diff --git a/examples/hello.sel b/examples/hello.sel new file mode 100644 index 0000000..daede0c --- /dev/null +++ b/examples/hello.sel @@ -0,0 +1,9 @@ +seal string title = "Selenium"; + +ritual greet(string name) -> void { + whisper title; + whisper name; +}; + +wax int moon = 3; +whisper moon; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..233cbe2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "selenium-compiler" +version = "0.1.0" +description = "An esoteric strongly typed language that compiles Selenium to C." +requires-python = ">=3.10" +authors = [{name = "OpenAI"}] + +[project.scripts] +seleniumc = "selenium.main:main" diff --git a/selenium/__init__.py b/selenium/__init__.py new file mode 100644 index 0000000..ba1e038 --- /dev/null +++ b/selenium/__init__.py @@ -0,0 +1,9 @@ +"""Selenium compiler package.""" + +__all__ = [ + "ast", + "lexer", + "parser", + "sema", + "codegen_c", +] diff --git a/selenium/ast.py b/selenium/ast.py new file mode 100644 index 0000000..3388cb4 --- /dev/null +++ b/selenium/ast.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional, Union + + +@dataclass(slots=True) +class TypeRef: + name: str + + +@dataclass(slots=True) +class Param: + type: TypeRef + name: str + + +@dataclass(slots=True) +class Program: + items: List[object] + + +@dataclass(slots=True) +class Block: + statements: List[object] + + +@dataclass(slots=True) +class VarDecl: + mutable: bool + type: TypeRef + name: str + value: "Expr" + + +@dataclass(slots=True) +class FunctionDecl: + name: str + params: List[Param] + return_type: TypeRef + body: Block + + +@dataclass(slots=True) +class Assign: + name: str + value: "Expr" + + +@dataclass(slots=True) +class IfStmt: + condition: "Expr" + then_block: Block + else_block: Optional[Block] + + +@dataclass(slots=True) +class WhileStmt: + condition: "Expr" + body: Block + + +@dataclass(slots=True) +class ReturnStmt: + value: Optional["Expr"] + + +@dataclass(slots=True) +class PrintStmt: + value: "Expr" + + +@dataclass(slots=True) +class ExprStmt: + expr: "Expr" + + +@dataclass(slots=True) +class Literal: + value: object + kind: str + + +@dataclass(slots=True) +class VarRef: + name: str + + +@dataclass(slots=True) +class Unary: + op: str + expr: "Expr" + + +@dataclass(slots=True) +class Binary: + left: "Expr" + op: str + right: "Expr" + + +@dataclass(slots=True) +class Call: + callee: str + args: List["Expr"] + + +@dataclass(slots=True) +class Cast: + target_type: TypeRef + expr: "Expr" + + +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] diff --git a/selenium/codegen_c.py b/selenium/codegen_c.py new file mode 100644 index 0000000..d4f9246 --- /dev/null +++ b/selenium/codegen_c.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List + +from .ast import ( + Assign, + Binary, + Block, + Call, + Cast, + Expr, + ExprStmt, + FunctionDecl, + IfStmt, + Literal, + PrintStmt, + Program, + ReturnStmt, + Stmt, + TopLevel, + Unary, + VarDecl, + VarRef, + WhileStmt, +) +from .sema import TypeInfo + + +class CodegenError(Exception): + pass + + +@dataclass(slots=True) +class CodegenContext: + expr_types: Dict[int, TypeInfo] + + +class CCodeGenerator: + def __init__(self, program: Program, context: CodegenContext): + self.program = program + self.ctx = context + self.lines: List[str] = [] + self.indent = 0 + + def generate(self) -> str: + self.lines = [] + 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)] + for fn in functions: + self._emit_function(fn) + self._writeline("") + self._emit_main(others) + return "\n".join(self.lines) + "\n" + + def _emit_prelude(self) -> None: + self._writeline("#include ") + self._writeline("#include ") + self._writeline("#include ") + self._writeline("") + self._writeline("static void selenium_print_int(int value) { printf(\"%d\\n\", value); }") + self._writeline("static void selenium_print_float(double value) { printf(\"%g\\n\", value); }") + self._writeline( + "static void selenium_print_bool(_Bool value) { printf(\"%s\\n\", value ? \"true\" : \"false\"); }" + ) + self._writeline("static void selenium_print_char(char value) { printf(\"%c\\n\", value); }") + self._writeline("static void selenium_print_string(const char *value) { printf(\"%s\\n\", value); }") + self._writeline("") + + def _emit_function(self, fn: FunctionDecl) -> None: + ret = self._c_type(fn.return_type.name) + params = ", ".join(f"{self._c_type(p.type.name)} {p.name}" for p in fn.params) + if not params: + params = "void" + self._writeline(f"static {ret} {fn.name}({params}) {{") + self.indent += 1 + self._emit_statements(fn.body.statements) + self.indent -= 1 + self._writeline("}") + + def _emit_main(self, items: List[TopLevel]) -> None: + self._writeline("int main(void) {") + self.indent += 1 + for item in items: + self._emit_item(item) + self._writeline("return 0;") + self.indent -= 1 + self._writeline("}") + + def _emit_item(self, item: object) -> None: + if isinstance(item, VarDecl): + self._emit_vardecl(item) + elif isinstance(item, Assign): + self._writeline(f"{item.name} = {self._expr(item.value)};") + elif isinstance(item, IfStmt): + self._emit_if(item) + elif isinstance(item, WhileStmt): + self._emit_while(item) + elif isinstance(item, ReturnStmt): + if item.value is None: + self._writeline("return;") + else: + self._writeline(f"return {self._expr(item.value)};") + elif isinstance(item, PrintStmt): + self._emit_print(item.value) + elif isinstance(item, ExprStmt): + self._writeline(f"{self._expr(item.expr)};") + elif isinstance(item, Block): + self._emit_block_stmt(item) + elif isinstance(item, FunctionDecl): + return + else: + raise CodegenError(f"Unhandled item: {type(item).__name__}") + + def _emit_statements(self, statements: List[Stmt]) -> None: + for stmt in statements: + self._emit_item(stmt) + + def _emit_block_stmt(self, block: Block) -> None: + self._writeline("{") + self.indent += 1 + self._emit_statements(block.statements) + self.indent -= 1 + self._writeline("}") + + def _emit_if(self, stmt: IfStmt) -> None: + self._writeline(f"if ({self._expr(stmt.condition)}) {{") + self.indent += 1 + self._emit_statements(stmt.then_block.statements) + self.indent -= 1 + if stmt.else_block is None: + self._writeline("}") + else: + self._writeline("} else {") + self.indent += 1 + self._emit_statements(stmt.else_block.statements) + self.indent -= 1 + self._writeline("}") + + def _emit_while(self, stmt: WhileStmt) -> None: + self._writeline(f"while ({self._expr(stmt.condition)}) {{") + self.indent += 1 + self._emit_statements(stmt.body.statements) + self.indent -= 1 + self._writeline("}") + + def _emit_vardecl(self, decl: VarDecl) -> None: + ctype = self._c_type(decl.type.name) + qualifier = "const " if not decl.mutable else "" + self._writeline(f"{qualifier}{ctype} {decl.name} = {self._expr(decl.value)};") + + def _emit_print(self, expr: Expr) -> None: + t = self._type_of(expr) + text = self._expr(expr) + if t.name == "int": + self._writeline(f"selenium_print_int({text});") + elif t.name == "float": + self._writeline(f"selenium_print_float({text});") + elif t.name == "bool": + self._writeline(f"selenium_print_bool({text});") + elif t.name == "char": + self._writeline(f"selenium_print_char({text});") + elif t.name == "string": + self._writeline(f"selenium_print_string({text});") + else: + raise CodegenError(f"Cannot print type {t.name}") + + def _expr(self, expr: Expr) -> str: + if isinstance(expr, Literal): + if expr.kind == "string": + return self._escape_string(expr.value) + if expr.kind == "char": + return self._escape_char(expr.value) + if expr.kind == "bool": + return "true" if expr.value else "false" + return str(expr.value) + if isinstance(expr, VarRef): + return expr.name + if isinstance(expr, Cast): + target = self._c_type(expr.target_type.name) + return f"(({target})({self._expr(expr.expr)}))" + if isinstance(expr, Call): + args = ", ".join(self._expr(arg) for arg in expr.args) + return f"{expr.callee}({args})" + if isinstance(expr, Unary): + return f"({expr.op}{self._expr(expr.expr)})" + if isinstance(expr, Binary): + return f"({self._expr(expr.left)} {expr.op} {self._expr(expr.right)})" + raise CodegenError(f"Unhandled expr: {type(expr).__name__}") + + def _type_of(self, expr: Expr) -> TypeInfo: + try: + return self.ctx.expr_types[id(expr)] + except KeyError as exc: + raise CodegenError(f"Missing inferred type for {type(expr).__name__}") from exc + + def _c_type(self, name: str) -> str: + mapping = { + "int": "int", + "float": "double", + "bool": "_Bool", + "char": "char", + "string": "const char *", + "void": "void", + } + if name not in mapping: + raise CodegenError(f"Unsupported C type: {name}") + return mapping[name] + + def _escape_string(self, value: str) -> str: + value = ( + value.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\r", "\\r") + ) + return f'"{value}"' + + def _escape_char(self, value: str) -> str: + mapping = {"\\": "\\\\", "'": "\\'", "\n": "\\n", "\t": "\\t", "\r": "\\r"} + return f"'{mapping.get(value, value)}'" + + def _writeline(self, text: str) -> None: + self.lines.append(" " * self.indent + text) diff --git a/selenium/lexer.py b/selenium/lexer.py new file mode 100644 index 0000000..841411e --- /dev/null +++ b/selenium/lexer.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, List + + +class LexError(Exception): + pass + + +@dataclass(slots=True) +class Token: + kind: str + value: Any + line: int + col: int + + +KEYWORDS = { + "wax": "WAX", + "seal": "SEAL", + "ritual": "RITUAL", + "eclipse": "ECLIPSE", + "shadow": "SHADOW", + "tide": "TIDE", + "whisper": "WHISPER", + "return": "RETURN", + "cast": "CAST", + "true": "BOOL", + "false": "BOOL", + "int": "TYPE", + "float": "TYPE", + "bool": "TYPE", + "char": "TYPE", + "string": "TYPE", + "void": "TYPE", +} + + +class Lexer: + def __init__(self, source: str): + self.source = source + self.length = len(source) + self.i = 0 + self.line = 1 + self.col = 1 + + def tokenize(self) -> List[Token]: + tokens: List[Token] = [] + while not self._eof(): + ch = self._peek() + if ch in " \t\r": + self._advance() + continue + if ch == "\n": + self._advance_line() + continue + if ch == "/" and self._peek(1) == "/": + self._skip_line_comment() + continue + if ch == "/" and self._peek(1) == "*": + self._skip_block_comment() + continue + + start_line, start_col = self.line, self.col + + two = ch + self._peek(1) + if two in {"->", "<=", ">=", "==", "!=", "&&", "||"}: + tokens.append(Token(two, two, start_line, start_col)) + self._advance() + self._advance() + continue + + if ch.isalpha() or ch == "_": + tokens.append(self._identifier()) + continue + if ch.isdigit(): + tokens.append(self._number()) + continue + if ch == '"': + tokens.append(self._string()) + continue + if ch == "'": + tokens.append(self._char()) + continue + + if ch in {";", ",", "(", ")", "{", "}", "+", "-", "*", "/", "%", "=", "<", ">", "!", ":"}: + tokens.append(Token(ch, ch, start_line, start_col)) + self._advance() + continue + + raise LexError(f"Unexpected character {ch!r} at {start_line}:{start_col}") + + tokens.append(Token("EOF", None, self.line, self.col)) + return tokens + + def _eof(self) -> bool: + return self.i >= self.length + + def _peek(self, offset: int = 0) -> str: + pos = self.i + offset + if pos >= self.length: + return "\0" + return self.source[pos] + + def _advance(self) -> str: + ch = self.source[self.i] + self.i += 1 + self.col += 1 + return ch + + def _advance_line(self) -> None: + self.i += 1 + self.line += 1 + self.col = 1 + + def _skip_line_comment(self) -> None: + while not self._eof() and self._peek() != "\n": + self._advance() + if not self._eof() and self._peek() == "\n": + self._advance_line() + + def _skip_block_comment(self) -> None: + self._advance() + self._advance() + while not self._eof(): + if self._peek() == "*" and self._peek(1) == "/": + self._advance() + self._advance() + return + if self._peek() == "\n": + self._advance_line() + else: + self._advance() + raise LexError("Unterminated block comment") + + def _identifier(self) -> Token: + start_line, start_col = self.line, self.col + buf = [] + while not self._eof() and (self._peek().isalnum() or self._peek() == "_"): + buf.append(self._advance()) + text = "".join(buf) + kind = KEYWORDS.get(text, "IDENT") + if kind == "BOOL": + value = text == "true" + else: + value = text + return Token(kind, value, start_line, start_col) + + def _number(self) -> Token: + start_line, start_col = self.line, self.col + buf = [] + has_dot = False + while not self._eof(): + ch = self._peek() + if ch == "." and not has_dot and self._peek(1).isdigit(): + has_dot = True + buf.append(self._advance()) + continue + if ch.isdigit(): + buf.append(self._advance()) + continue + break + text = "".join(buf) + if has_dot: + return Token("FLOAT", float(text), start_line, start_col) + return Token("INT", int(text), start_line, start_col) + + def _string(self) -> Token: + start_line, start_col = self.line, self.col + self._advance() + buf = [] + while not self._eof(): + ch = self._peek() + if ch == '"': + self._advance() + return Token("STRING", "".join(buf), start_line, start_col) + if ch == "\\": + self._advance() + if self._eof(): + break + esc = self._advance() + mapping = {"n": "\n", "t": "\t", "r": "\r", '"': '"', "\\": "\\"} + buf.append(mapping.get(esc, esc)) + continue + if ch == "\n": + raise LexError(f"Unterminated string literal at {start_line}:{start_col}") + buf.append(self._advance()) + raise LexError(f"Unterminated string literal at {start_line}:{start_col}") + + def _char(self) -> Token: + start_line, start_col = self.line, self.col + self._advance() + if self._eof(): + raise LexError(f"Unterminated char literal at {start_line}:{start_col}") + if self._peek() == "\\": + self._advance() + if self._eof(): + raise LexError(f"Unterminated char literal at {start_line}:{start_col}") + esc = self._advance() + mapping = {"n": "\n", "t": "\t", "r": "\r", "'": "'", '"': '"', "\\": "\\"} + value = mapping.get(esc, esc) + else: + value = self._advance() + if self._eof() or self._peek() != "'": + raise LexError(f"Unterminated char literal at {start_line}:{start_col}") + self._advance() + return Token("CHAR", value, start_line, start_col) diff --git a/selenium/main.py b/selenium/main.py new file mode 100644 index 0000000..009479d --- /dev/null +++ b/selenium/main.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from .codegen_c import CCodeGenerator, CodegenContext +from .parser import Parser, ParseError +from .sema import SemanticAnalyzer, SemanticError + + +class CompileError(Exception): + pass + + +def compile_source(source: str) -> str: + parser = Parser.from_source(source) + program = parser.parse() + analyzer = SemanticAnalyzer() + analyzer.analyze(program) + generator = CCodeGenerator(program, CodegenContext(expr_types=analyzer.expr_types)) + return generator.generate() + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="seleniumc", description="Compile Selenium source to C") + parser.add_argument("input", help="Input .sel file") + parser.add_argument("-o", "--output", required=True, help="Output C file") + args = parser.parse_args(argv) + + input_path = Path(args.input) + output_path = Path(args.output) + + try: + source = input_path.read_text(encoding="utf-8") + c_code = compile_source(source) + output_path.write_text(c_code, encoding="utf-8") + except (OSError, ParseError, SemanticError, CompileError) as exc: + print(f"seleniumc: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/selenium/parser.py b/selenium/parser.py new file mode 100644 index 0000000..e21c3f3 --- /dev/null +++ b/selenium/parser.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +from typing import List, Optional + +from .ast import ( + Assign, + Binary, + Block, + Call, + Cast, + Expr, + ExprStmt, + FunctionDecl, + IfStmt, + Literal, + Param, + PrintStmt, + Program, + ReturnStmt, + Stmt, + TopLevel, + TypeRef, + Unary, + VarDecl, + VarRef, + WhileStmt, +) +from .lexer import Lexer, Token, LexError + + +class ParseError(Exception): + pass + + +TYPE_TOKENS = {"TYPE"} + + +class Parser: + def __init__(self, tokens: List[Token]): + self.tokens = tokens + self.i = 0 + + @classmethod + def from_source(cls, source: str) -> "Parser": + return cls(Lexer(source).tokenize()) + + def parse(self) -> Program: + items: List[TopLevel] = [] + while not self._check("EOF"): + items.append(self._declaration_or_stmt()) + return Program(items) + + def _declaration_or_stmt(self) -> TopLevel: + if self._match("RITUAL"): + return self._function_decl() + if self._match("WAX"): + return self._var_decl(mutable=True) + if self._match("SEAL"): + return self._var_decl(mutable=False) + return self._statement() + + def _function_decl(self) -> FunctionDecl: + name = self._consume("IDENT", "Expected function name").value + self._consume("(", "Expected '(' after function name") + params: List[Param] = [] + if not self._check(")"): + while True: + ptype = self._type_ref() + pname = self._consume("IDENT", "Expected parameter name").value + params.append(Param(ptype, pname)) + if not self._match(","): + break + self._consume(")", "Expected ')' after parameters") + self._consume("->", "Expected '->' before return type") + return_type = self._type_ref() + body = self._block() + self._consume(";", "Expected ';' after function body") + return FunctionDecl(name, params, return_type, body) + + def _var_decl(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() + self._consume(";", "Expected ';' after declaration") + return VarDecl(mutable, var_type, name, value) + + def _statement(self) -> Stmt: + if self._match("ECLIPSE"): + self._consume("(", "Expected '(' after eclipse") + cond = self._expression() + self._consume(")", "Expected ')' after condition") + then_block = self._block() + else_block = None + if self._match("SHADOW"): + else_block = self._block() + self._consume(";", "Expected ';' after if statement") + return IfStmt(cond, then_block, else_block) + + if self._match("TIDE"): + self._consume("(", "Expected '(' after tide") + cond = self._expression() + self._consume(")", "Expected ')' after condition") + body = self._block() + self._consume(";", "Expected ';' after while statement") + return WhileStmt(cond, body) + + if self._match("RETURN"): + if self._check(";"): + self._advance() + return ReturnStmt(None) + value = self._expression() + self._consume(";", "Expected ';' after return") + return ReturnStmt(value) + + if self._match("WHISPER"): + value = self._expression() + self._consume(";", "Expected ';' after whisper") + return PrintStmt(value) + + if self._check("IDENT") and self._check_next("="): + name = self._advance().value + self._advance() # = + value = self._expression() + self._consume(";", "Expected ';' after assignment") + return Assign(name, value) + + if self._check("{"): + block = self._block() + self._consume(";", "Expected ';' after block") + return block + + expr = self._expression() + self._consume(";", "Expected ';' after expression") + return ExprStmt(expr) + + def _block(self) -> Block: + self._consume("{", "Expected '{' to start block") + statements: List[Stmt] = [] + while not self._check("}"): + statements.append(self._declaration_or_stmt()) + self._consume("}", "Expected '}' after block") + return Block(statements) + + def _type_ref(self) -> TypeRef: + tok = self._consume("TYPE", "Expected type name") + return TypeRef(tok.value) + + def _expression(self) -> Expr: + return self._or() + + def _or(self) -> Expr: + expr = self._and() + while self._match("||"): + op = "||" + right = self._and() + expr = Binary(expr, op, right) + return expr + + def _and(self) -> Expr: + expr = self._equality() + while self._match("&&"): + op = "&&" + right = self._equality() + expr = Binary(expr, op, right) + return expr + + def _equality(self) -> Expr: + expr = self._comparison() + while self._match("==", "!="): + op = self._previous().kind + right = self._comparison() + expr = Binary(expr, op, right) + return expr + + def _comparison(self) -> Expr: + expr = self._term() + while self._match("<", "<=", ">", ">="): + op = self._previous().kind + right = self._term() + expr = Binary(expr, op, right) + return expr + + def _term(self) -> Expr: + expr = self._factor() + while self._match("+", "-"): + op = self._previous().kind + right = self._factor() + expr = Binary(expr, op, right) + return expr + + def _factor(self) -> Expr: + expr = self._unary() + while self._match("*", "/", "%"): + op = self._previous().kind + right = self._unary() + expr = Binary(expr, op, right) + return expr + + def _unary(self) -> Expr: + if self._match("!", "-"): + op = self._previous().kind + expr = self._unary() + return Unary(op, expr) + return self._call() + + def _call(self) -> Expr: + expr = self._primary() + while self._match("("): + if not isinstance(expr, VarRef): + raise self._error(self._previous(), "Only named functions can be called") + args: List[Expr] = [] + if not self._check(")"): + while True: + args.append(self._expression()) + if not self._match(","): + break + self._consume(")", "Expected ')' after arguments") + expr = Call(expr.name, args) + return expr + + def _primary(self) -> Expr: + if self._match("INT"): + return Literal(self._previous().value, "int") + if self._match("FLOAT"): + return Literal(self._previous().value, "float") + if self._match("STRING"): + return Literal(self._previous().value, "string") + if self._match("CHAR"): + return Literal(self._previous().value, "char") + if self._match("BOOL"): + return Literal(self._previous().value, "bool") + if self._match("IDENT"): + return VarRef(self._previous().value) + if self._match("("): + expr = self._expression() + self._consume(")", "Expected ')'") + return expr + if self._match("CAST"): + self._consume("(", "Expected '(' after cast") + target = self._type_ref() + self._consume(",", "Expected ',' in cast") + expr = self._expression() + self._consume(")", "Expected ')' after cast") + return Cast(target, expr) + raise self._error(self._peek(), "Expected expression") + + def _match(self, *kinds: str) -> bool: + for kind in kinds: + if self._check(kind): + self._advance() + return True + return False + + def _consume(self, kind: str, message: str) -> Token: + if self._check(kind): + return self._advance() + raise self._error(self._peek(), message) + + def _check(self, kind: str) -> bool: + return self._peek().kind == kind + + def _check_next(self, kind: str) -> bool: + if self.i + 1 >= len(self.tokens): + return False + return self.tokens[self.i + 1].kind == kind + + def _advance(self) -> Token: + tok = self.tokens[self.i] + if not self._check("EOF"): + self.i += 1 + return tok + + def _peek(self) -> Token: + return self.tokens[self.i] + + def _previous(self) -> Token: + return self.tokens[self.i - 1] + + def _error(self, tok: Token, message: str) -> ParseError: + return ParseError(f"{message} at {tok.line}:{tok.col}; got {tok.kind}") diff --git a/selenium/runtime.c b/selenium/runtime.c new file mode 100644 index 0000000..f4fa070 --- /dev/null +++ b/selenium/runtime.c @@ -0,0 +1,8 @@ +#include +#include + +void selenium_print_int(int value) { printf("%d\n", value); } +void selenium_print_float(double value) { printf("%g\n", value); } +void selenium_print_bool(_Bool value) { printf("%s\n", value ? "true" : "false"); } +void selenium_print_char(char value) { printf("%c\n", value); } +void selenium_print_string(const char *value) { printf("%s\n", value); } diff --git a/selenium/sema.py b/selenium/sema.py new file mode 100644 index 0000000..9f8231a --- /dev/null +++ b/selenium/sema.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, Optional + +from .ast import ( + Assign, + Binary, + Block, + Call, + Cast, + Expr, + ExprStmt, + FunctionDecl, + IfStmt, + Literal, + Param, + PrintStmt, + Program, + ReturnStmt, + Stmt, + TopLevel, + TypeRef, + Unary, + VarDecl, + VarRef, + WhileStmt, +) + + +class SemanticError(Exception): + pass + + +@dataclass(slots=True) +class TypeInfo: + name: str + + @property + def is_numeric(self) -> bool: + return self.name in {"int", "float"} + + @property + def is_primitive(self) -> bool: + return self.name in {"int", "float", "bool", "char", "string", "void"} + + +@dataclass(slots=True) +class Symbol: + type: TypeInfo + mutable: bool + is_function: bool = False + params: Optional[List[TypeInfo]] = None + return_type: Optional[TypeInfo] = None + + +BUILTINS = {name: TypeInfo(name) for name in ("int", "float", "bool", "char", "string", "void")} + + +class Scope: + def __init__(self, parent: Optional["Scope"] = None): + self.parent = parent + self.symbols: Dict[str, Symbol] = {} + + def define(self, name: str, symbol: Symbol) -> None: + if name in self.symbols: + raise SemanticError(f"Duplicate name: {name}") + self.symbols[name] = symbol + + def lookup(self, name: str) -> Symbol: + scope: Optional[Scope] = self + while scope is not None: + if name in scope.symbols: + return scope.symbols[name] + scope = scope.parent + raise SemanticError(f"Undefined name: {name}") + + +@dataclass(slots=True) +class FunctionInfo: + decl: FunctionDecl + param_types: List[TypeInfo] + return_type: TypeInfo + + +class SemanticAnalyzer: + def __init__(self): + self.globals = Scope() + self.functions: Dict[str, FunctionInfo] = {} + self.current_return: Optional[TypeInfo] = None + self.expr_types: Dict[int, TypeInfo] = {} + + def analyze(self, program: Program) -> Program: + for item in program.items: + if isinstance(item, FunctionDecl): + self._register_function(item) + for item in program.items: + self._analyze_top_level(item, self.globals) + return program + + def _register_function(self, decl: FunctionDecl) -> None: + if decl.name in self.functions or decl.name in self.globals.symbols: + raise SemanticError(f"Duplicate function name: {decl.name}") + param_types = [self._type_of_ref(p.type) for p in decl.params] + return_type = self._type_of_ref(decl.return_type) + self.functions[decl.name] = FunctionInfo(decl, param_types, return_type) + self.globals.define( + decl.name, + Symbol( + type=return_type, + mutable=False, + is_function=True, + params=param_types, + return_type=return_type, + ), + ) + + def _analyze_top_level(self, item: TopLevel, scope: Scope) -> None: + if isinstance(item, FunctionDecl): + self._analyze_function(item) + return + self._analyze_stmt(item, scope, in_function=False) + + def _analyze_function(self, decl: FunctionDecl) -> None: + info = self.functions[decl.name] + fn_scope = Scope(self.globals) + for param, ptype in zip(decl.params, info.param_types): + fn_scope.define(param.name, Symbol(ptype, mutable=True)) + prev = self.current_return + self.current_return = info.return_type + self._analyze_block(decl.body, fn_scope, in_function=True) + self.current_return = prev + + def _analyze_block(self, block: Block, scope: Scope, in_function: bool) -> None: + child = Scope(scope) + for stmt in block.statements: + self._analyze_stmt(stmt, child, in_function) + + def _analyze_stmt(self, stmt: Stmt, scope: Scope, in_function: bool) -> None: + if isinstance(stmt, VarDecl): + value_type = self._infer_expr(stmt.value, scope) + decl_type = self._type_of_ref(stmt.type) + self._require_same_type(decl_type, value_type, f"Type mismatch in declaration of {stmt.name}") + scope.define(stmt.name, Symbol(decl_type, mutable=stmt.mutable)) + return + + if isinstance(stmt, Assign): + sym = scope.lookup(stmt.name) + if sym.is_function: + raise SemanticError(f"Cannot assign to function name: {stmt.name}") + if not sym.mutable: + raise SemanticError(f"Cannot assign to immutable binding: {stmt.name}") + value_type = self._infer_expr(stmt.value, scope) + self._require_same_type(sym.type, value_type, f"Type mismatch in assignment to {stmt.name}") + return + + if isinstance(stmt, IfStmt): + cond_type = self._infer_expr(stmt.condition, scope) + self._require_type(cond_type, "bool", "If condition must be bool") + self._analyze_block(stmt.then_block, scope, in_function) + if stmt.else_block is not None: + self._analyze_block(stmt.else_block, scope, in_function) + return + + if isinstance(stmt, WhileStmt): + cond_type = self._infer_expr(stmt.condition, scope) + self._require_type(cond_type, "bool", "While condition must be bool") + self._analyze_block(stmt.body, scope, in_function) + return + + if isinstance(stmt, ReturnStmt): + if not in_function: + raise SemanticError("Return is only allowed inside a function") + if self.current_return is None: + raise SemanticError("Internal error: missing function return type") + if stmt.value is None: + self._require_type(self.current_return, "void", "Return value required") + else: + value_type = self._infer_expr(stmt.value, scope) + self._require_same_type(self.current_return, value_type, "Return type mismatch") + return + + if isinstance(stmt, PrintStmt): + self._infer_expr(stmt.value, scope) + return + + if isinstance(stmt, ExprStmt): + self._infer_expr(stmt.expr, scope) + return + + if isinstance(stmt, Block): + self._analyze_block(stmt, scope, in_function) + return + + raise SemanticError(f"Unhandled statement type: {type(stmt).__name__}") + + def _infer_expr(self, expr: Expr, scope: Scope) -> TypeInfo: + if isinstance(expr, Literal): + t = self._type_of_literal(expr) + self.expr_types[id(expr)] = t + return t + if isinstance(expr, VarRef): + t = scope.lookup(expr.name).type + self.expr_types[id(expr)] = t + return t + if isinstance(expr, Unary): + t = self._infer_expr(expr.expr, scope) + if expr.op == "-": + if not t.is_numeric: + raise SemanticError("Unary - expects a numeric value") + self.expr_types[id(expr)] = t + return t + if expr.op == "!": + self._require_type(t, "bool", "Unary ! expects bool") + self.expr_types[id(expr)] = BUILTINS["bool"] + return BUILTINS["bool"] + raise SemanticError(f"Unsupported unary operator: {expr.op}") + if isinstance(expr, Binary): + left = self._infer_expr(expr.left, scope) + right = self._infer_expr(expr.right, scope) + op = expr.op + if op in {"+", "-", "*", "/", "%"}: + self._require_same_type(left, right, f"Operands of {op} must have the same type") + if not left.is_numeric: + raise SemanticError(f"Operands of {op} must be numeric") + self.expr_types[id(expr)] = left + return left + if op in {"<", "<=", ">", ">="}: + self._require_same_type(left, right, f"Operands of {op} must have the same type") + if not left.is_numeric: + raise SemanticError(f"Operands of {op} must be numeric") + self.expr_types[id(expr)] = BUILTINS["bool"] + return BUILTINS["bool"] + if op in {"==", "!="}: + self._require_same_type(left, right, f"Operands of {op} must have the same type") + self.expr_types[id(expr)] = BUILTINS["bool"] + return BUILTINS["bool"] + if op in {"&&", "||"}: + self._require_type(left, "bool", f"Operands of {op} must be bool") + self._require_type(right, "bool", f"Operands of {op} must be bool") + self.expr_types[id(expr)] = BUILTINS["bool"] + return BUILTINS["bool"] + raise SemanticError(f"Unsupported binary operator: {op}") + if isinstance(expr, Call): + if expr.callee not in self.functions: + raise SemanticError(f"Unknown function: {expr.callee}") + info = self.functions[expr.callee] + if len(expr.args) != len(info.param_types): + raise SemanticError( + f"Function {expr.callee} expects {len(info.param_types)} argument(s), got {len(expr.args)}" + ) + for arg, expected in zip(expr.args, info.param_types): + actual = self._infer_expr(arg, scope) + self._require_same_type(expected, actual, f"Argument type mismatch in call to {expr.callee}") + self.expr_types[id(expr)] = info.return_type + return info.return_type + if isinstance(expr, Cast): + src = self._infer_expr(expr.expr, scope) + dst = self._type_of_ref(expr.target_type) + if src.name == dst.name: + self.expr_types[id(expr)] = dst + return dst + if src.is_numeric and dst.is_numeric: + self.expr_types[id(expr)] = dst + return dst + if src.name == "bool" and dst.is_numeric: + self.expr_types[id(expr)] = dst + return dst + if src.is_numeric and dst.name == "bool": + self.expr_types[id(expr)] = dst + return dst + if src.name == "char" and dst.is_numeric: + self.expr_types[id(expr)] = dst + return dst + if src.is_numeric and dst.name == "char": + self.expr_types[id(expr)] = dst + return dst + raise SemanticError(f"Unsupported cast from {src.name} to {dst.name}") + raise SemanticError(f"Unhandled expression type: {type(expr).__name__}") + + def _type_of_literal(self, lit: Literal) -> TypeInfo: + return BUILTINS[lit.kind] + + def _type_of_ref(self, tref: TypeRef) -> TypeInfo: + if tref.name not in BUILTINS: + raise SemanticError(f"Unknown type: {tref.name}") + return BUILTINS[tref.name] + + def _require_type(self, actual: TypeInfo, expected_name: str, message: str) -> None: + if actual.name != expected_name: + raise SemanticError(message) + + def _require_same_type(self, left: TypeInfo, right: TypeInfo, message: str) -> None: + if left.name != right.name: + raise SemanticError(message) -- 2.51.2