old
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# example.snip — demonstrates the Snippet language
|
||||||
|
|
||||||
|
snippet init
|
||||||
|
set x = 5
|
||||||
|
set y = 10
|
||||||
|
print x
|
||||||
|
print y
|
||||||
|
|
||||||
|
snippet adjust_x
|
||||||
|
# If x is small, bump it up; otherwise pull it back down
|
||||||
|
if x < 8
|
||||||
|
increase x
|
||||||
|
increase x
|
||||||
|
increase x
|
||||||
|
if x > 6
|
||||||
|
increase x
|
||||||
|
else
|
||||||
|
decrease x
|
||||||
|
else
|
||||||
|
decrease x
|
||||||
|
print x
|
||||||
|
|
||||||
|
snippet adjust_y
|
||||||
|
if y > 8
|
||||||
|
decrease y
|
||||||
|
if y > 5
|
||||||
|
decrease y
|
||||||
|
decrease y
|
||||||
|
else
|
||||||
|
increase y
|
||||||
|
else
|
||||||
|
increase y
|
||||||
|
print y
|
||||||
|
|
||||||
|
snippet final
|
||||||
|
# Both snippets share variables — x and y from above are visible here
|
||||||
|
if x > 7
|
||||||
|
print x
|
||||||
|
else
|
||||||
|
print y
|
||||||
@@ -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