#!/usr/bin/env python3 """ ╔══════════════════════════════════════════════════════════════╗ ║ S N I P P E T L A N G U A G E ║ ║ Interpreter v1.0 ║ ╠══════════════════════════════════════════════════════════════╣ ║ Syntax: ║ ║ snippet ║ ║ set = # assign a variable ║ ║ increase # var += 1 ║ ║ decrease # var -= 1 ║ ║ print # print variable value ║ ║ if > # conditional (> or <) ║ ║ ... ║ ║ else ║ ║ ... ║ ║ ║ ║ Rules: ║ ║ • Snippets run top-to-bottom and share all variables ║ ║ • Indentation defines scope (spaces or tabs, be consistent) ║ • No loops ║ ║ • Numbers only (integers and decimals) ║ ║ • Lines starting with # are comments ║ ╚══════════════════════════════════════════════════════════════╝ Usage: python interpreter.py program.snip # run a file python interpreter.py # REPL (blank line to run) """ import sys import re from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple # ══════════════════════════════════════════════════════════════ # TOKEN TYPES # ══════════════════════════════════════════════════════════════ class TT: SNIPPET = 'SNIPPET' IF = 'IF' ELSE = 'ELSE' INCREASE = 'INCREASE' DECREASE = 'DECREASE' SET = 'SET' PRINT = 'PRINT' NAME = 'NAME' NUMBER = 'NUMBER' GT = 'GT' LT = 'LT' EQ = 'EQ' KEYWORDS: Dict[str, str] = { 'snippet': TT.SNIPPET, 'if': TT.IF, 'else': TT.ELSE, 'increase': TT.INCREASE, 'decrease': TT.DECREASE, 'set': TT.SET, 'print': TT.PRINT, } @dataclass class Token: type: str value: Any line: int def __repr__(self) -> str: return f"Token({self.type}, {self.value!r}, line={self.line})" # ══════════════════════════════════════════════════════════════ # LEXER → list of (indent, [Token …], line_num) # ══════════════════════════════════════════════════════════════ class LexerError(Exception): pass class Lexer: def __init__(self, source: str): self.source = source def tokenize(self) -> List[Tuple[int, List[Token], int]]: result: List[Tuple[int, List[Token], int]] = [] for line_num, raw in enumerate(self.source.splitlines(), 1): stripped = raw.rstrip() if not stripped or stripped.lstrip().startswith('#'): continue indent = len(raw) - len(raw.lstrip(' \t')) tokens = self._lex_words(stripped.lstrip().split(), line_num) if tokens: result.append((indent, tokens, line_num)) return result def _lex_words(self, words: List[str], line_num: int) -> List[Token]: tokens: List[Token] = [] for word in words: if word in KEYWORDS: tokens.append(Token(KEYWORDS[word], word, line_num)) elif re.fullmatch(r'-?\d+(\.\d+)?', word): tokens.append(Token(TT.NUMBER, float(word), line_num)) elif word == '>': tokens.append(Token(TT.GT, '>', line_num)) elif word == '<': tokens.append(Token(TT.LT, '<', line_num)) elif word == '=': tokens.append(Token(TT.EQ, '=', line_num)) elif re.fullmatch(r'[a-zA-Z_]\w*', word): tokens.append(Token(TT.NAME, word, line_num)) else: raise LexerError(f"[Line {line_num}] Unknown token: {word!r}") return tokens # ══════════════════════════════════════════════════════════════ # AST NODES # ══════════════════════════════════════════════════════════════ @dataclass class ProgramNode: snippets: List @dataclass class SnippetNode: name: str body: List line: int @dataclass class ConditionNode: left: str # variable name op: str # '>' or '<' right: float # literal number line: int @dataclass class IfNode: condition: ConditionNode then_body: List else_body: List line: int @dataclass class IncreaseNode: name: str line: int @dataclass class DecreaseNode: name: str line: int @dataclass class SetNode: name: str value: float line: int @dataclass class PrintNode: name: str line: int # ══════════════════════════════════════════════════════════════ # PARSER # ══════════════════════════════════════════════════════════════ class ParseError(Exception): pass # Shorthand type for a lexed line LexLine = Tuple[int, List[Token], int] class Parser: def __init__(self, lines: List[LexLine]): self.lines = lines self.pos = 0 # ── low-level helpers ───────────────────────────────────── def _peek(self) -> Optional[LexLine]: return self.lines[self.pos] if self.pos < len(self.lines) else None def _consume(self) -> LexLine: line = self.lines[self.pos] self.pos += 1 return line # ── top level ───────────────────────────────────────────── def parse(self) -> ProgramNode: snippets = [] while self._peek(): indent, tokens, line_num = self._peek() if tokens[0].type != TT.SNIPPET: raise ParseError( f"[Line {line_num}] Expected 'snippet', got {tokens[0].value!r}" ) snippets.append(self._parse_snippet()) return ProgramNode(snippets) # ── snippet ─────────────────────────────────────────────── def _parse_snippet(self) -> SnippetNode: indent, tokens, line_num = self._consume() if len(tokens) < 2 or tokens[1].type != TT.NAME: raise ParseError(f"[Line {line_num}] Expected a name after 'snippet'") name = tokens[1].value body = self._parse_block(parent_indent=indent) if not body: raise ParseError(f"[Line {line_num}] Snippet '{name}' has an empty body") return SnippetNode(name, body, line_num) # ── block ───────────────────────────────────────────────── def _parse_block(self, parent_indent: int) -> List: """ Collect statements whose indentation is strictly greater than parent_indent and all equal to the first line's indentation. """ nxt = self._peek() if nxt is None or nxt[0] <= parent_indent: return [] block_indent = nxt[0] stmts = [] while True: nxt = self._peek() if nxt is None or nxt[0] < block_indent: break indent, tokens, line_num = nxt if indent > block_indent: raise ParseError( f"[Line {line_num}] Unexpected indentation " f"(expected {block_indent}, got {indent})" ) if tokens[0].type == TT.ELSE: break # parent if-parser will handle this stmts.append(self._parse_statement(block_indent)) return stmts # ── statements ──────────────────────────────────────────── def _parse_statement(self, block_indent: int): indent, tokens, line_num = self._peek() tt = tokens[0].type dispatch = { TT.IF: self._parse_if, TT.INCREASE: self._parse_increase, TT.DECREASE: self._parse_decrease, TT.SET: self._parse_set, TT.PRINT: self._parse_print, } if tt not in dispatch: raise ParseError( f"[Line {line_num}] Unexpected keyword: {tokens[0].value!r}" ) return dispatch[tt](block_indent) if tt == TT.IF else dispatch[tt]() def _parse_if(self, current_indent: int) -> IfNode: indent, tokens, line_num = self._consume() # Expected shape: IF NAME (GT|LT) NUMBER if len(tokens) < 4: raise ParseError(f"[Line {line_num}] Incomplete 'if' condition") if tokens[1].type != TT.NAME: raise ParseError(f"[Line {line_num}] Expected variable name after 'if'") if tokens[2].type not in (TT.GT, TT.LT): raise ParseError(f"[Line {line_num}] Expected '>' or '<' in condition") if tokens[3].type != TT.NUMBER: raise ParseError(f"[Line {line_num}] Expected a number in condition") cond = ConditionNode( left = tokens[1].value, op = tokens[2].value, right = tokens[3].value, line = line_num, ) then_body = self._parse_block(parent_indent=indent) # Optional else at the SAME indent level as the if else_body: List = [] nxt = self._peek() if nxt and nxt[0] == indent and nxt[1][0].type == TT.ELSE: self._consume() # eat 'else' else_body = self._parse_block(parent_indent=indent) return IfNode(cond, then_body, else_body, line_num) def _parse_increase(self) -> IncreaseNode: indent, tokens, line_num = self._consume() if len(tokens) < 2 or tokens[1].type != TT.NAME: raise ParseError(f"[Line {line_num}] Expected variable name after 'increase'") return IncreaseNode(tokens[1].value, line_num) def _parse_decrease(self) -> DecreaseNode: indent, tokens, line_num = self._consume() if len(tokens) < 2 or tokens[1].type != TT.NAME: raise ParseError(f"[Line {line_num}] Expected variable name after 'decrease'") return DecreaseNode(tokens[1].value, line_num) def _parse_set(self) -> SetNode: indent, tokens, line_num = self._consume() # set NAME = NUMBER if len(tokens) < 4: raise ParseError(f"[Line {line_num}] Syntax: set = ") if tokens[1].type != TT.NAME: raise ParseError(f"[Line {line_num}] Expected variable name after 'set'") if tokens[2].type != TT.EQ: raise ParseError(f"[Line {line_num}] Expected '=' after variable name") if tokens[3].type != TT.NUMBER: raise ParseError(f"[Line {line_num}] Expected a number after '='") return SetNode(tokens[1].value, tokens[3].value, line_num) def _parse_print(self) -> PrintNode: indent, tokens, line_num = self._consume() if len(tokens) < 2 or tokens[1].type != TT.NAME: raise ParseError(f"[Line {line_num}] Expected variable name after 'print'") return PrintNode(tokens[1].value, line_num) # ══════════════════════════════════════════════════════════════ # INTERPRETER (tree-walk evaluator) # ══════════════════════════════════════════════════════════════ class SnippetRuntimeError(Exception): pass class Interpreter: def __init__(self): # All snippets share this variable store self.variables: Dict[str, float] = {} def run(self, program: ProgramNode) -> None: for snippet in program.snippets: self._exec_snippet(snippet) # ── snippet ─────────────────────────────────────────────── def _exec_snippet(self, node: SnippetNode) -> None: print(f"── snippet {node.name} ──") self._exec_block(node.body) # ── block / dispatch ────────────────────────────────────── def _exec_block(self, stmts: List) -> None: for stmt in stmts: self._exec(stmt) def _exec(self, node) -> None: dispatch = { IfNode: self._exec_if, IncreaseNode: self._exec_increase, DecreaseNode: self._exec_decrease, SetNode: self._exec_set, PrintNode: self._exec_print, } handler = dispatch.get(type(node)) if handler is None: raise SnippetRuntimeError(f"Unknown AST node: {type(node).__name__}") handler(node) # ── statements ──────────────────────────────────────────── def _exec_if(self, node: IfNode) -> None: if self._eval_condition(node.condition): self._exec_block(node.then_body) else: self._exec_block(node.else_body) def _eval_condition(self, cond: ConditionNode) -> bool: val = self._get_var(cond.left, cond.line) if cond.op == '>': return val > cond.right if cond.op == '<': return val < cond.right raise SnippetRuntimeError(f"Unknown operator: {cond.op!r}") def _exec_increase(self, node: IncreaseNode) -> None: self.variables[node.name] = self._get_var(node.name, node.line) + 1 def _exec_decrease(self, node: DecreaseNode) -> None: self.variables[node.name] = self._get_var(node.name, node.line) - 1 def _exec_set(self, node: SetNode) -> None: self.variables[node.name] = node.value def _exec_print(self, node: PrintNode) -> None: val = self._get_var(node.name, node.line) # Show as int when the value is a whole number display = int(val) if val == int(val) else val print(f" {node.name} = {display}") # ── helpers ─────────────────────────────────────────────── def _get_var(self, name: str, line: int) -> float: if name not in self.variables: raise SnippetRuntimeError( f"[Line {line}] Undefined variable: '{name}'" ) return self.variables[name] # ══════════════════════════════════════════════════════════════ # PUBLIC ENTRY POINT # ══════════════════════════════════════════════════════════════ def run_source(source: str) -> None: """Lex → parse → interpret a source string.""" try: lines = Lexer(source).tokenize() ast = Parser(lines).parse() Interpreter().run(ast) except (LexerError, ParseError, SnippetRuntimeError) as err: print(f"\n ✖ {err}", file=sys.stderr) sys.exit(1) # ══════════════════════════════════════════════════════════════ # COMMAND LINE # ══════════════════════════════════════════════════════════════ REPL_BANNER = """\ ╔════════════════════════════════╗ ║ Snippet Language REPL v1.0 ║ ║ blank line → run ║ ║ 'exit' → quit ║ ╚════════════════════════════════╝ """ def _repl() -> None: print(REPL_BANNER) while True: collected: List[str] = [] prompt = '> ' try: while True: line = input(prompt) if line.strip().lower() == 'exit': sys.exit(0) if line == '' and collected: break collected.append(line) prompt = ' ' except EOFError: break if collected: run_source('\n'.join(collected)) print() def main() -> None: if len(sys.argv) == 1: _repl() elif len(sys.argv) == 2: path = sys.argv[1] try: with open(path) as fh: source = fh.read() except FileNotFoundError: print(f"File not found: {path}", file=sys.stderr) sys.exit(1) run_source(source) else: print(f"Usage: {sys.argv[0]} [file.snip]", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()