From a96f5bbd6a9bb8977340ba6ed8b00c05bc82a2a5 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Thu, 25 Jun 2026 16:27:15 +0100 Subject: [PATCH] docs: add docstrings to source files and examples --- examples/bitwise.sel | 2 ++ examples/bool.sel | 2 ++ examples/break_continue.sel | 2 ++ examples/char.sel | 2 ++ examples/factorial.sel | 3 +++ examples/float.sel | 2 ++ examples/for.sel | 2 ++ examples/inc_dec.sel | 2 ++ examples/input.sel | 3 +++ examples/switch.sel | 2 ++ examples/ternary.sel | 2 ++ flake.nix | 4 ++++ selenium/__init__.py | 5 ++++- selenium/ast.py | 6 ++++++ selenium/codegen_c.py | 23 ++++++++++++++++++++++- selenium/lexer.py | 14 +++++++++++++- selenium/main.py | 10 +++++++++- selenium/parser.py | 24 +++++++++++++++++++++++- selenium/sema.py | 26 +++++++++++++++++++++++++- 19 files changed, 130 insertions(+), 6 deletions(-) diff --git a/examples/bitwise.sel b/examples/bitwise.sel index 251c678..667bfda 100644 --- a/examples/bitwise.sel +++ b/examples/bitwise.sel @@ -1,3 +1,5 @@ +// Bitwise shift and logical operators on integers. + wax int a = 5; wax int b = 3; diff --git a/examples/bool.sel b/examples/bool.sel index 9d9d4f9..e2bfd85 100644 --- a/examples/bool.sel +++ b/examples/bool.sel @@ -1,3 +1,5 @@ +// Boolean literals, logical operators, and negation. + seal bool t = true; seal bool f = false; whisper t; diff --git a/examples/break_continue.sel b/examples/break_continue.sel index 30951a7..a13814e 100644 --- a/examples/break_continue.sel +++ b/examples/break_continue.sel @@ -1,3 +1,5 @@ +// Loop control: skip i==3 with continue, stop at i==7 with break. + orbit (wax int i = 0; i < 10; i = i + 1) { eclipse (i == 3) { continue; diff --git a/examples/char.sel b/examples/char.sel index 04a0093..8f77343 100644 --- a/examples/char.sel +++ b/examples/char.sel @@ -1,3 +1,5 @@ +// Character literals and cast to integer (ASCII value). + seal char c = 'A'; whisper c; diff --git a/examples/factorial.sel b/examples/factorial.sel index 990a63b..ed34dfe 100644 --- a/examples/factorial.sel +++ b/examples/factorial.sel @@ -1,3 +1,6 @@ +// Recursive factorial -- exercises function definition, conditionals, +// and recursion (all forward-declarable in Selenium). + ritual fact(int n) -> int { eclipse (n <= 1) { return 1; diff --git a/examples/float.sel b/examples/float.sel index 1483e98..7c71674 100644 --- a/examples/float.sel +++ b/examples/float.sel @@ -1,3 +1,5 @@ +// Floating-point literals, variables, and explicit cast from int. + wax float pi = 3.14159; whisper pi; diff --git a/examples/for.sel b/examples/for.sel index 38da95b..e45db2c 100644 --- a/examples/for.sel +++ b/examples/for.sel @@ -1,3 +1,5 @@ +// For-loop equivalent: orbit with init, condition, and increment. + orbit (wax int i = 0; i < 5; i = i + 1) { whisper i; }; \ No newline at end of file diff --git a/examples/inc_dec.sel b/examples/inc_dec.sel index 3fad42c..edb258f 100644 --- a/examples/inc_dec.sel +++ b/examples/inc_dec.sel @@ -1,3 +1,5 @@ +// Prefix increment and decrement operators. + wax int i = 0; whisper ++i; whisper i; diff --git a/examples/input.sel b/examples/input.sel index f9682e1..2d43e59 100644 --- a/examples/input.sel +++ b/examples/input.sel @@ -1,3 +1,6 @@ +// Runtime input: prompt, read, display -- exercises the read_int +// built-in and variable assignment. + whisper "Enter an int:"; wax int x = 0; diff --git a/examples/switch.sel b/examples/switch.sel index 62ce057..231dc4b 100644 --- a/examples/switch.sel +++ b/examples/switch.sel @@ -1,3 +1,5 @@ +// Switch statement with multiple cases and a default branch. + wax int x = 2; switch (x) { case 1: { diff --git a/examples/ternary.sel b/examples/ternary.sel index cb7647b..6489080 100644 --- a/examples/ternary.sel +++ b/examples/ternary.sel @@ -1,3 +1,5 @@ +// Ternary conditional: compute max, then use inline for string choice. + seal int a = 5; seal int b = 10; wax int max = a > b ? a : b; diff --git a/flake.nix b/flake.nix index 1157b0c..c8740ce 100644 --- a/flake.nix +++ b/flake.nix @@ -1,3 +1,7 @@ +# ── Selenium compiler dev shell ───────────────────────────────────── +# Provides Python 3 + setuptools + virtualenv for hacking on the +# compiler itself. Formatting is handled by nixfmt-rfc-style. + { description = "selenium — lunar/poetic esoteric language that compiles to C"; diff --git a/selenium/__init__.py b/selenium/__init__.py index ba1e038..8f04966 100644 --- a/selenium/__init__.py +++ b/selenium/__init__.py @@ -1,4 +1,7 @@ -"""Selenium compiler package.""" +"""Selenium compiler package. + +A small esoteric language with a lunar / poetic surface and a strict, +C-like core. Each public module is exported here for convenience.""" __all__ = [ "ast", diff --git a/selenium/ast.py b/selenium/ast.py index daf4966..2e249bc 100644 --- a/selenium/ast.py +++ b/selenium/ast.py @@ -1,3 +1,9 @@ +"""AST node types for the Selenium language. + +Every construct in the language maps to a dataclass here. +The parse phase produces these; the semantic analyser and code +generator consume them by isinstance-dispatch.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/selenium/codegen_c.py b/selenium/codegen_c.py index ef7e41c..e0105d9 100644 --- a/selenium/codegen_c.py +++ b/selenium/codegen_c.py @@ -1,3 +1,9 @@ +"""C code generator for Selenium. + +Walks the analysed AST and emits C89-compatible source. +Each Selenium construct maps to its C equivalent; the I/O +builtins compile to thin static wrapper functions.""" + from __future__ import annotations from dataclasses import dataclass @@ -34,7 +40,7 @@ from .sema import TypeInfo class CodegenError(Exception): - pass + """Raised when an AST node cannot be translated to C.""" @dataclass(slots=True) @@ -43,6 +49,12 @@ class CodegenContext: class CCodeGenerator: + """Walk the analysed AST and emit C89-compatible source code. + + Selenium's lunar keywords (ritual, eclipse, tide, whisper, ...) + compile directly to their C equivalents. Built-in I/O functions + delegate to static wrapper functions emitted in the prelude.""" + def __init__(self, program: Program, context: CodegenContext): self.program = program self.ctx = context @@ -50,6 +62,7 @@ class CCodeGenerator: self.indent = 0 def generate(self) -> str: + """Produce the full C source: prelude, globals, functions, main.""" self.lines = [] self.indent = 0 self._emit_prelude() @@ -65,6 +78,8 @@ class CCodeGenerator: self._emit_main(statements) return "\n".join(self.lines) + "\n" + # ── C prelude (I/O wrappers) ────────────────────────────────── + def _emit_prelude(self) -> None: self._writeline("#include ") self._writeline("#include ") @@ -83,6 +98,8 @@ class CCodeGenerator: self._writeline("static char selenium_read_char() { char x; scanf(\" %c\", &x); return x; }") self._writeline("") + # ── Function emission ───────────────────────────────────────── + 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) @@ -103,6 +120,8 @@ class CCodeGenerator: self.indent -= 1 self._writeline("}") + # ── Statement-by-statement dispatch ─────────────────────────── + def _emit_item(self, item: object) -> None: if isinstance(item, VarDecl): self._emit_vardecl(item) @@ -231,6 +250,8 @@ class CCodeGenerator: else: raise CodegenError(f"Cannot print type {t.name}") + # ── Expression emission ─────────────────────────────────────── + def _expr(self, expr: Expr) -> str: if isinstance(expr, Literal): if expr.kind == "string": diff --git a/selenium/lexer.py b/selenium/lexer.py index eed2411..f380704 100644 --- a/selenium/lexer.py +++ b/selenium/lexer.py @@ -1,3 +1,8 @@ +"""Tokeniser for the Selenium language. + +Maps source text to a stream of tokens: keywords, literals, operators, +and delimiters. Every token carries line and column for error reporting.""" + from __future__ import annotations from dataclasses import dataclass @@ -5,7 +10,7 @@ from typing import Any, List class LexError(Exception): - pass + """Raised on invalid tokens, unterminated literals, or unexpected characters.""" @dataclass(slots=True) @@ -44,6 +49,12 @@ KEYWORDS = { class Lexer: + """Scan source text left-to-right, producing a token list. + + Handles line comments (//), block comments (/* */), identifiers, + numeric literals (int and float), string/char literals with escape + sequences, and all operator symbols.""" + def __init__(self, source: str): self.source = source self.length = len(source) @@ -52,6 +63,7 @@ class Lexer: self.col = 1 def tokenize(self) -> List[Token]: + """Scan the entire source and return the token stream.""" tokens: List[Token] = [] while not self._eof(): ch = self._peek() diff --git a/selenium/main.py b/selenium/main.py index b3e678b..72a16f2 100644 --- a/selenium/main.py +++ b/selenium/main.py @@ -1,3 +1,9 @@ +"""CLI entry point for the Selenium compiler. + +Invoked as ``seleniumc`` (installed via pyproject.toml). +Compiles .sel files to C; optionally drives the C compiler +and runs the result in one step.""" + from __future__ import annotations import argparse @@ -12,10 +18,11 @@ from .sema import SemanticAnalyzer, SemanticError class CompileError(Exception): - pass + """Raised on failures that fall outside parse or semantic errors.""" def compile_source(source: str) -> str: + """Full compilation pipeline: parse, type-check, emit C.""" parser = Parser.from_source(source) program = parser.parse() analyzer = SemanticAnalyzer() @@ -25,6 +32,7 @@ def compile_source(source: str) -> str: def main(argv: list[str] | None = None) -> int: + """CLI entry point. Parse args, compile, optionally compile C and run.""" ap = argparse.ArgumentParser( prog="seleniumc", description="Compile Selenium source to C (optionally compile & run)", diff --git a/selenium/parser.py b/selenium/parser.py index 831ddd3..8d1d780 100644 --- a/selenium/parser.py +++ b/selenium/parser.py @@ -1,3 +1,9 @@ +"""Recursive-descent parser for Selenium. + +Tokenises source via the lexer, then walks a Pratt-style +precedence-climbing expression parser inside a statement-first +top-down structure. Produces a Program AST.""" + from __future__ import annotations from typing import List, Optional @@ -35,27 +41,37 @@ from .lexer import Lexer, Token, LexError class ParseError(Exception): - pass + """Raised on syntax errors with line/col context and expected token details.""" TYPE_TOKENS = {"TYPE"} class Parser: + """Recursive-descent parser over a token stream. + + Expression parsing uses operator-precedence climbing (Pratt-style) + for correct associativity and precedence across 11 levels.""" + def __init__(self, tokens: List[Token]): self.tokens = tokens self.i = 0 @classmethod def from_source(cls, source: str) -> "Parser": + """Convenience: lex source, return parser ready to go.""" return cls(Lexer(source).tokenize()) def parse(self) -> Program: + """Parse the full token stream into a Program AST.""" + items: List[TopLevel] = [] while not self._check("EOF"): items.append(self._declaration_or_stmt()) return Program(items) + # ── Top-level dispatch ──────────────────────────────────────────── + def _declaration_or_stmt(self) -> TopLevel: if self._match("RITUAL"): return self._function_decl() @@ -107,6 +123,8 @@ class Parser: value = self._expression() return VarDecl(mutable, var_type, name, value) + # ── Statement parsing ─────────────────────────────────────────── + def _statement(self) -> Stmt: if self._match("ECLIPSE"): self._consume("(", "Expected '(' after eclipse") @@ -220,6 +238,8 @@ class Parser: tok = self._consume("TYPE", "Expected type name") return TypeRef(tok.value) + # ── Expression parsing (precedence climbing) ──────────────────── + def _expression(self) -> Expr: return self._ternary() @@ -360,6 +380,8 @@ class Parser: return Cast(target, expr) raise self._error(self._peek(), "Expected expression") + # ── Parser primitives ─────────────────────────────────────────── + def _match(self, *kinds: str) -> bool: for kind in kinds: if self._check(kind): diff --git a/selenium/sema.py b/selenium/sema.py index 99f2c72..80cb5cd 100644 --- a/selenium/sema.py +++ b/selenium/sema.py @@ -1,3 +1,10 @@ +"""Semantic analyser and type checker for Selenium. + +Two-pass over the AST: first pass registers all function signatures +so forward calls resolve; second pass type-checks every expression +and statement, building a type map keyed by object identity for the +code generator.""" + from __future__ import annotations from dataclasses import dataclass @@ -35,7 +42,7 @@ from .ast import ( class SemanticError(Exception): - pass + """Raised when a program passes syntax but violates Selenium's type rules.""" @dataclass(slots=True) @@ -64,16 +71,20 @@ BUILTINS = {name: TypeInfo(name) for name in ("int", "float", "bool", "char", "s class Scope: + """Lexical scope chain: names defined here or in parent scopes.""" + def __init__(self, parent: Optional["Scope"] = None): self.parent = parent self.symbols: Dict[str, Symbol] = {} def define(self, name: str, symbol: Symbol) -> None: + """Bind a name in this scope. Rejects duplicates.""" if name in self.symbols: raise SemanticError(f"Duplicate name: {name}") self.symbols[name] = symbol def lookup(self, name: str) -> Symbol: + """Resolve a name by walking the scope chain upward.""" scope: Optional[Scope] = self while scope is not None: if name in scope.symbols: @@ -89,7 +100,15 @@ class FunctionInfo: return_type: TypeInfo +# ── Symbol tables ───────────────────────────────────────────────── + class SemanticAnalyzer: + """Two-pass type checker and name resolver. + + Pass 1 registers all function signatures (enabling forward calls). + Pass 2 walks every top-level item and expression, validating types + and populating ``expr_types`` for downstream code generation.""" + def __init__(self): self.globals = Scope() self.functions: Dict[str, FunctionInfo] = {} @@ -99,6 +118,7 @@ class SemanticAnalyzer: self.switch_depth = 0 def analyze(self, program: Program) -> Program: + """Run both passes over the program. Mutates ``expr_types`` in place.""" for item in program.items: if isinstance(item, FunctionDecl): self._register_function(item) @@ -246,6 +266,8 @@ class SemanticAnalyzer: raise SemanticError(f"Unhandled statement type: {type(stmt).__name__}") + # ── Expression type inference ────────────────────────────────── + def _infer_expr(self, expr: Expr, scope: Scope) -> TypeInfo: if isinstance(expr, Literal): t = self._type_of_literal(expr) @@ -354,6 +376,8 @@ class SemanticAnalyzer: raise SemanticError(f"Unsupported cast from {src.name} to {dst.name}") raise SemanticError(f"Unhandled expression type: {type(expr).__name__}") + # ── Helpers ──────────────────────────────────────────────────── + def _type_of_literal(self, lit: Literal) -> TypeInfo: return BUILTINS[lit.kind] -- 2.51.2