Files
organism/elements/neuron/appunti/simple_interpreter/interpreter.py
T

481 lines
19 KiB
Python

#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════╗
║ S N I P P E T L A N G U A G E ║
║ Interpreter v1.0 ║
╠══════════════════════════════════════════════════════════════╣
║ Syntax: ║
║ snippet <name> ║
║ set <var> = <number> # assign a variable ║
║ increase <var> # var += 1 ║
║ decrease <var> # var -= 1 ║
║ print <var> # print variable value ║
║ if <var> > <number> # 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 <var> = <number>")
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()