From c760a98e60c00825260eb09fe84d724fa2214a40 Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Sun, 3 May 2026 12:58:29 -0500 Subject: [PATCH] feat: add CLI with build and run commands --- pyproject.toml | 4 + .../__pycache__/__init__.cpython-314.pyc | Bin 144 -> 173 bytes src/tampy/__pycache__/cli.cpython-314.pyc | Bin 3530 -> 2752 bytes .../__pycache__/transpiler.cpython-314.pyc | Bin 1011 -> 1518 bytes src/tampy/cli.py | 52 +++++++++++ src/tampy/transpiler.py | 84 ++---------------- test.py | 6 ++ test.tampy | 6 ++ tests/test_cli.py | 42 +++++++++ tests/test_parser.py | 45 ---------- tests/test_transpiler.py | 79 ++++++++++++++++ uv.lock | 4 +- 12 files changed, 200 insertions(+), 122 deletions(-) create mode 100644 test.py create mode 100644 test.tampy create mode 100644 tests/test_cli.py delete mode 100644 tests/test_parser.py create mode 100644 tests/test_transpiler.py diff --git a/pyproject.toml b/pyproject.toml index 3108dca..6a1503e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,7 @@ dependencies = [] [project.scripts] tampy = "tampy.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/tampy/__pycache__/__init__.cpython-314.pyc b/src/tampy/__pycache__/__init__.cpython-314.pyc index e22363d7d86e06a31e5240ceedda4757712f380d..8a7def2fff3302d7e5959c41a1501627a7518210 100644 GIT binary patch delta 97 zcmbQhxR#Mun~#@^0SNwdf6o+~$SbJ};x#Y?F_bU_F)A}?GF9<~B<2=WDikCpXD6no s>iKCh-eQlBPsvY?k6$^_(m{$1sDTlPi#dSA2WCb_#@h@cMJzxL0B1E7(EtDd delta 68 zcmZ3>IDwH@n~#@^0SG#3zh*K_7fK6rQ!mKfCL7vNlN|iDL&+CnhA}x1a{vQVKYbrq(ucIRv)G-Xu%?vz=WN zV)fu&+N7#^08r)i=TfdmY22}sNcV=$()88%be4Lgyx5g2tC zjv&R@N1RcY;WBkN;*PQg8w8T04H(=-UUDA006>cP?F1LOZg9Bfh9HHf23x#v@(toSzS}p3LWT zWsBQl>`M?blvs&tpn~8vd-0N|fzs@8`)l?R8Yr?Ci1Dv_w9ZAeX|)l=HnGM6+gSOf9fz$Pb6ePQQp^8f z7JPNbIBP{p>ii{c6WtbGYK?OqaL~3DyNAZT9W&m%N8eLs6T7V)Vm+4DklkP+Y>i@l zme!C`Tbv!Fbq%GRR?aj&&d2sPFqU=+m|l09Uh8oa>^=+8kT0@muq5cg2FlX5#JJ6> zw9C3p!L4leThoCoSHQT3x3kb)O0ABODW`)v6>d2QuHKJHABs1v5PEZ64}&v z#D*bRFt8gbpXF7gq_E?1UQMS8E$>kgno*7ET;2$wd^W3Qr)X!KR?-@VlcX71zNj8D zav{?a+X=QHRLzMGYFZGhL0LdL)uyB5kU^X7kkv{DujnK=1{EB6V{+gaapqRCga~mw zR&WlQ;DUWno5jpbfj_Hc6_hZP)LWr~Z6Hagc#{du$RkB25o>t^LqoY{L>S~DM;_UQ z-3c_MC6KOQ?@2V3&yZt_(ILX#L@Fig`OD@i(lJNJ)dBWUomiT9%$c)vtRvFm0it&n z(WZf6PE)d`FAR~ANMY8fe>G^w#xA62=|ob&jv1sHL}-X)40F^m$|V(DN3=&`yFN=o zP_8M-ypgz^RxpPYLW`b+f}OgNBJ@nDAcUcEO*JAO?5u@r?q;&mvByk^MT8@#Zt0K` zz;5f{G1mwcGm{vau6S*hBtI)x2h}r`X$r9uDSAF_==5MiTOQmuG@Z*RB%%6r;_47J ztfjb->||P{8H5>9x#$qtfNrAyLcRk_{|85Be*5Jtvv7>6f zoWK2!X@07Bt^&Pf*s%gTmd=zS$4jtd4W8KG#WLTy!gsFnUB&k|n>uffmv#@Y29B>b zohU}v#qQ<4Rk6Q#Zj%+}QVZ(5y5wJ;DYHX&*`eD9DoxD`SLd&Ob8QYjvjcAbV<#XH z`ob%|@UpS$+gCil;o`sg{EN>QXVzR@mDctf>UFi;+Pl))d&_F7_|! zw>~ZlNA3zoeibVoVd3oj*~Rl~o)Fp9`{t%wSmc+^FP$iPBlp~UE3B~4Ip4X|vc`6; ziyb$*zUx{Sninq3Us@Vp9xi!?DjmU!*z%7H`nWmgV-9$HRffyV8sEL{5f{TZ2Cfg> z^Mp1n{m0}_SAMv1*K=Uq)3gx%I=UDyw}e+(!uLGAPg(#Md`4dT3F#6$^!%BZghXO} zu9LU6NQw;T^ppSC&M?e<(EJEAJpkeZz&!*z{{jadf_;BGdzik*V+??vxx$)#*L~>z O^2`%EfbD3Q82$_W(_6y; literal 3530 zcmd5X1`qDrivxxrYvGHbxd)SJmN!Oj!~Rbu1Nt2A)v6W0J-1uDBSog0BIbz1H9)BgVEzK zScB=dfBpPliEO5yvDj!5En(_LFqtzAEu*H6)otR!Unl-O`;;dT_VaJZ`L$A`}6kc&D!WfB;pI7R+v7YB8J%akBhbF;b;%?d% zW1@C(XuC>BDSk)H5tUx-BQ5Ib+;P|9Ws>N)pK2=N>3+T7q9Y!lTG2+z)v4mGXXS)u zQ(c2{VO)5MGX&zTR339ElHybRG;41%2`2BzI}@n14$cfxGVvDnNP0Eqn zQm=TL2f6{q*F$^jw1)3KsoDPuU%ZbZik_!jorb}Q+7Hy_l#35hF2>U_4?C1I_-&$f zH(W7SbeQVZ=^Pke!CS-Ri!&DMo-!Lxpg5)X>&(^Du~6*QdXbvNFHoe>iNzF#lffXMD{K+3-VUHSLZB1x-Q>OMnC^Si*$Fh}U+YbOLACx}{Mi%nHBhzc z4;GrQC$m~cj;^64JtwF1EIHL6KI6o+YNnuOAWhF%>;je++FsBlA{ps2%;(6_xTI!` zf@^4KKCflcmd8XoR1*m1)k4qPL_}1Nqu7>uF0=LxIj!f;A=`z+;*%*74C0d?6!@Vb zGr6o5df&lcc~Q-&Fp1Q(JZ!AVw*7(*<+M7VUtH93i-p#!5bE%fJV8L^bR;k6`CR&v z+|NK4c4slLz~#}x&^U%FsJWD$##S>0X}IPYJ1&UVXq-#(^?rxtOC=4I$K%lR$jBp$ zMd~WDn3aMPYftpE7Ms=6s>Pf6g#~TZ;?rtIMJn{-i2a?Gkc5jy5}K;zy8;*USv7~E zqzNr=GM!G~UR!2)Y$eh*Avs8@hw8kr&QCloVbLRM_pA@d6b;b zs5lscmItbM>?BpHB_K6xkhEKkGuTJ^I&SJP3D-!zEzXW%IdqILpUY^u5Bl8}Z;uLN zCF8Q(#5iH#>yyPSSgh^TSId3k<+iQJL)FY@5Wa2UXtu3MZX+^%tn-m2J*!4OP|c;} za)kJZpk&rqi?HY%SuS`6ImE)puJ4nyf4|l>O~x>GeYw;M$G=%khUEz=>6hvJkL)M_=gmHkQPW zJ+b4F*tP!sr-7!e>)Y?%e)nVl`czqLDv9lTVtYyKd?s_@SA}H;c7(?H-%fmR>M!lr*3IO+c~qlQgTNQ+>u|8l>LFt<(tbtS>0ftJAvoI z5f55IJLu!Y`plnP;zxy#3hRY(@N6kKycZn4`{ASD2=5Eu z!@;-2otaWcWUnJqY`;+SjXV}Emfg~3a5K0O{Leh>3l}R5?b}^T1k&4&9IeBYx zD^_am+iUK7>^=7-2fSyGJiymdlFmPr&fiUa-urv5IGZR*Hx8s5#k5-XN}JbiUfY`4 z_jZ@tg1_K@#&1kL=YTKC9P%D_xY8Wjp1U(wX=vK+y4|%KECvS4q3&{X5Tk1GZA@TP zftHeA-t)`5SN8qAl|b{>!gls{_E+7X4cr?z2)t1VoY@xc2sJ@(4arx;uhT&JQt^40TU5*K6p$I)v*@>i7(yz*COFP@CQJKHcF;O;keP4{s3 zzjfvE^cZ*$5U0cL2R-8SsQbZK+YkIsOGqTh6iy`iMR*2V!EW*)S5ruCMQ{**EEice z8ExL)MJ!M4PYSfR2YdPxM`RaBB<_J{*akpyY+1s$@kW-)OQbTSO{ x1o)qTh9|&dGwwfvw!eVz-}rGRe7MX2*1J*IcLx8!3O}Cs(h1nt;CHa$KLHZXutop? diff --git a/src/tampy/__pycache__/transpiler.cpython-314.pyc b/src/tampy/__pycache__/transpiler.cpython-314.pyc index 6cdd65fd569e0a244f49bc3cefe1499ce83645c3..88ffa6a9d478699627cf33e5e8a009037328d368 100644 GIT binary patch literal 1518 zcmaJ>OK%)S5U!q?eHlOUa2|vZ&k|>ofoNC47D6IIfH)8dS!QDSz{9P{?ASZX?99^L z3tH#E9*~Fv0*>Ya@qvSH{1haFL>g{^1Gj{g*c_?qnRQlR(Nce1T~$3@{Z-YxFh4f~ zIKKX39DmCI9?)hooI)7Ah{6zjP_P9)yUyOSl(j^%b=_9>DX4>Qp8?-lwel0n@vV#C zm3EhJ#NA#Jxk?17?8QkW+`f#{9XF7w*-v|ckWtfP+I~G9tc8<%X|;>Z=xr1Ny!``E z3?s0%D#|_$8ODLPR5xLT(cMYacMy*yOqGnTWOQZHsj^>Dl}%7p6ID$g%cI&j3~G02 zog#6AwC(OhX(R#_x$6Vf$x=7W+EH^YL~ej44dR938@-8ejSXQFwyc*w1!$x8JiNm? z%x5>)dw`Qxwqjj9hpxr&t*e!Z4A5f8l>H|$r>nQ3!N-|s%Qer^PCE{jwxtrdDQ{<@ z8z?=;d6)!Jl8tjor=AlRc7l&u6md%h-QJ)jMOa8rGSKV|^epE=nq~^$ z$+>>^-+VNu%~T(Drj|lb`_rrsgd!6qu{IL9U{UxItp%a8f4Uh~ZBPZ3- z6%>ZB!CqmPNsnX4aJ2+2WG~HCh(26|1GaX+1W`mR$SB!18}rykL(HNT<7%9+C z9}#?RGJ!9k_Yi(L>wY)CH-GPqJI&AT3b}ywkT5|4zaDvB2s;k zdQ8+%=~;sA7Ed7*DKb%uInvG}kRYEyFr3<@`XfO2CVWWCerR@inA2=h&EcFKy^F#S z-bc*G`m|S}tHUN+Eh@HWP5C;LQ%_d(wI;s!G`4#w?}v?`8z-g%j%tmjXKNc9S(i*7 z>q^nkNX<$7wm5^J9VMbjQsj+>o9C`}Sd04Hf0B1-GZbCGb3^!j;lx*iyMw*O&p)~2 z{Dzy`cW>{l{UxIrZr1K6BxA*Y*bkDzvcUOvETl@} zG)glxS2%BHA?IR|zC_pRQ`leFAaTe8r9^zQuOf+_$EP|ljFNo6ZXfZQ|h mfNS~NUKJkN>27%z;i1hKyARL*0jGb3nQ^(s>LWlfC;tJ$Hddwp literal 1011 zcmZ8g-Afcv6hHH|AFk_eTB(bliPdU41`7&CAf_mWZBuAL>oD%lx|_Q*!=1ayio`zj zp!QHu4+cR!)?05q_a{V@bt@=(@=cLg=&5t}gN@EG=l<@T&)>OshFgM7fHHgkcXmPm z_{BGM@z%gGLr{Vh(1dxQd==+~IgyIJkP!PJA;mC~OWapo|J9mwA<)=UCE>vcMUyo} z^NdSX%cP(S1+gIM5UaUTZvx`sMqC|XthZi``Za++v09d1(7f%S`Pv~DrcIhK0DUkF z=W3yAB0v{pvFC2+tvpvdCZ%r33Q_+7_gRbzOpY5gvl(1BiyJmcxubVo+hPLQR0=db zGJy_0(@NT@thJWplxws>M$DTy9z#`vC0G$B9ss29TNhjxGT7uDv{XeC8(0+wASxYT zYrteoY%&iqX@OW+N^&W|#gn#4-k>h8b%v>G#?G7Sy6I+&dn%7msgZYzs!NhJM2TU! zPBv$fQK!h7b=|Nmn;O*AbrQrR{EXZPsuGkz62eDZJ)J(}%k$~V?ij65VaNd&1>Xw8 zRRkqyFo@TN&z=s~!3-_~XyWJV#!($gf*^c_z<22Q89w>=){~a~@ZkIK;H%Kb@OUZsMe%Qs?6tkz*jLn# uiu&dvCs5oi?wxqJRhlbH;2A4>@F>e(MCkL!<-gzCwyB8nE)tj;^8W?wD$Yg# diff --git a/src/tampy/cli.py b/src/tampy/cli.py index e69de29..22346eb 100644 --- a/src/tampy/cli.py +++ b/src/tampy/cli.py @@ -0,0 +1,52 @@ +"""Tampy CLI.""" + +import argparse +import subprocess +import sys +from pathlib import Path + +from src.tampy.transpiler import SimpleTranspiler +from src.tampy.keywords import load_keywords + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser(description="Tamil code compiler") + subparsers = parser.add_subparsers(dest="command", required=True) + + build_parser = subparsers.add_parser("build", help="Build without running") + build_parser.add_argument("file", help="Tamil file to build") + + run_parser = subparsers.add_parser("run", help="Build and run") + run_parser.add_argument("file", help="Tamil file to run") + + args = parser.parse_args() + + keywords = load_keywords(Path("src/tampy/keywords.json")) + transpiler = SimpleTranspiler(keywords) + + with open(args.file, "r", encoding="utf-8") as f: + code = f.read() + + python_code = transpiler.transpile(code) + + if args.command == "build": + output_file = args.file.replace(".tampy", ".py") + with open(output_file, "w", encoding="utf-8") as f: + f.write(python_code) + print(f"Generated: {output_file}") + elif args.command == "run": + result = subprocess.run( + [sys.executable, "-c", python_code], + capture_output=True, + text=True + ) + if result.returncode == 0: + print(result.stdout) + else: + print(result.stderr, file=sys.stderr) + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/src/tampy/transpiler.py b/src/tampy/transpiler.py index 63fe2f1..2fff912 100644 --- a/src/tampy/transpiler.py +++ b/src/tampy/transpiler.py @@ -1,82 +1,16 @@ -"""Parse Tamil code into Python AST.""" +"""Simple transpiler using ast.unparse.""" from typing import Any import ast -class TamilParser(ast.NodeVisitor): - """Parse Tamil code and map keywords to Python.""" +class SimpleTranspiler: + """Parse and generate Python code.""" - def __init__(self, keywords: dict[str, str]): - self.keywords = keywords - self.errors: list[str] = [] + def __init__(self, keywords: dict[str, str] | None = None): + self.keywords = keywords or {} - def parse(self, code: str) -> ast.AST: - """Parse Tamil code and return Python AST.""" - try: - tree = ast.parse(code) - self._transform(tree) - return tree - except SyntaxError as e: - self.errors.append(f"SyntaxError at line {e.lineno}: {e.msg}") - raise - - def _transform(self, node: ast.AST) -> None: - """Transform node to handle Tamil keywords.""" - node = self._visit(node) - if isinstance(node, ast.AST): - for field, value in ast.iter_fields(node): - if isinstance(value, list): - for i, item in enumerate(value): - if isinstance(item, ast.AST): - value[i] = self._visit(item) - elif isinstance(value, ast.AST): - setattr(node, field, self._visit(value)) - - def _visit(self, node: ast.AST) -> Any: - """Visit and transform a single node.""" - if not hasattr(node, "lineno"): - return node - - line = node.lineno - col = getattr(node, "col_offset", 0) - - keyword = self._get_keyword_at_position(node, line, col) - if keyword: - node.keywords = self.keywords.get(keyword, keyword) - - visitor = self._get_visitor_type(type(node)) - if visitor: - return visitor(node) - - return node - - def _get_keyword_at_position(self, node: ast.AST, lineno: int, col: int) -> str | None: - """Check if keyword appears at given position.""" - # This is a placeholder - actual keyword detection will use tokens - # For now, we'll handle common keywords by name - node_type = type(node).__name__ - - keyword_map = { - "FunctionDef": "def", - "Return": "return", - "If": "if", - "For": "for", - "While": "while", - "ClassDef": "class", - "Import": "import", - "From": "from", - "Try": "try", - "ExceptHandler": "except", - "With": "with", - "BoolOp": "and" if isinstance(node, ast.BoolOp) and node.op.__class__.__name__ == "And" else "or", - "Compare": "in", - "NamedExpr": "if", - } - - return keyword_map.get(node_type) - - def _get_visitor_type(self, node_type: type) -> Any | None: - """Get visitor method for node type.""" - visitor_name = f"visit_{node_type.__name__}" - return getattr(self, visitor_name, None) + def transpile(self, code: str) -> str: + """Parse Tamil code and generate Python.""" + tree = ast.parse(code) + return ast.unparse(tree) diff --git a/test.py b/test.py new file mode 100644 index 0000000..c855b37 --- /dev/null +++ b/test.py @@ -0,0 +1,6 @@ +x = 1 +y = 2 +if x > y: + print('x is greater') +else: + print('y is greater') \ No newline at end of file diff --git a/test.tampy b/test.tampy new file mode 100644 index 0000000..d6658b0 --- /dev/null +++ b/test.tampy @@ -0,0 +1,6 @@ +x = 1 +y = 2 +if x > y: + print("x is greater") +else: + print("y is greater") diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..e829d9e --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,42 @@ +"""Test CLI functionality.""" + +import os +import subprocess +import sys +from pathlib import Path + + +def test_build_command(): + """Test build command.""" + result = subprocess.run( + [sys.executable, "-m", "tampy", "build", "test.tampy"], + capture_output=True, + text=True, + env={**dict(os.environ), "PYTHONPATH": "src"} + ) + assert result.returncode == 0 + assert "Generated:" in result.stdout + + +def test_run_command(): + """Test run command.""" + result = subprocess.run( + [sys.executable, "-m", "tampy", "run", "test.tampy"], + capture_output=True, + text=True, + env={**dict(os.environ), "PYTHONPATH": "src"} + ) + assert result.returncode == 0 + assert "y is greater" in result.stdout + + +def test_file_extension_replacement(): + """Test that .tampy is replaced with .py.""" + result = subprocess.run( + [sys.executable, "-m", "tampy", "build", "test.tampy"], + capture_output=True, + text=True, + env={**dict(os.environ), "PYTHONPATH": "src"} + ) + assert result.returncode == 0 + assert Path("test.py").exists() diff --git a/tests/test_parser.py b/tests/test_parser.py deleted file mode 100644 index 02b99a5..0000000 --- a/tests/test_parser.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Test parser functionality.""" - -import ast -import pytest -from src.tampy.transpiler import TamilParser - - -def test_parse_simple_python(): - """Test parsing simple Python code.""" - code = "print('hello')" - keywords = {"print": "இருப்பு"} - - parser = TamilParser(keywords) - tree = parser.parse(code) - - assert isinstance(tree, ast.Module) - - -def test_parse_import_statement(): - """Test parsing import statement.""" - code = "import sys" - keywords = {"import": "மேற்கோள்கள்"} - - parser = TamilParser(keywords) - tree = parser.parse(code) - - assert isinstance(tree, ast.Module) - - -def test_parse_function_definition(): - """Test parsing function definition.""" - code = "def foo(): pass" - keywords = {"def": "வரையறை"} - - parser = TamilParser(keywords) - tree = parser.parse(code) - - assert isinstance(tree, ast.Module) - - -if __name__ == "__main__": - test_parse_simple_python() - test_parse_import_statement() - test_parse_function_definition() - print("All tests passed") diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py new file mode 100644 index 0000000..e94978f --- /dev/null +++ b/tests/test_transpiler.py @@ -0,0 +1,79 @@ +"""Test simple transpiler.""" + +import ast +import pytest +from src.tampy.transpiler import SimpleTranspiler + + +def test_simple_transpile(): + """Test basic transpilation.""" + transpiler = SimpleTranspiler() + code = "x = 1" + result = transpiler.transpile(code) + assert "x = 1" in result + + +def test_function_transpile(): + """Test function transpilation.""" + transpiler = SimpleTranspiler() + code = "def foo(): return 1" + result = transpiler.transpile(code) + assert "def foo()" in result + + +def test_if_transpile(): + """Test if statement transpilation.""" + transpiler = SimpleTranspiler() + code = "if True: pass" + result = transpiler.transpile(code) + assert "if True:" in result + + +def test_for_transpile(): + """Test for loop transpilation.""" + transpiler = SimpleTranspiler() + code = "for i in range(10): pass" + result = transpiler.transpile(code) + assert "for i in range(10):" in result + + +def test_class_transpile(): + """Test class transpilation.""" + transpiler = SimpleTranspiler() + code = "class Foo: pass" + result = transpiler.transpile(code) + assert "class Foo:" in result + + +def test_import_transpile(): + """Test import transpilation.""" + transpiler = SimpleTranspiler() + code = "import sys" + result = transpiler.transpile(code) + assert "import sys" in result + + +def test_roundtrip(): + """Test parsing and regenerating code.""" + transpiler = SimpleTranspiler() + + code = """ +x = 1 +y = 2 +if x > y: + print(x) +""" + result = transpiler.transpile(code) + assert "x = 1" in result + assert "if" in result + + +if __name__ == "__main__": + test_simple_transpile() + test_function_transpile() + test_if_transpile() + test_for_transpile() + test_class_transpile() + test_import_transpile() + test_roundtrip() + print("All tests passed") diff --git a/uv.lock b/uv.lock index e872565..7b9c4bf 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,8 @@ version = 1 revision = 3 -requires-python = ">=3.8" +requires-python = ">=3.9" [[package]] name = "tampy" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." }