ristrutturazione in directory
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user