├── .all-contributorsrc ├── .github ├── FUNDING.yml └── workflows │ └── compile.yml ├── .gitignore ├── LICENSE ├── README.md ├── rbxpy.spec └── src ├── .gitignore ├── README.md ├── binop.py ├── compop.py ├── config.py ├── const.py ├── context.py ├── lib.py ├── log.py ├── loopcounter.py ├── luau.py ├── nodevisitor.py ├── rbxpy.py ├── symbols.py ├── tokenend.py ├── translator.py └── unary.py /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "AsynchronousAI", 12 | "name": "aqzp", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/72946059?v=4", 14 | "profile": "https://github.com/AsynchronousAI", 15 | "contributions": [ 16 | "code" 17 | ] 18 | }, 19 | { 20 | "login": "tututuana", 21 | "name": "tututuana", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/51187395?v=4", 23 | "profile": "https://github.com/tututuana", 24 | "contributions": [ 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "BazirGames", 30 | "name": "BazirGames", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/49544193?v=4", 32 | "profile": "https://github.com/BazirGames", 33 | "contributions": [ 34 | "bug" 35 | ] 36 | }, 37 | { 38 | "login": "LawMixer", 39 | "name": "LawMixer", 40 | "avatar_url": "https://avatars.githubusercontent.com/u/53837083?v=4", 41 | "profile": "http://lawmixerscpf.tk/group", 42 | "contributions": [ 43 | "bug" 44 | ] 45 | }, 46 | { 47 | "login": "cataclysmic-dev", 48 | "name": "cataclysmic-dev", 49 | "avatar_url": "https://avatars.githubusercontent.com/u/141081747?v=4", 50 | "profile": "https://github.com/cataclysmic-dev", 51 | "contributions": [ 52 | "code" 53 | ] 54 | }, 55 | { 56 | "login": "luxkatana", 57 | "name": "luxkatana", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/57036931?v=4", 59 | "profile": "https://github.com/luxkatana", 60 | "contributions": [ 61 | "bug" 62 | ] 63 | } 64 | ], 65 | "contributorsPerLine": 7, 66 | "skipCi": true, 67 | "repoType": "github", 68 | "repoHost": "https://github.com", 69 | "projectName": "roblox-py", 70 | "projectOwner": "AsynchronousAI" 71 | } 72 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: AsynchronousAI 4 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Windows, macOS, and Ubuntu (Latest Build) 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [windows-latest, ubuntu-latest, macos-latest] 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.12' 23 | 24 | - name: Download RCC 25 | run: | 26 | git clone https://github.com/roblox-compilers/rcc 27 | cd rcc 28 | 29 | - name: Install dependencies 30 | run: | 31 | cd rcc 32 | python -m pip install --upgrade pip 33 | pip install -r requirements.txt 34 | 35 | - name: Run script 36 | run: | 37 | cd rcc 38 | mkdir ${{ github.workspace }}/build 39 | echo ${{ github.workspace }}/build | python rcc.py install rbxpy 40 | 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: ${{ matrix.os == 'windows-latest' && 'rbxpy-win' || matrix.os == 'ubuntu-latest' && 'rbxpy-linux' || 'rbxpy-macos' }} 46 | path: '${{ github.workspace }}/build/' 47 | 48 | #- name: Create Release 49 | # id: create_release 50 | # uses: actions/create-release@v1 51 | # env: 52 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | # with: 54 | # tag_name: ${{ github.ref }} 55 | # release_name: Release ${{ github.ref }} 56 | # draft: false 57 | # prerelease: false 58 | 59 | #- name: Upload release asset 60 | # uses: actions/upload-release-asset@v1 61 | # env: 62 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | # with: 64 | # upload_url: ${{ steps.latest_release.outputs.upload_url }} 65 | # asset_path: '${{ github.workspace }}/build/' 66 | # asset_name: ${{ runner.os == 'Windows' && 'rcc-win' || runner.os == 'Ubuntu' && 'rcc-ubuntu' || 'rcc-macos' }}' 67 | # asset_content_type: application/octet-stream 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | 4 | # Test 5 | test.py 6 | test.lua 7 | test.ipynb 8 | 9 | # Build 10 | /build 11 | /dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | 663 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 |
7 | 8 |
9 | 10 | 11 | ## roblox-py 12 | 13 | [**Docs**](https://docs.unexex.tech) **|** [**Devforum**]() **|** [**Github**](https://github.com/AsynchronousAI/roblox.pyc) **|** [**Discord**](https://discord.gg/3WCdXhwgfE) **|** [**Playground**](https://coolpro200021.pythonanywhere.com/) 14 | 15 | *** 16 | 17 | Python to Luau Compiler for Roblox. 18 | 19 | 20 | ## Sponsors 21 | 22 | 0 sadly 23 | 24 | ## Contributors 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
aqzp
aqzp

💻
tututuana
tututuana

💻
BazirGames
BazirGames

🐛
LawMixer
LawMixer

🐛
cataclysmic-dev
cataclysmic-dev

💻
luxkatana
luxkatana

🐛
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /rbxpy.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['src/rbxpy.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | ) 16 | pyz = PYZ(a.pure) 17 | 18 | exe = EXE( 19 | pyz, 20 | a.scripts, 21 | a.binaries, 22 | a.datas, 23 | [], 24 | name='rbxpy', 25 | debug=False, 26 | bootloader_ignore_signals=False, 27 | strip=False, 28 | upx=True, 29 | upx_exclude=[], 30 | runtime_tmpdir=None, 31 | console=True, 32 | disable_windowed_traceback=False, 33 | argv_emulation=False, 34 | target_arch=None, 35 | codesign_identity=None, 36 | entitlements_file=None, 37 | ) 38 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | test.py 2 | test.sh 3 | .mypy_cache 4 | .ruff_cache 5 | __pycache__ -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 |
7 | 8 |
9 | 10 | 11 | ## roblox-py 12 | 13 | [**Docs**](https://rcc-1.gitbook.io/roblox-py/) **|** [**Devforum**]() **|** [**Github**](https://github.com/AsynchronousAI/roblox.pyc) **|** [**Discord**](https://discord.gg/3WCdXhwgfE) **|** [**Playground**](https://roblox-py.aqzp.repl.co) 14 | 15 | *** 16 | 17 | Python to Luau Compiler for Roblox. 18 | 19 | 20 | ## Sponsors 21 | 22 | 0 sadly 23 | 24 | ## Contributors 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
aqzp
aqzp

💻
tututuana
tututuana

💻
BazirGames
BazirGames

🐛
LawMixer
LawMixer

🐛
cataclysmic-dev
cataclysmic-dev

💻
luxkatana
luxkatana

🐛
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /src/binop.py: -------------------------------------------------------------------------------- 1 | """Binary operation description""" 2 | 3 | import ast 4 | 5 | _DEFAULT_BIN_FORMAT = "{left} {operation} {right}" 6 | 7 | 8 | class BinaryOperationDesc: 9 | """Binary operation description""" 10 | 11 | OPERATION = { 12 | ast.Add: { 13 | "value": "+", 14 | "format": _DEFAULT_BIN_FORMAT, 15 | "depend": "", 16 | }, 17 | ast.Sub: { 18 | "value": "-", 19 | "format": _DEFAULT_BIN_FORMAT, 20 | "depend": "", 21 | }, 22 | ast.Mult: { 23 | "value": "*", 24 | "format": _DEFAULT_BIN_FORMAT, 25 | "depend": "", 26 | }, 27 | ast.Div: { 28 | "value": "/", 29 | "format": _DEFAULT_BIN_FORMAT, 30 | "depend": "", 31 | }, 32 | ast.Mod: { 33 | "value": "%", 34 | "format": _DEFAULT_BIN_FORMAT, 35 | "depend": "", 36 | }, 37 | ast.Pow: { 38 | "value": "^", 39 | "format": _DEFAULT_BIN_FORMAT, 40 | "depend": "", 41 | }, 42 | ast.FloorDiv: { 43 | "value": "/", 44 | "format": "math.floor({left} {operation} {right})", 45 | "depend": "", 46 | }, 47 | ast.LShift: { 48 | "value": "", 49 | "format": "bit32.lshift({left}, {right})", 50 | "depend": "", 51 | }, 52 | ast.RShift: { 53 | "value": "", 54 | "format": "bit32.rshift({left}, {right})", 55 | "depend": "", 56 | }, 57 | ast.BitOr: { 58 | "value": "", 59 | "format": "bit32.bor({left}, {right})", 60 | "depend": "", 61 | }, 62 | ast.BitAnd: { 63 | "value": "", 64 | "format": "bit32.band({left}, {right})", 65 | "depend": "", 66 | }, 67 | ast.BitXor: { 68 | "value": "", 69 | "format": "bit32.bxor({left}, {right})", 70 | "depend": "", 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /src/compop.py: -------------------------------------------------------------------------------- 1 | """Compare operation description""" 2 | 3 | import ast 4 | 5 | 6 | class CompareOperationDesc: 7 | """Compare operation description""" 8 | 9 | OPERATION = { 10 | ast.Eq: "==", 11 | ast.NotEq: "~=", 12 | ast.Lt: "<", 13 | ast.LtE: "<=", 14 | ast.Gt: ">", 15 | ast.GtE: ">=", 16 | ast.Is: "==", 17 | ast.IsNot: "~=", 18 | } 19 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | """Config""" 2 | 3 | import json 4 | from log import error 5 | 6 | 7 | class Config: 8 | """Translator config.""" 9 | 10 | def __init__(self, filename=None): 11 | self.data = { 12 | "class": { 13 | "return_at_the_end": False, 14 | }, 15 | "luau": None, 16 | } 17 | 18 | if filename is not None: 19 | self.load(filename) 20 | 21 | def load(self, filename): 22 | """Load config from the file""" 23 | try: 24 | with open(filename, "r") as stream: 25 | data = json.load(stream) 26 | self.data.update(data) 27 | except FileNotFoundError: 28 | pass # Use a default config if the file not found 29 | except json.decoder.JSONDecodeError: 30 | error("Config file is not a valid JSON file.") 31 | 32 | def __getitem__(self, key): 33 | """Get data values""" 34 | return self.data[key] 35 | -------------------------------------------------------------------------------- /src/const.py: -------------------------------------------------------------------------------- 1 | """Name constant description""" 2 | 3 | 4 | class NameConstantDesc: 5 | """Name constant description""" 6 | 7 | NAME = { 8 | None: "nil", 9 | True: "true", 10 | False: "false", 11 | } 12 | 13 | 14 | #### CONSTANTS #### 15 | VERSION = "3.27.111" 16 | TAB = "\t\b\b\b\b" 17 | 18 | """Header""" 19 | HEADER = f"--> generated by roblox-py v{VERSION}\n" 20 | PY_HEADER = f"## generated by roblox-py v{VERSION} ##\nfrom rbx import *\n" 21 | -------------------------------------------------------------------------------- /src/context.py: -------------------------------------------------------------------------------- 1 | """Class to store the python code context""" 2 | 3 | from tokenend import * 4 | from symbols import SymbolsStack 5 | 6 | 7 | class Context: 8 | """Class to store the python code context""" 9 | 10 | def __init__(self, values=None): 11 | values = ( 12 | values 13 | if values is not None 14 | else { 15 | "token_end_mode": TokenEndMode.LINE_FEED, 16 | "class_name": "", 17 | "locals": SymbolsStack(), 18 | "globals": SymbolsStack(), # Not working yet 19 | "loop_label_name": "", 20 | "docstring": False, 21 | } 22 | ) 23 | 24 | self.ctx_stack = [values] 25 | 26 | def last(self): 27 | """Return actual context state""" 28 | return self.ctx_stack[-1] 29 | 30 | def push(self, values): 31 | """Push new context state with new values""" 32 | value = self.ctx_stack[-1].copy() 33 | value.update(values) 34 | self.ctx_stack.append(value) 35 | 36 | def pop(self): 37 | """Pop last context state""" 38 | assert ( 39 | len(self.ctx_stack) > 1 40 | ), "Pop context failed. This is a last context in the stack." 41 | return self.ctx_stack.pop() 42 | -------------------------------------------------------------------------------- /src/lib.py: -------------------------------------------------------------------------------- 1 | # This file includes Lua snippets 2 | 3 | #checks needed: 721 4 | COMPLEX = """ 5 | local _cmplxMeta = { 6 | __index = function(n, index) 7 | if index == "real" then 8 | return n[1] 9 | elseif index == "imag" then 10 | return n[2] 11 | elseif index == "conjugate" then 12 | return function() return setmetatable({n[1], -n[2]}, getmetatable(n)) end 13 | end 14 | end, 15 | __newindex = function(n, index, value) 16 | if type(index) == "number" or type(index) == "string" then 17 | error(index .. " cannot be assigned to") 18 | else 19 | error("invalid argument #2 (number or string expected, got " .. typeof(index) .. ")") 20 | end 21 | end, 22 | __unm = function(n) -- Negates the number. 23 | return setmetatable({-n[1], -n[2]}, getmetatable(n)) 24 | end, 25 | __add = function(n1, n2) -- Adds two numbers. 26 | if (type(n1) == "number" or type(n2) == "number") or (type(n1) == "table" and type(n2) == "table" and getmetatable(n1) == getmetatable(n2)) then 27 | if type(n1) == "number" then 28 | n1 = {n1, 0} 29 | end 30 | if type(n2) == "number" then 31 | n2 = {n2, 0} 32 | end 33 | 34 | local t1, t2 35 | if math.abs(n1[1]) == math.huge and math.abs(n2[1]) == math.huge and -n1[1] == n2 then 36 | t1 = 0 37 | else 38 | t1 = n1[1] + n2[1] 39 | end 40 | if math.abs(n1[2]) == math.huge and math.abs(n2[2]) == math.huge and -n1[2] == n2 then 41 | t2 = 0 42 | else 43 | t2 = n1[2] + n2[2] 44 | end 45 | 46 | return setmetatable({t1, t2}, getmetatable(n1) or getmetatable(n2)) 47 | else 48 | if type(n1) == type(n2) then 49 | error("attempt to perform arithmetic (add) on " .. typeof(n1)) 50 | else 51 | error("attempt to perform arithmetic (add) on " .. typeof(n1) .. "and" .. typeof(n2)) 52 | end 53 | end 54 | end, 55 | __sub = function(n1, n2) -- Subtracts the first number with the second one. 56 | if (type(n1) == "number" or type(n2) == "number") or (type(n1) == "table" and type(n2) == "table" and getmetatable(n1) == getmetatable(n2)) then 57 | if type(n1) == "number" then 58 | n1 = {n1, 0} 59 | end 60 | if type(n2) == "number" then 61 | n2 = {n2, 0} 62 | end 63 | 64 | local t1, t2 65 | if math.abs(n1[1]) == math.huge and math.abs(n2[1]) == math.huge and n1[1] == n2[1] then 66 | t1 = 0 67 | else 68 | t1 = n1[1] - n2[1] 69 | end 70 | if math.abs(n1[2]) == math.huge and math.abs(n2[2]) == math.huge and n1[2] == n2[2] then 71 | t2 = 0 72 | else 73 | t2 = n1[2] - n2[2] 74 | end 75 | 76 | return setmetatable({t1, t2}, getmetatable(n1) or getmetatable(n2)) 77 | else 78 | if type(n1) == type(n2) then 79 | error("attempt to perform arithmetic (sub) on " .. typeof(n1)) 80 | else 81 | error("attempt to perform arithmetic (sub) on " .. typeof(n1) .. "and" .. typeof(n2)) 82 | end 83 | end 84 | end, 85 | __mul = function(n1, n2) -- Multiply two numbers. 86 | if (type(n1) == "number" or type(n2) == "number") or (type(n1) == "table" and type(n2) == "table" and getmetatable(n1) == getmetatable(n2)) then 87 | if type(n1) == "number" then 88 | n1 = {n1, 0} 89 | end 90 | if type(n2) == "number" then 91 | n2 = {n2, 0} 92 | end 93 | 94 | local t1, t2 = n1[1] * n2[1] - n1[2] * n2[2], n1[1] * n2[2] + n2[1] * n1[2] 95 | if string.find(tostring(t1), "nan") and not string.find(tostring(n1[1]), "nan") and not string.find(tostring(n1[2]), "nan") and not string.find(tostring(n2[1]), "nan") and not string.find(tostring(n2[2]), "nan") then 96 | t1 = 0 97 | end 98 | if string.find(tostring(t2), "nan") and not string.find(tostring(n1[1]), "nan") and not string.find(tostring(n1[2]), "nan") and not string.find(tostring(n2[1]), "nan") and not string.find(tostring(n2[2]), "nan") then 99 | t2 = 0 100 | end 101 | 102 | return setmetatable({t1, t2}, getmetatable(n1) or getmetatable(n2)) 103 | else 104 | if type(n1) == type(n2) then 105 | error("attempt to perform arithmetic (mul) on " .. typeof(n1)) 106 | else 107 | error("attempt to perform arithmetic (mul) on " .. typeof(n1) .. "and" .. typeof(n2)) 108 | end 109 | end 110 | end, 111 | __div = function(n1, n2) -- Divides two numbers. 112 | if (type(n1) == "number" or type(n2) == "number") or (type(n1) == "table" and type(n2) == "table" and getmetatable(n1) == getmetatable(n2)) then 113 | if type(n1) == "number" then 114 | n1 = {n1, 0} 115 | end 116 | if type(n2) == "number" then 117 | n2 = {n2, 0} 118 | end 119 | 120 | local t1, t2 121 | if (n1[1] == 0 and n1[2] == 0 and n2[1] == 0 and n2[2] == 0) or ((math.abs(n1[1]) == math.huge or math.abs(n1[2]) == math.huge) and (math.abs(n2[1]) == math.huge or math.abs(n2[2]) == math.huge)) then 122 | t1, t2 = tonumber("-nan(ind)"), tonumber("-nan(ind)") 123 | elseif math.abs(n1[1]) == math.huge or math.abs(n1[2]) == math.huge then 124 | t1 = (setmetatable({n1[1], 0}, getmetatable(n1) or getmetatable(n2)) * setmetatable({n2[1], 0}, getmetatable(n1) or getmetatable(n2)) + setmetatable({n1[2], 0}, getmetatable(n1) or getmetatable(n2)) * setmetatable({n2[2], 0}, getmetatable(n1) or getmetatable(n2)))[1] / (n2[1] ^ 2 + n2[2] ^ 2) 125 | t2 = (setmetatable({n1[2], 0}, getmetatable(n1) or getmetatable(n2)) * setmetatable({n2[1], 0}, getmetatable(n1) or getmetatable(n2)) - setmetatable({n1[1], 0}, getmetatable(n1) or getmetatable(n2)) * setmetatable({n2[2], 0}, getmetatable(n1) or getmetatable(n2)))[1] / (n2[1] ^ 2 + n2[2] ^ 2) 126 | elseif math.abs(n2[1]) == math.huge or math.abs(n2[2]) == math.huge then 127 | t1, t2 = 0, 0 128 | else 129 | t1 = (n1[1] * n2[1] + n1[2] * n2[2]) / (n2[1] ^ 2 + n2[2] ^ 2) 130 | t2 = (n1[2] * n2[1] - n1[1] * n2[2]) / (n2[1] ^ 2 + n2[2] ^ 2) 131 | end 132 | 133 | if tostring(t1) == "nan" and (tostring(n1[1]) ~= "nan" or tostring(n2[1]) ~= "nan") then 134 | t1 = 0 135 | end 136 | if tostring(t2) == "nan" and (tostring(n1[2]) ~= "nan" or tostring(n2[2]) ~= "nan") then 137 | t2 = 0 138 | end 139 | 140 | return setmetatable({t1, t2}, getmetatable(n1) or getmetatable(n2)) 141 | else 142 | if type(n1) == type(n2) then 143 | error("attempt to perform arithmetic (div) on " .. typeof(n1)) 144 | else 145 | error("attempt to perform arithmetic (div) on " .. typeof(n1) .. "and" .. typeof(n2)) 146 | end 147 | end 148 | end, 149 | __pow = function(n1, n2) 150 | local function __asin(x) 151 | return 1 / math.sin(x) 152 | end 153 | 154 | local function __acos(x) 155 | return 1 / math.cos(x) 156 | end 157 | 158 | local function __atan(x) 159 | return 1 / math.tan(x) 160 | end 161 | 162 | if (type(n1) == "number" or type(n2) == "number") or (type(n1) == "table" and type(n2) == "table" and getmetatable(n1) == getmetatable(n2)) then 163 | if type(n1) == "number" then 164 | n1 = {n1, 0} 165 | end 166 | if type(n2) == "number" then 167 | n2 = {n2, 0} 168 | end 169 | 170 | local z, ex, i 171 | local norm = math.sqrt(n1[1] ^ 2 + n1[2] ^ 2) 172 | if norm ~= 0 then 173 | z = setmetatable({math.log(norm), math.atan2(n1[2], n1[1])}, getmetatable(n1)) 174 | ex = n2 * z 175 | norm = math.exp(ex[1]) 176 | i = ex[2] 177 | else 178 | norm = 0 179 | i = 0 180 | end 181 | 182 | return setmetatable({norm * math.cos(i), norm * math.sin(i)}, getmetatable(n1) or getmetatable(n2)) 183 | else 184 | if type(n1) == type(n2) then 185 | error("attempt to perform arithmetic (pow) on " .. typeof(n1)) 186 | else 187 | error("attempt to perform arithmetic (pow) on " .. typeof(n1) .. "and" .. typeof(n2)) 188 | end 189 | end 190 | end, 191 | __eq = function(n1, n2) -- Tests for equality. 192 | if (type(n1) == "number" or type(n2) == "number") or (type(n1) == "table" and type(n2) == "table" and getmetatable(n1) == getmetatable(n2)) then 193 | if type(n1) == "number" then 194 | n1 = {n1, 0} 195 | end 196 | if type(n2) == "number" then 197 | n2 = {n2, 0} 198 | end 199 | 200 | return (n1[1] == n2[1]) and (n1[2] == n2[2]) 201 | else 202 | return false 203 | end 204 | end, 205 | __tostring = function(n) -- Converts CmplxNumber datatype to a string. 206 | local t1, t2 207 | if n[1] == 0 and n[2] == 0 then 208 | t1 = "0" 209 | elseif n[1] == 0 then 210 | t1 = "" 211 | else 212 | t1 = tostring(n[1]) 213 | end 214 | if n[2] == 0 then 215 | t2 = "" 216 | elseif n[2] == 1 then 217 | if n[1] == 0 then 218 | t2 = "i" 219 | else 220 | t2 = "+i" 221 | end 222 | elseif n[2] == -1 then 223 | t2 = "-i" 224 | else 225 | if string.find(tostring(n[2]), "e") or string.find(tostring(n[2]), "inf") or string.find(n[2], "nan") then 226 | t2 = tostring(n[2]) .. "*i" 227 | else 228 | t2 = tostring(n[2]) .. "i" 229 | end 230 | if n[1] ~= 0 and string.sub(tostring(n[2]), 1, 1) ~= "-" then 231 | t2 = "+" .. t2 232 | end 233 | end 234 | 235 | return t1 .. t2 236 | end, 237 | __type = function(n) 238 | return typeof(n) 239 | end, 240 | } 241 | 242 | local _cmplxFactory = { 243 | __call = function(t, re, im) 244 | re = type(re) == "number" and re or 0 245 | im = type(im) == "number" and im or 0 246 | local cmplx = { re, im } 247 | setmetatable(cmplx, _cmplxMeta) 248 | return cmplx 249 | end 250 | } 251 | 252 | complex = setmetatable({}, _cmplxFactory) 253 | """ 254 | 255 | struct = """ 256 | local struct = {} 257 | 258 | function struct.pack(format: string, size: number?, ...) 259 | local predictedSize = size or 2048 260 | local vars = {...} 261 | 262 | local stream = buffer.create(predictedSize) 263 | local endianness = true 264 | 265 | local ind = 0 266 | 267 | for i = 1, format:len() do 268 | local opt = (format:sub(i, i) :: string) 269 | 270 | if opt == '<' then 271 | endianness = true 272 | elseif opt == '>' then 273 | endianness = false 274 | elseif opt:find('[bBhHiIlL]') then 275 | local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 276 | local val = (tonumber(table.remove(vars, 1)) :: number) 277 | 278 | local bytes = {} 279 | for j = 1, n do 280 | table.insert(bytes, string.char(val % (2 ^ 8))) 281 | val = math.floor(val / (2 ^ 8)) 282 | end 283 | local data 284 | if not endianness then 285 | data = string.reverse(table.concat(bytes)) 286 | else 287 | data = table.concat(bytes) 288 | end 289 | buffer.writestring(stream, ind, data) 290 | ind += #data 291 | elseif opt:find('[fd]') then 292 | local val = (tonumber(table.remove(vars, 1)) :: number) 293 | local sign = 0 294 | 295 | if val < 0 then 296 | sign = 1 297 | val = (-val :: number) 298 | end 299 | 300 | local mantissa, exponent = math.frexp(val) 301 | if val == 0 then 302 | mantissa = 0 303 | exponent = 0 304 | else 305 | mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24) 306 | exponent = exponent + ((opt == 'd') and 1022 or 126) 307 | end 308 | 309 | local bytes = {} 310 | if opt == 'd' then 311 | val = mantissa 312 | for i = 1, 6 do 313 | table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) 314 | val = math.floor(val / (2 ^ 8)) 315 | end 316 | else 317 | table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8))) 318 | val = math.floor(mantissa / (2 ^ 8)) 319 | table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) 320 | val = math.floor(val / (2 ^ 8)) 321 | end 322 | 323 | table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8))) 324 | val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8)) 325 | table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8))) 326 | val = math.floor((sign * 128 + val) / (2 ^ 8)) 327 | 328 | local data 329 | if not endianness then 330 | data = string.reverse(table.concat(bytes)) 331 | else 332 | data = table.concat(bytes) 333 | end 334 | buffer.writestring(stream, ind, data) 335 | ind += #data 336 | elseif opt == 's' then 337 | local data = tostring(table.remove(vars, 1)) 338 | 339 | buffer.writestring(stream, ind, data) 340 | ind += #data 341 | buffer.writestring(stream, ind, string.char(0)) 342 | ind += 1 343 | elseif opt == 'c' then 344 | local n = format:sub(i + 1):match('%d+') 345 | local str = tostring(table.remove(vars, 1)) 346 | local len = (tonumber(n) :: number) 347 | if len <= 0 then 348 | len = str:len() 349 | end 350 | if len - str:len() > 0 then 351 | str = str .. string.rep(' ', len - str:len()) 352 | end 353 | local data = str:sub(1, len) 354 | buffer.writestring(stream, ind, data) 355 | ind += #data 356 | i = i + (n :: string):len() 357 | end 358 | end 359 | 360 | return buffer.tostring(stream) 361 | end 362 | 363 | function struct.unpack(format: string, stream: string, pos: number?) 364 | local stream = buffer.fromstring(stream) 365 | local vars = {} 366 | local iterator = pos or 1 367 | local endianness = true 368 | 369 | for i = 1, format:len() do 370 | local opt: string = format:sub(i, i) 371 | 372 | if opt == '<' then 373 | endianness = true 374 | elseif opt == '>' then 375 | endianness = false 376 | elseif opt:find('[bBhHiIlL]') then 377 | local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 378 | local signed = opt:lower() == opt 379 | 380 | local val = 0 381 | for j = 1, n do 382 | local byte = buffer.readi8(stream, iterator) 383 | if endianness then 384 | val = val + byte * (2 ^ ((j - 1) * 8)) 385 | else 386 | val = val + byte * (2 ^ ((n - j) * 8)) 387 | end 388 | iterator = iterator + 1 389 | end 390 | 391 | if signed and val >= 2 ^ (n * 8 - 1) then 392 | val = val - 2 ^ (n * 8) 393 | end 394 | 395 | table.insert(vars, math.floor(val)) 396 | elseif opt:find('[fd]') then 397 | local n = if (opt == 'd') then 8 else 4 398 | local x = --[[stream:sub(iterator, iterator + n - 1)]] buffer.readstring(stream, iterator, n) 399 | iterator = iterator + n 400 | 401 | if not endianness then 402 | x = string.reverse(x) 403 | end 404 | 405 | local sign = 1 406 | local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128) 407 | for i = n - 2, 1, -1 do 408 | mantissa = mantissa * (2 ^ 8) + string.byte(x, i) 409 | end 410 | 411 | if string.byte(x, n) > 127 then 412 | sign = -1 413 | end 414 | 415 | local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) + math.floor(string.byte(x, n - 1) / ((opt == 'd') and 16 or 128)) 416 | if exponent == 0 then 417 | table.insert(vars, 0.0) 418 | else 419 | mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign 420 | table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127))) 421 | end 422 | elseif opt == 's' then 423 | local bytes = {} 424 | for j = iterator, buffer.len(stream) do 425 | local val = buffer.readstring(stream, j, 1) 426 | if val == string.char(0) or val == '' then 427 | break 428 | end 429 | 430 | table.insert(bytes, val) 431 | end 432 | 433 | local str = table.concat(bytes) 434 | iterator = iterator + str:len() + 1 435 | table.insert(vars, str) 436 | elseif opt == 'c' then 437 | local val = buffer.readstring(stream, i + 1, 1) 438 | local n: string = (val:match('%d+') :: string) 439 | local len = (tonumber(n) :: number) 440 | if len <= 0 then 441 | len = (table.remove(vars) :: number) 442 | end 443 | 444 | val = buffer.readstring(stream, iterator, len) 445 | table.insert(vars, val) 446 | iterator = iterator + len 447 | i = i + n:len() 448 | end 449 | end 450 | 451 | return unpack(vars) 452 | end 453 | """ 454 | 455 | JSON = """local json = {} 456 | function json.loads(t) 457 | return game.HttpService:JSONEncode(t) 458 | end 459 | function json.dumps(t) 460 | return game.HttpService:JSONDecode(t) 461 | end 462 | function fault() 463 | error("[roblox-py] (json-lib) Outputting to files is not supported.") 464 | end 465 | json.load, json.dump = fault, fault""" 466 | 467 | # libs = ["json"] 468 | 469 | errs = [ 470 | "ValueError", 471 | "TypeError", 472 | "AttributeError", 473 | "IndexError", 474 | "KeyError", 475 | "ZeroDivisionError", 476 | "AssertionError", 477 | "NotImplementedError", 478 | "RuntimeError", 479 | "NameError", 480 | "SyntaxError", 481 | "IndentationError", 482 | "TabError", 483 | "ImportError", 484 | "ModuleNotFoundError", 485 | "OSError", 486 | "FileNotFoundError", 487 | "PermissionError", 488 | "EOFError", 489 | "ConnectionError", 490 | "TimeoutError", 491 | "UnboundLocalError", 492 | "RecursionError", 493 | "MemoryError", 494 | "OverflowError", 495 | "FloatingPointError", 496 | "ArithmeticError", 497 | "ReferenceError", 498 | "SystemError", 499 | "SystemExit", 500 | "GeneratorExit", 501 | "KeyboardInterrupt", 502 | "StopIteration", 503 | "Exception", 504 | "BaseException", 505 | "Error", 506 | ] 507 | libs = [ 508 | "complex", 509 | "class", 510 | "dict", 511 | "__name__", 512 | "range", 513 | "len", 514 | "abs", 515 | "str", 516 | "int", 517 | "sum", 518 | "max", 519 | "min", 520 | "reversed", 521 | "split", 522 | "round", 523 | "all", 524 | "any", 525 | "ord", 526 | "chr", 527 | "callable", 528 | "float", 529 | "super", 530 | "format", 531 | "hex", 532 | "id", 533 | "map", 534 | "bool", 535 | "divmod", 536 | "slice", 537 | "anext", 538 | "ascii", 539 | "dir", 540 | "getattr", 541 | "globals", 542 | "hasattr", 543 | "isinstance", 544 | "issubclass", 545 | "iter", 546 | "locals", 547 | "oct", 548 | "pow", 549 | "eval", 550 | "exec", 551 | "filter", 552 | "frozenset", 553 | "aiter", 554 | "bin", 555 | "deltaattr", 556 | "enumerate", 557 | "bytearray", 558 | "bytes", 559 | "compile", 560 | "help", 561 | "memoryview", 562 | "repr", 563 | "sorted", 564 | "vars", 565 | ] 566 | 567 | DEPENDENCY = """\n\n--> imports 568 | py = _G.rbxpy or require(game.ReplicatedStorage.Packages.pyruntime) 569 | if game.ReplicatedStorage:FindFirstChild("Packages") and game.ReplicatedStorage.Packages:FindFirstChild("rcclib") then 570 | rcc = _G.rcc or require(game.ReplicatedStorage.Packages.rcclib) 571 | else 572 | rcc = { } 573 | setmetatable(rcc, {__index = function(_, index) return function()end end}) 574 | end 575 | """ 576 | 577 | FN = """\n\nif game then 578 | __name__ = (if script:IsA("BaseScript") then "__main__" else script.Name) 579 | else 580 | __name__ = nil 581 | end 582 | range = function(s, e, f) -- range() 583 | local tb = {} 584 | local a = 0 585 | local b = 0 586 | local c = 0 587 | if not e then a=1 else a=s end 588 | if not e then b=s else b=e end 589 | if not f then c=1 else c=f end 590 | for i = a, b, c do 591 | tb[#tb+1] = i 592 | end 593 | return tb 594 | end 595 | len = function(x) return #x end -- len() 596 | abs = math.abs -- abs() 597 | str = tostring -- str() 598 | int = tonumber -- int() 599 | sum = function(tbl) --sum() 600 | local total = 0 601 | for _, v in ipairs(tbl) do 602 | total = total + v 603 | end 604 | return total 605 | end 606 | max = function(tbl) --max() 607 | local maxValue = -math.huge 608 | for _, v in ipairs(tbl) do 609 | if v > maxValue then 610 | maxValue = v 611 | end 612 | end 613 | return maxValue 614 | end 615 | min = function(tbl) --min() 616 | local minValue = math.huge 617 | for _, v in ipairs(tbl) do 618 | if v < minValue then 619 | minValue = v 620 | end 621 | end 622 | return minValue 623 | end 624 | reversed = function(seq) -- reversed() 625 | local reversedSeq = {} 626 | local length = #seq 627 | for i = length, 1, -1 do 628 | reversedSeq[length - i + 1] = seq[i] 629 | end 630 | return reversedSeq 631 | end 632 | split = function(str, sep) -- split 633 | local substrings = {} 634 | local pattern = string.format("([^%s]+)",sep or "%s") 635 | for substring in string.gmatch(str, pattern) do 636 | table.insert(substrings, substring) 637 | end 638 | return substrings 639 | end 640 | round = math.round -- round() 641 | all = function (iter) -- all() 642 | for i, v in iter do if not v then return false end end 643 | 644 | return true 645 | end 646 | any = function (iter) -- any() 647 | for i, v in iter do 648 | if v then return true end 649 | end 650 | return false 651 | end 652 | ord = string.byte -- ord 653 | chr = string.char -- chr 654 | callable = function(fun) -- callable() 655 | if rawget(fun) ~= fun then warn("At the momement Roblox.py's function callable() does not fully support metatables.") end 656 | return typeof(rawget(fun)) == "function" 657 | end 658 | float = tonumber -- float() 659 | super = function() 660 | error("roblox-pyc does not has a Lua implementation of the function `super`. Use `self` instead") 661 | end 662 | format = function(format, ...) -- format 663 | local args = {...} 664 | local num_args = select("#", ...) 665 | 666 | local formatted_string = string.gsub(format, "{(%d+)}", function(index) 667 | index = tonumber(index) 668 | if index >= 1 and index <= num_args then 669 | return tostring(args[index]) 670 | else 671 | return "{" .. index .. "}" 672 | end 673 | end) 674 | 675 | return formatted_string 676 | end 677 | hex = function (value) -- hex 678 | return string.format("%x", value) 679 | end 680 | id = function (obj) -- id 681 | return print(tostring({obj}):gsub("table: ", ""):split(" ")[1]) 682 | end 683 | map = function (func, ...) --map 684 | local args = {...} 685 | local result = {} 686 | local num_args = select("#", ...) 687 | 688 | local shortest_length = math.huge 689 | for i = 1, num_args do 690 | local arg = args[i] 691 | local arg_length = #arg 692 | if arg_length < shortest_length then 693 | shortest_length = arg_length 694 | end 695 | end 696 | 697 | for i = 1, shortest_length do 698 | local mapped_args = {} 699 | for j = 1, num_args do 700 | local arg = args[j] 701 | table.insert(mapped_args, arg[i]) 702 | end 703 | table.insert(result, func(unpack(mapped_args))) 704 | end 705 | 706 | return result 707 | end 708 | bool = function(x) -- bool 709 | if x == false or x == nil or x == 0 then 710 | return false 711 | end 712 | 713 | if typeof(x) == "table" then 714 | if x._is_list or x._is_dict then 715 | return next(x._data) ~= nil 716 | end 717 | end 718 | 719 | return true 720 | end 721 | divmod = function(a, b) -- divmod 722 | local res = { math.floor(a / b), math.fmod(a, b) } 723 | return unpack(res) 724 | end 725 | slice = function (seq, start, stop, step) 726 | local sliced = {} 727 | local len = #seq 728 | start = start or 1 729 | stop = stop or len 730 | step = step or 1 731 | if start < 0 then 732 | start = len + start + 1 733 | end 734 | if stop < 0 then 735 | stop = len + stop + 1 736 | end 737 | for i = start, stop - 1, step do 738 | table.insert(sliced, seq[i]) 739 | end 740 | return sliced 741 | end 742 | anext = function (iterator) -- anext 743 | local status, value = pcall(iterator) 744 | if status then 745 | return value 746 | end 747 | end 748 | ascii = function (obj) -- ascii 749 | return string.format("%q", tostring(obj)) 750 | end 751 | dir = function (obj) -- dir 752 | local result = {} 753 | for key, _ in pairs(obj) do 754 | table.insert(result, key) 755 | end 756 | return result 757 | end 758 | getattr = function (obj, name, default) -- getattr 759 | local value = obj[name] 760 | if value == nil then 761 | return default 762 | end 763 | return value 764 | end 765 | globals = function () -- globals 766 | return _G 767 | end 768 | hasattr = function (obj, name) --hasattr 769 | return obj[name] ~= nil 770 | end 771 | isinstance = function (obj, class) -- isinstance 772 | return type(obj) == class 773 | end 774 | issubclass = function (cls, classinfo) -- issubclass 775 | local mt = getmetatable(cls) 776 | while mt do 777 | if mt.__index == classinfo then 778 | return true 779 | end 780 | mt = getmetatable(mt.__index) 781 | end 782 | return false 783 | end 784 | iter = function (obj) -- iter 785 | if type(obj) == "table" and obj.__iter__ ~= nil then 786 | return obj.__iter__ 787 | end 788 | return nil 789 | end 790 | locals = function () -- locals 791 | return _G 792 | end 793 | oct = function (num) --oct 794 | return string.format("%o", num) 795 | end 796 | pow = function (base, exponent, modulo) --pow 797 | if modulo ~= nil then 798 | return math.pow(base, exponent) % modulo 799 | else 800 | return base ^ exponent 801 | end 802 | end 803 | eval = function (expr, env) 804 | return loadstring(expr)() 805 | end 806 | exec = loadstring 807 | filter = function (predicate, iterable) 808 | local result = {} 809 | for _, value in ipairs(iterable) do 810 | if predicate(value) then 811 | table.insert(result, value) 812 | end 813 | end 814 | return result 815 | end 816 | frozenset = function (...) 817 | local elements = {...} 818 | local frozenSet = {} 819 | for _, element in ipairs(elements) do 820 | frozenSet[element] = true 821 | end 822 | return frozenSet 823 | end 824 | aiter = function (iterable) -- aiter 825 | return pairs(iterable) 826 | end 827 | bin = function(num: number) 828 | local bits = {} 829 | repeat 830 | table.insert(bits, 1, num % 2) 831 | num = math.floor(num / 2) 832 | until num == 0 833 | return "0b" .. table.concat(bits) 834 | end 835 | 836 | deltaattr = function (object, attribute) -- delattr 837 | object[attribute] = nil 838 | end 839 | 840 | enumerate = function (t) 841 | local i = 0 842 | return function() 843 | i = i + 1 844 | local value = t(nil, i) 845 | if value ~= nil then 846 | return i, value 847 | end 848 | end 849 | end 850 | 851 | bytearray = function (arg) -- bytearray 852 | if type(arg) == "string" then 853 | local bytes = {} 854 | for i = 1, #arg do 855 | table.insert(bytes, string.byte(arg, i)) 856 | end 857 | return bytes 858 | elseif type(arg) == "number" then 859 | local bytes = {} 860 | while arg > 0 do 861 | table.insert(bytes, 1, arg % 256) 862 | arg = math.floor(arg / 256) 863 | end 864 | return bytes 865 | elseif type(arg) == "table" then 866 | return arg -- Assuming it's already a bytearray table 867 | else 868 | error("Invalid argument type for bytearray()") 869 | end 870 | end 871 | bytes = function (arg) -- bytes 872 | if type(arg) == "string" then 873 | local bytes = {} 874 | for i = 1, #arg do 875 | table.insert(bytes, string.byte(arg, i)) 876 | end 877 | return bytes 878 | elseif type(arg) == "table" then 879 | return arg -- Assuming it's already a bytes table 880 | else 881 | error("Invalid argument type for bytes()") 882 | end 883 | end 884 | compile = loadstring 885 | help = function (object) -- help 886 | print("Help for object:", object) 887 | print("Type:", type(object)) 888 | print("Learn more in the official roblox documentation!") 889 | end 890 | memoryview = function (object) -- memoryview 891 | if type(object) == "table" then 892 | local buffer = table.concat(object) 893 | return { buffer = buffer, itemsize = 1 } 894 | else 895 | error("Invalid argument type for memoryview()") 896 | end 897 | end 898 | repr = function (object) -- repr 899 | return tostring(object) 900 | end 901 | sorted = function (iterable, cmp, key, reverse) -- sorted 902 | local sortedTable = {} 903 | for key, value in pairs(iterable) do 904 | table.insert(sortedTable, { key = key, value = value }) 905 | end 906 | table.sort(sortedTable, function(a, b) 907 | -- Compare logic based on cmp, key, reverse parameters 908 | return a.key < b.key 909 | end) 910 | local i = 0 911 | return function() 912 | i = i + 1 913 | local entry = sortedTable[i] 914 | if entry then 915 | return entry.key, entry.value 916 | end 917 | end 918 | end 919 | vars = function (object) -- vars 920 | local attributes = {} 921 | for key, value in pairs(object) do 922 | attributes[key] = value 923 | end 924 | return attributes 925 | end""" 926 | 927 | CLASS = """\n\nfunction class(class_init, bases) 928 | bases = bases or {} 929 | 930 | local c = {} 931 | 932 | for _, base in ipairs(bases) do 933 | for k, v in pairs(base) do 934 | c[k] = v 935 | end 936 | end 937 | 938 | c._bases = bases 939 | 940 | c = class_init(c) 941 | 942 | local mt = getmetatable(c) or {} 943 | mt.__call = function(_, ...) 944 | local object = {} 945 | 946 | setmetatable(object, { 947 | __index = function(tbl, idx) 948 | local method = c[idx] 949 | if typeof(method) == "function" then 950 | return function(...) 951 | return c[idx](object, ...) 952 | end 953 | end 954 | 955 | return method 956 | end, 957 | }) 958 | 959 | if typeof(object.__init__) == "function" then 960 | object.__init__(...) 961 | end 962 | 963 | meta = { 964 | __add = object.__add__, 965 | __sub = object.__sub__, 966 | __div = object.__div__, 967 | __unm = object.__unm, 968 | } 969 | 970 | 971 | return setmetatable(object, meta) 972 | end 973 | 974 | setmetatable(c, mt) 975 | 976 | return c 977 | end""" 978 | DICT = """\n\nfunction dict(t) 979 | local result = {} 980 | 981 | result._is_dict = true 982 | 983 | result._data = {} 984 | for k, v in pairs(t) do 985 | result._data[k] = v 986 | end 987 | 988 | local methods = {} 989 | 990 | local key_index = nil 991 | 992 | methods.clear = function() 993 | result._data = {} 994 | end 995 | 996 | methods.copy = function() 997 | return dict(result._data) 998 | end 999 | 1000 | methods.get = function(key, default) 1001 | default = default or nil 1002 | if result._data[key] == nil then 1003 | return default 1004 | end 1005 | 1006 | return result._data[key] 1007 | end 1008 | 1009 | methods.items = function() 1010 | return pairs(result._data) 1011 | end 1012 | 1013 | methods.keys = function() 1014 | return function(self, idx, _) 1015 | if idx == nil and key_index ~= nil then 1016 | key_index = nil 1017 | end 1018 | 1019 | key_index, _ = next(result._data, key_index) 1020 | return key_index 1021 | end 1022 | end 1023 | 1024 | methods.pop = function(key, default) 1025 | default = default or nil 1026 | if result._data[key] ~= nil then 1027 | local value = result._data[key] 1028 | result._data[key] = nil 1029 | return key, value 1030 | end 1031 | 1032 | return key, default 1033 | end 1034 | 1035 | methods.popitem = function() 1036 | local key, value = next(result._data) 1037 | if key ~= nil then 1038 | result._data[key] = nil 1039 | end 1040 | 1041 | return key, value 1042 | end 1043 | 1044 | methods.setdefault = function(key, default) 1045 | if result._data[key] == nil then 1046 | result._data[key] = default 1047 | end 1048 | 1049 | return result._data[key] 1050 | end 1051 | 1052 | methods.update = function(t) 1053 | assert(t._is_dict) 1054 | 1055 | for k, v in t.items() do 1056 | result._data[k] = v 1057 | end 1058 | end 1059 | 1060 | methods.values = function() 1061 | return function(self, idx, _) 1062 | if idx == nil and key_index ~= nil then 1063 | key_index = nil 1064 | end 1065 | 1066 | local key_index, value = next(result._data, key_index) 1067 | return value 1068 | end 1069 | end 1070 | 1071 | setmetatable(result, { 1072 | __index = function(self, index) 1073 | if result._data[index] ~= nil then 1074 | return result._data[index] 1075 | end 1076 | return methods[index] 1077 | end, 1078 | __newindex = function(self, index, value) 1079 | result._data[index] = value 1080 | end, 1081 | __call = function(self, _, idx) 1082 | if idx == nil and key_index ~= nil then 1083 | key_index = nil 1084 | end 1085 | 1086 | key_index, _ = next(result._data, key_index) 1087 | 1088 | return key_index 1089 | end, 1090 | }) 1091 | 1092 | return result 1093 | end""" 1094 | 1095 | GENERATOR = """\n\nlocal __PY_GENERATORS = {} 1096 | function yieldGenerator(name, value) 1097 | if not __PY_GENERATORS[name] then 1098 | error("[roblox-py]: Attempt to yield a generator that is not being iterated over.") 1099 | end 1100 | table.insert(__PY_GENERATORS[name], value) 1101 | end 1102 | function generatorLoop(name, func, ...) 1103 | local args = {...} 1104 | if not __PY_GENERATORS[name] then 1105 | __PY_GENERATORS[name] = {} 1106 | end 1107 | return function(loopFunc) 1108 | local DONE = false 1109 | local currentSize = 0 1110 | task.spawn(function() 1111 | func(unpack(args)) 1112 | end) 1113 | while task.wait() do 1114 | if #__PY_GENERATORS[name] > currentSize then 1115 | for i = currentSize + 1, #__PY_GENERATORS[name] do 1116 | if __PY_GENERATORS[name][i] == "__END" then break end 1117 | 1118 | loopFunc(__PY_GENERATORS[name][i]) 1119 | end 1120 | currentSize = #__PY_GENERATORS[name] 1121 | end 1122 | end 1123 | end 1124 | end 1125 | """ 1126 | -------------------------------------------------------------------------------- /src/log.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def error(msg): 5 | print("\033[91;1merror\033[0m \033[90mPY roblox-py:\033[0m " + msg) 6 | sys.exit(1) 7 | 8 | 9 | def warn(msg): 10 | sys.stderr.write( 11 | "\033[1;33m" 12 | + "warning: " 13 | + "\033[0m" 14 | + "\033[90mPY roblox-py:\033[0m " 15 | + msg 16 | + "\n" 17 | ) 18 | 19 | 20 | def info(msg): 21 | sys.stderr.write( 22 | "\033[1;32m" 23 | + "info: " 24 | + "\033[0m" 25 | + "\033[90mPY roblox-py:\033[0m " 26 | + msg 27 | + "\n" 28 | ) 29 | -------------------------------------------------------------------------------- /src/loopcounter.py: -------------------------------------------------------------------------------- 1 | """Label counter for the loops continue""" 2 | 3 | 4 | class LoopCounter: 5 | """Loop counter""" 6 | 7 | COUNTER = 0 8 | 9 | @staticmethod 10 | def get_next(): 11 | """Return next loop continue label name""" 12 | LoopCounter.COUNTER += 1 13 | return "loop_label_{}".format(LoopCounter.COUNTER) 14 | -------------------------------------------------------------------------------- /src/luau.py: -------------------------------------------------------------------------------- 1 | """Luau""" 2 | 3 | reserves = [ 4 | "and", 5 | "break", 6 | "do", 7 | "else", 8 | "elseif", 9 | "end", 10 | "for", 11 | "function", 12 | "if", 13 | "in", 14 | "local", 15 | "not", 16 | "or", 17 | "repeat", 18 | "return", 19 | "then", 20 | "until", 21 | "while", 22 | ] 23 | -------------------------------------------------------------------------------- /src/nodevisitor.py: -------------------------------------------------------------------------------- 1 | """Node visitor""" 2 | 3 | import ast 4 | from tokenend import TokenEndMode 5 | from context import Context 6 | from log import error, warn 7 | from symbols import * 8 | from binop import * 9 | from compop import * 10 | from const import * 11 | from loopcounter import * 12 | from luau import * 13 | from symbols import * 14 | from translator import * 15 | from config import * 16 | import lib, re 17 | from unary import * 18 | 19 | # Note from @AsynchronousAI: This file is legit the compiler, if u wanna change up the generated code use this. 20 | dependencies = [] 21 | exports = [] 22 | 23 | 24 | class NodeVisitor(ast.NodeVisitor): 25 | LUACODE = "luau" 26 | 27 | """Node visitor""" 28 | 29 | def __init__(self, context=None, config=None, variables=None, functions=None, currentFunction=None, generators=None): 30 | dependencies = [] 31 | exports = [] 32 | 33 | self.context = context if context is not None else Context() 34 | self.config = config 35 | self.last_end_mode = TokenEndMode.LINE_FEED 36 | self.output = [] 37 | self.variables = variables if variables is not None else {} 38 | self.functions = functions if functions is not None else [] 39 | self.currentFunctionName = currentFunction 40 | self.generators = generators if generators is not None else [] 41 | 42 | def visit_YieldFrom(self, node): 43 | """Visit yield from""" 44 | self.emit( 45 | "for _, v in {} do coroutine.yield(v) end".format( 46 | self.visit_all(node.value, inline=True) 47 | ) 48 | ) 49 | 50 | def get_variable_name(self, node): 51 | if isinstance(node, ast.Name): 52 | return node.id 53 | return None 54 | 55 | 56 | def visit_Assign(self, node): 57 | """Visit assign""" 58 | 59 | for target in node.targets: 60 | var_name = self.get_variable_name(target) 61 | if var_name: 62 | var_value_node = node.value 63 | if isinstance(var_value_node, ast.Constant): 64 | var_type = type(var_value_node.value).__name__ 65 | else: 66 | var_type = type(var_value_node).__name__ 67 | 68 | self.variables[var_name] = var_type 69 | 70 | target = self.visit_all(node.targets[0], inline=True) 71 | value = self.visit_all(node.value, inline=True) 72 | if "," in target: 73 | t2 = target.replace(" ", "").split(",") 74 | i = 0 75 | while i <= (len(t2) - 1): 76 | if t2[i] in reserves: 77 | error(f"'{t2[i]}' is a reserved Luau keyword.") 78 | else: 79 | i += 1 80 | 81 | local_keyword = "" 82 | 83 | last_ctx = self.context.last() 84 | 85 | if last_ctx["class_name"]: 86 | target = ".".join([last_ctx["class_name"], target]) 87 | 88 | if target.isalnum() and not last_ctx["locals"].exists(target): 89 | local_keyword = "local " 90 | last_ctx["locals"].add_symbol(target) 91 | 92 | if target in reserves or target in lib.libs: 93 | error(f"'{target}' is a reserved Luau keyword.") 94 | 95 | self.emit( 96 | "{local}{target} = {value}".format( 97 | local=local_keyword, target=target, value=value 98 | ) 99 | ) 100 | 101 | ### MATCHES ### 102 | 103 | def visit_Match(self, node): 104 | """Visit match""" 105 | for case in node.cases: 106 | if hasattr(case.pattern, "value"): 107 | first = "" 108 | if case is node.cases[0]: 109 | first = "" 110 | else: 111 | first = "else" 112 | 113 | val = case.pattern.value.s 114 | if isinstance(val, str): 115 | val = '"{}"'.format(val) 116 | self.emit( 117 | "{}if {} == {} then".format( 118 | first, self.visit_all(node.subject, inline=True), val 119 | ) 120 | ) 121 | self.visit_all(case.body) 122 | else: 123 | # self.emit("else") 124 | # self.visit_all(case.body) 125 | error( 126 | "Match statement requires a compile-time constant, not a variable." 127 | ) 128 | self.emit("end") 129 | 130 | def visit_MatchValue(self, node): 131 | """Visit match value""" 132 | return self.visit_all(node.value, inline=True) 133 | 134 | def visit_MatchCase(self, node): 135 | """Visit match case""" 136 | return self.visit_all(node.body) 137 | 138 | def visit_MatchPattern(self, node): 139 | """Visit match pattern""" 140 | return self.visit_all(node.pattern, inline=True) 141 | 142 | def visit_MatchSingleton(self, node): 143 | """Visit match singleton""" 144 | return self.visit_all(node.pattern, inline=True) 145 | 146 | def visit_MatchSequence(self, node): 147 | """Visit match sequence""" 148 | return self.visit_all(node.pattern, inline=True) 149 | 150 | def visit_MatchMapping(self, node): 151 | """Visit match mapping""" 152 | return self.visit_all(node.pattern, inline=True) 153 | 154 | def visit_MatchClass(self, node): 155 | """Visit match class""" 156 | return self.visit_all(node.pattern, inline=True) 157 | 158 | def visit_MatchAs(self, node): 159 | """Visit match as""" 160 | return self.visit_all(node.pattern, inline=True) 161 | 162 | def visit_MatchKeyword(self, node): 163 | """Visit match keyword""" 164 | return self.visit_all(node.pattern, inline=True) 165 | 166 | def visit_MatchStar(self, node): 167 | """Visit match star""" 168 | return self.visit_all(node.pattern, inline=True) 169 | 170 | def visit_MatchOr(self, node): 171 | """Visit match or""" 172 | return self.visit_all(node.pattern, inline=True) 173 | 174 | ### END MATCH ### 175 | def visit_AsyncWith(self, node): 176 | """Visit async with""" 177 | """Visit with""" 178 | self.emit("task.spawn(function()") 179 | self.visit_all(node.body) 180 | body = self.output[-1] 181 | lines = [] 182 | for i in node.items: 183 | line = "" 184 | if i.optional_vars is not None: 185 | line = "local {} = " 186 | line = line.format(self.visit_all(i.optional_vars, inline=True)) 187 | line += self.visit_all(i.context_expr, inline=True) 188 | lines.append(line) 189 | for line in lines: 190 | body.insert(0, line) 191 | self.emit("end)") 192 | 193 | def visit_Slice(self, node): 194 | if node.lower: 195 | lower = self.visit_all(node.lower, inline=True) 196 | else: 197 | lower = "0" 198 | lower += ", " 199 | if node.upper: 200 | upper = self.visit_all(node.upper, inline=True) 201 | else: 202 | upper = "0" 203 | upper += ", " 204 | if node.step: 205 | step = self.visit_all(node.step, inline=True) 206 | else: 207 | step = "0" 208 | 209 | self.emit("slice({}{}{})".format(lower, upper, step)) 210 | 211 | def visit_JoinedStr(self, node): 212 | # f"{a} {b}" 213 | # becomes 214 | # `{a} {b}` 215 | 216 | """Visit joined string""" 217 | values = [] 218 | for value in node.values: 219 | if isinstance(value, ast.Constant): 220 | values.append(value.s) 221 | else: 222 | values.append(self.visit_all(value, inline=True)) 223 | self.emit(repr("`{}`".format("".join(values)))[1:-1]) 224 | 225 | def visit_FormattedValue(self, node): 226 | """Visit formatted value""" 227 | # f"{a}" 228 | # becomes 229 | # {a} 230 | self.emit("{" + (self.visit_all(node.value, inline=True)) + "}") 231 | 232 | def visit_Bytes(self, node): 233 | """Visit bytes""" 234 | # Use utf8 strings instead of bytes 235 | self.emit("[[{}]]".format(node.s.decode("utf8"))) 236 | 237 | def visit_Assert(self, node): 238 | """Visit assert""" 239 | self.emit("assert({})".format(self.visit_all(node.test, True))) 240 | 241 | def visit_Nonlocal(self, node): 242 | """Visit nonlocal""" 243 | for name in node.names: 244 | self.context.last()["nonlocals"].add_symbol(name) 245 | 246 | def visit_AnnAssign(self, node): 247 | """Visit annassign""" 248 | target = self.visit_all(node.target, inline=True) 249 | value = self.visit_all(node.value, inline=True) 250 | local_keyword = "" 251 | last_ctx = self.context.last() 252 | istype = last_ctx["class_name"] == "TYPE" 253 | if last_ctx["class_name"] and not istype: 254 | target = ".".join([last_ctx["class_name"], target]) 255 | if "." not in target and not last_ctx["locals"].exists(target): 256 | local_keyword = "local " 257 | last_ctx["locals"].add_symbol(target) 258 | type = self.visit_all(node.annotation, inline=True) 259 | 260 | if value != None and value != "": 261 | self.emit( 262 | "{local}{target} = {value}".format( 263 | local=local_keyword, target=target, value=value 264 | ) 265 | ) 266 | else: 267 | if istype: 268 | self.emit("{target},".format(target=target)) 269 | else: 270 | if node.annotation.__class__.__name__ == "Call": 271 | self.visit_Call(node.annotation, target) 272 | else: 273 | self.emit( 274 | "{local}{target} = nil".format( 275 | local=local_keyword, target=target 276 | ) 277 | ) 278 | # example input: 279 | # a: int = 1 280 | # example output: 281 | # local a = 1 282 | 283 | def visit_AugAssign(self, node): 284 | """Visit augassign""" 285 | operation = BinaryOperationDesc.OPERATION[node.op.__class__] 286 | 287 | if operation["depend"]: 288 | self.depend(operation["depend"]) 289 | 290 | target = self.visit_all(node.target, inline=True) 291 | 292 | values = { 293 | "left": target, 294 | "right": self.visit_all(node.value, inline=True), 295 | "operation": operation["value"], 296 | } 297 | 298 | line = "({})".format(operation["format"]) 299 | 300 | line = line.format(**values) 301 | 302 | self.emit("{target} = {line}".format(target=target, line=line)) 303 | 304 | def visit_Attribute(self, node): 305 | """Visit attribute""" 306 | line = "{object}.{attr}" 307 | values = { 308 | "object": self.visit_all(node.value, True), 309 | "attr": node.attr, 310 | } 311 | # Does object start and end with a " " 312 | if ( 313 | (values["object"].startswith('"') and values["object"].endswith('"')) 314 | or (values["object"].startswith("'") and values["object"].endswith("'")) 315 | or (values["object"].startswith("{") and values["object"].endswith("}")) 316 | or (values["object"].startswith("[") and values["object"].endswith("]")) 317 | or (values["object"].startswith("`") and values["object"].endswith("`")) 318 | ): 319 | values["object"] = "({})".format(values["object"]) 320 | 321 | self.emit(line.format(**values)) 322 | 323 | def visit_BinOp(self, node): 324 | """Visit binary operation""" 325 | operation = BinaryOperationDesc.OPERATION[node.op.__class__] 326 | 327 | if operation["depend"]: 328 | self.depend(operation["depend"]) 329 | 330 | line = "({})".format(operation["format"]) 331 | 332 | left_value = self.visit_all(node.left, inline=True) 333 | right_value = self.visit_all(node.right, inline=True) 334 | left_is_str = ( 335 | isinstance(node.left, ast.Constant) 336 | and isinstance(node.left.value, str) 337 | or self.variables.get(left_value) == "str" 338 | ) 339 | right_is_str = ( 340 | isinstance(node.right, ast.Constant) 341 | and isinstance(node.right.value, str) 342 | or self.variables.get(right_value) == "str" 343 | ) 344 | if operation["value"] == "*": 345 | if left_is_str or self.variables.get(left_value) == "str": 346 | line = "string.rep({left},{right})" 347 | elif right_is_str or self.variables.get(right_value) == "str": 348 | line = "string.rep({right},{left})" 349 | 350 | if operation["value"] == "+": 351 | # Checks for list + list 352 | if self.variables.get(left_value) == "list" or isinstance( 353 | node.left, ast.List 354 | ): 355 | if self.variables.get(right_value) == "list" or isinstance( 356 | node.right, ast.List 357 | ): 358 | pass 359 | else: 360 | error("Not supported.") 361 | if left_is_str and right_is_str: 362 | line = "{} .. {}".format(left_value, right_value) 363 | self.emit(line) 364 | return 365 | elif left_is_str or right_is_str: 366 | # Handle the case when one operand is a string and the other is not 367 | if left_is_str: 368 | line = "{} .. tostring({})".format(left_value, right_value) 369 | else: 370 | line = "tostring({}) .. {}".format(left_value, right_value) 371 | self.emit(line) 372 | return 373 | else: 374 | # Handle the case when neither operand is a string 375 | values = { 376 | "left": left_value, 377 | "right": right_value, 378 | "operation": operation["value"], 379 | } 380 | line = line.format(**values) 381 | self.emit(line) 382 | return 383 | 384 | else: 385 | values = { 386 | "left": left_value, 387 | "right": right_value, 388 | "operation": operation["value"], 389 | } 390 | line = line.format(**values) 391 | self.emit(line) 392 | 393 | def visit_BoolOp(self, node): 394 | """Visit boolean operations""" 395 | BOOL_OPS = { 396 | ast.And: " and ", 397 | ast.Or: " or ", 398 | } 399 | 400 | values = [] 401 | for value in node.values: 402 | values.append(self.visit_all(value, inline=True)) 403 | 404 | op = BOOL_OPS[type(node.op)] 405 | line = f"({op.join(values)})" 406 | self.emit(line) 407 | 408 | 409 | def visit_Break(self, node): 410 | """Visit break""" 411 | self.emit("break") 412 | 413 | def visit_Call(self, node, method=None): 414 | """Visit function call""" 415 | 416 | def parse_expr(d): 417 | if isinstance(d, ast.Name): 418 | return d.id 419 | elif isinstance(d, ast.Attribute): 420 | return f"{parse_expr(d.value)}.{d.attr}" 421 | elif isinstance(d, ast.List): # Handling list literals 422 | elements = ", ".join([ast.unparse(el) for el in d.elts]) 423 | return "{" + f"{elements}" + "}" 424 | else: 425 | return ast.unparse(d) 426 | 427 | line = "{name}({arguments})" 428 | 429 | name = self.visit_all(node.func, inline=True) 430 | 431 | if str(name) == "complex": 432 | self.depend("complex") 433 | if method: 434 | name = method + ":" + name 435 | arguments = [self.visit_all(arg, inline=True) for arg in node.args] 436 | 437 | if isinstance(node.func, ast.Attribute): 438 | # Assuming the first argument exists and is what we want to inspect 439 | match node.func.attr: 440 | case "append": 441 | arg_value = ast.parse(node.args[0]) 442 | self.emit( 443 | "table.insert({},{})".format( 444 | parse_expr(node.func.value), parse_expr(arg_value) 445 | ) 446 | ) 447 | return 448 | case "reverse": 449 | self.emit( 450 | "table.sort({},{})".format( 451 | parse_expr(node.func.value), 452 | "(function(a,b) return a > b end)", 453 | ) 454 | ) 455 | return 456 | case "clear": 457 | self.emit("table.clear({})".format(parse_expr(node.func.value))) 458 | return 459 | case "pop": 460 | arg_value = ast.parse(node.args[0]) 461 | self.emit( 462 | "table.remove({},{})".format( 463 | parse_expr(node.func.value), (arg_value.value) + 1 464 | ) 465 | ) 466 | return 467 | case "copy": 468 | self.emit("table.copy({})".format(parse_expr(node.func.value))) 469 | return 470 | case "insert": 471 | arg_value = ast.parse(node.args[0]) 472 | self.emit( 473 | "table.insert({},{},{})".format( 474 | parse_expr(node.func.value), 475 | int(parse_expr(arg_value)) + 1, 476 | parse_expr(ast.parse(node.args[1])), 477 | ) 478 | ) 479 | return 480 | case "remove": 481 | arg_value = ast.parse(node.args[0]) 482 | self.emit( 483 | "table.remove({},{})".format( 484 | parse_expr(node.func.value), parse_expr(arg_value) 485 | ) 486 | ) 487 | return 488 | case "sort": 489 | kwargs = {} 490 | for keyword in node.keywords: 491 | kwargs[keyword.arg] = self.visit_all(keyword.value, inline=True) 492 | if len(kwargs) == 1 and kwargs.get("reverse", False): 493 | self.emit( 494 | "table.sort({},{})".format( 495 | parse_expr(node.func.value), 496 | "(function(a,b) return a > b end)", 497 | ) 498 | ) 499 | return 500 | elif len(kwargs) == 1 and kwargs.get("key", False): 501 | error("The key kwarg is not supported.") 502 | else: 503 | error("The key kwarg is not supported.") 504 | 505 | case "count": 506 | arg = node.args[0] 507 | 508 | self.emit(""" 509 | (function(tbl, val) 510 | local result = 0 511 | for _,v in tbl do 512 | if v == val then 513 | result += 1 514 | end 515 | end 516 | return result 517 | end)({}, {}) 518 | """.format(parse_expr(node.func.value),parse_expr(arg))) 519 | return 520 | 521 | case "index": 522 | args = 0 523 | for _ in node.args: 524 | args += 1 525 | 526 | if args == 1: 527 | self.emit("table.find({},{})".format( 528 | parse_expr(node.func.value),parse_expr(node.args[0]) 529 | )) 530 | return 531 | elif args == 2: 532 | self.emit("table.find({},{},{})".format(parse_expr(node.func.value),parse_expr(node.args[0]),parse_expr(node.args[1]))) 533 | return 534 | else: 535 | self.emit(""" 536 | (function(tbl, val, start, stop) 537 | for index = start, stop do 538 | if tbl[index] == val then 539 | return index 540 | end 541 | end 542 | return nil 543 | end)({}, {}, {}, {})""".format(parse_expr(node.func.value),parse_expr(node.args[0]),parse_expr(node.args[1]),parse_expr(node.args[2]))) 544 | return 545 | 546 | 547 | case "extend": 548 | arg_value = ast.parse(node.args[0]) 549 | self.emit( 550 | """ 551 | for _,i in {} do 552 | table.insert({},i) 553 | 554 | """.format(parse_expr(arg_value), parse_expr(node.func.value)) 555 | ) 556 | return 557 | 558 | self.emit(line.format(name=name, arguments=", ".join(arguments))) 559 | 560 | def visit_TypeAlias(self, node): 561 | if self.config["luau"] == None: 562 | warn( 563 | 'in your \033[1m.robloxpy.json\033[0m file, specify weather or not to export Luau types like \033[1m"luau": true\033[0m`. Ignoring type alias.' 564 | ) 565 | elif self.config["luau"] == True: 566 | self.emit( 567 | "type {} = {}".format( 568 | node.name.id, self.visit_all(node.value, inline=True) 569 | ) 570 | ) 571 | 572 | def visit_ClassDef(self, node): 573 | """Visit class definition""" 574 | bases = [self.visit_all(base, inline=True) for base in node.bases] 575 | 576 | # if "type" in bases: 577 | # self.emit("type {} = {{".format(node.name, self.visit_all(node.bases[0], inline=True))) 578 | # self.context.push({"class_name": "TYPE"}) 579 | # self.visit_all(node.body) 580 | # self.context.pop() 581 | # self.emit("}") 582 | # else: 583 | local_keyword = "" 584 | last_ctx = self.context.last() 585 | if not last_ctx["class_name"] and not last_ctx["locals"].exists(node.name): 586 | local_keyword = "local " 587 | last_ctx["locals"].add_symbol(node.name) 588 | 589 | name = node.name 590 | if last_ctx["class_name"]: 591 | name = ".".join([last_ctx["class_name"], name]) 592 | 593 | values = { 594 | "local": local_keyword, 595 | "name": name, 596 | "node_name": node.name, 597 | } 598 | 599 | self.emit("{local}{name} = class(function({node_name})".format(**values)) 600 | self.depend("class") 601 | 602 | self.context.push({"class_name": node.name}) 603 | self.visit_all(node.body) 604 | self.context.pop() 605 | 606 | self.output[-1].append("return {node_name}".format(**values)) 607 | 608 | self.emit("end, {{{}}})".format(", ".join(bases))) 609 | 610 | # Return class object only in the top-level classes. 611 | # Not in the nested classes. 612 | if self.config["class"]["return_at_the_end"] and not last_ctx["class_name"]: 613 | self.emit("return {}".format(name)) 614 | 615 | def visit_Compare(self, node): 616 | """Visit compare""" 617 | line = "" 618 | left = self.visit_all(node.left, inline=True) 619 | for i, op in enumerate(node.ops): 620 | operation = CompareOperationDesc.OPERATION.get(op.__class__) 621 | comparator = node.comparators[i] 622 | right = self.visit_all(comparator, inline=True) 623 | 624 | values = { 625 | "left": left, 626 | "operation": operation, 627 | "right": right, 628 | } 629 | if op.__class__.__name__ == "In" or op.__class__.__name__ == "NotIn": 630 | op_str = "not" if op.__class__.__name__ == "NotIn" else "" 631 | leftval = type(node.left).__name__ 632 | rightval = type(comparator).__name__ 633 | 634 | if leftval == 'Constant': 635 | leftval = (type(node.left.value).__name__).capitalize() 636 | if rightval == 'Name': 637 | x = self.visit_all(comparator, inline=True) 638 | rightval = self.variables.get(x) 639 | if rightval == 'Constant': 640 | rightval = (type(comparator.value).__name__).capitalize() 641 | if leftval == 'Name': 642 | x = self.visit_all(node.left, inline=True) 643 | leftval = self.variables.get(x) 644 | ##IN CHECK## 645 | if rightval == 'List': 646 | line += f"{op_str}(table.find({right}, {left}) ~= nil)" 647 | elif rightval == 'Str': 648 | line += f"{op_str}(string.find({right}, {left}, 1, true) ~= nil)" 649 | else: 650 | line += f"{op_str}({right}[{left}] ~= nil)" 651 | else: 652 | line += f"{values['left']} {values['operation']} {values['right']}" 653 | 654 | if i < len(node.ops) - 1: 655 | line += " and " 656 | left = right 657 | 658 | self.emit(line) 659 | 660 | 661 | 662 | def visit_Continue(self, node): 663 | """Visit continue""" 664 | last_ctx = self.context.last() 665 | line = "continue" 666 | self.emit(line) 667 | 668 | def visit_Delete(self, node): 669 | """Visit delete""" 670 | targets = [self.visit_all(target, inline=True) for target in node.targets] 671 | nils = ["nil" for _ in targets] 672 | line = "{targets} = {nils}".format( 673 | targets=", ".join(targets), nils=", ".join(nils) 674 | ) 675 | self.emit(line) 676 | 677 | def visit_Dict(self, node): 678 | """Visit dictionary""" 679 | keys = [] 680 | 681 | for key in node.keys: 682 | value = self.visit_all(key, inline=True) 683 | value = "[{}]".format(value) 684 | keys.append(value) 685 | 686 | values = [self.visit_all(item, inline=True) for item in node.values] 687 | 688 | elements = ["{} = {}".format(keys[i], values[i]) for i in range(len(keys))] 689 | elements = ", ".join(elements) 690 | self.depend("dict") 691 | self.emit("dict {{{}}}".format(elements)) 692 | self.depend("dict") 693 | 694 | def visit_DictComp(self, node): 695 | """Visit dictionary comprehension""" 696 | self.emit("(function()") 697 | self.depend("dict") 698 | self.emit("local result = dict {}") 699 | self.depend("dict") 700 | 701 | ends_count = 0 702 | 703 | for comp in node.generators: 704 | line = "for {target} in {iterator} do" 705 | values = { 706 | "target": self.visit_all(comp.target, inline=True), 707 | "iterator": self.visit_all(comp.iter, inline=True), 708 | } 709 | line = line.format(**values) 710 | self.emit(line) 711 | ends_count += 1 712 | 713 | for if_ in comp.ifs: 714 | line = "if {} then".format(self.visit_all(if_, inline=True)) 715 | self.emit(line) 716 | ends_count += 1 717 | 718 | line = "result[{key}] = {value}" 719 | values = { 720 | "key": self.visit_all(node.key, inline=True), 721 | "value": self.visit_all(node.value, inline=True), 722 | } 723 | self.emit(line.format(**values)) 724 | 725 | self.emit(" ".join(["end"] * ends_count)) 726 | 727 | self.emit("return result") 728 | self.emit("end)()") 729 | 730 | def visit_Ellipsis(self, node): 731 | """Visit ellipsis""" 732 | self.emit("...") 733 | 734 | def visit_Expr(self, node): 735 | """Visit expr""" 736 | if isinstance(node.value, ast.Constant): 737 | try: 738 | self.functions[-1]["body"].append(node.value.value) 739 | except: 740 | pass 741 | 742 | expr_is_docstring = False 743 | if isinstance(node.value, ast.Constant): 744 | expr_is_docstring = True 745 | 746 | self.context.push({"docstring": expr_is_docstring}) 747 | output = self.visit_all(node.value) 748 | self.context.pop() 749 | 750 | self.output.append(output) 751 | 752 | def visit_FunctionDef(self, node): 753 | """Visit function definition""" 754 | line = "{local} function {name}({arguments})" 755 | 756 | self.functions.append( 757 | { 758 | "name": node.name, 759 | "args": [arg.arg for arg in node.args.args], 760 | "returns": None, 761 | "body": [], 762 | } 763 | ) 764 | self.currentFunctionName = node.name 765 | 766 | last_ctx = self.context.last() 767 | 768 | name = node.name 769 | type = 1 # 1 = static, 2 = class 770 | for decorator in reversed(node.decorator_list): 771 | decorator_name = self.visit_all(decorator, inline=True) 772 | 773 | if decorator_name == "classmethod": 774 | type = 2 775 | elif decorator_name == "staticmethod": 776 | type = 1 777 | if last_ctx["class_name"]: 778 | name = ".".join([last_ctx["class_name"], name]) 779 | 780 | if type == 1: 781 | arguments = [arg.arg for arg in node.args.args] 782 | else: 783 | arguments = ["self"] 784 | arguments.extend([arg.arg for arg in node.args.args]) 785 | 786 | if node.args.vararg is not None: 787 | arguments.append("...") 788 | 789 | local_keyword = "" 790 | 791 | if "." not in name and not last_ctx["locals"].exists(name): 792 | local_keyword = "local " 793 | last_ctx["locals"].add_symbol(name) 794 | 795 | function_def = line.format( 796 | local=local_keyword, name=name, arguments=", ".join(arguments) 797 | ) 798 | 799 | self.emit(function_def) 800 | 801 | self.context.push({"class_name": ""}) 802 | self.visit_all(node.body) 803 | if node.name in self.generators: 804 | self.depend("generator") 805 | self.emit("\tyieldGenerator('{}', {})".format(node.name, "'__END'")) 806 | self.context.pop() 807 | 808 | body = self.output[-1] 809 | 810 | if node.args.vararg is not None: 811 | line = "local {name} = {{...}}".format(name=node.args.vararg.arg) 812 | body.insert(0, line) 813 | 814 | arg_index = -1 815 | for i in reversed(node.args.defaults): 816 | line = "{name} = {name} or {value}" 817 | 818 | arg = node.args.args[arg_index] 819 | values = { 820 | "name": arg.arg, 821 | "value": self.visit_all(i, inline=True), 822 | } 823 | body.insert(0, line.format(**values)) 824 | 825 | arg_index -= 1 826 | 827 | self.emit("end") 828 | 829 | for decorator in reversed(node.decorator_list): 830 | decorator_name = self.visit_all(decorator, inline=True) 831 | if decorator_name == "classmethod" or decorator_name == "staticmethod": 832 | continue 833 | values = { 834 | "name": name, 835 | "decorator": decorator_name, 836 | } 837 | line = "{name} = {decorator}:Connect({name})".format(**values) 838 | self.emit(line) 839 | 840 | self.currentFunctionName = None 841 | # exports.append(name) 842 | 843 | def visit_For(self, node): 844 | """Visit for loop""" 845 | values = { 846 | "target": self.visit_all(node.target, inline=True), 847 | "iter": self.visit_all(node.iter, inline=True), 848 | } 849 | 850 | line = "for {target} in {iter} do" 851 | 852 | if isinstance(node.iter, ast.Constant): 853 | line = 'for {target} in string.gmatch({iter},".") do' 854 | 855 | if values["iter"] in self.variables: 856 | if self.variables[values["iter"]] == "str": 857 | line = 'for {target} in string.gmatch({iter},".") do' 858 | else: 859 | line = "for {target} in {iter} do" 860 | 861 | isAGenerator = re.sub(r"\([^)]*\)", "", values["iter"]) in self.generators 862 | if isAGenerator: 863 | funcName = re.sub(r"\([^)]*\)", "", values["iter"]) 864 | args = re.search(r"\(([^)]*)\)", values["iter"]).group(1) 865 | if args == "": 866 | args = "nil" 867 | 868 | line = "generatorLoop('"+funcName+"', "+funcName+", "+args+")(function({target})" 869 | 870 | self.emit(line.format(**values)) 871 | 872 | continue_label = LoopCounter.get_next() 873 | self.context.push( 874 | { 875 | "loop_label_name": continue_label, 876 | } 877 | ) 878 | self.visit_all(node.body) 879 | self.context.pop() 880 | 881 | if isAGenerator: 882 | self.emit("end)") 883 | else: 884 | self.emit("end") 885 | 886 | 887 | def visit_Global(self, node): 888 | """Visit globals""" 889 | last_ctx = self.context.last() 890 | for name in node.names: 891 | last_ctx["globals"].add_symbol(name) 892 | exports.append(name) 893 | 894 | def visit_Yield(self, node): 895 | """Visit yield""" 896 | self.generators.append(self.currentFunctionName) 897 | self.emit("yieldGenerator('{}', {})".format(self.currentFunctionName, self.visit_all(node.value, inline=True))) 898 | 899 | def visit_If(self, node): 900 | """Visit if""" 901 | test = self.visit_all(node.test, inline=True) 902 | 903 | line = "if {} then".format(test) 904 | 905 | self.emit(line) 906 | self.visit_all(node.body) 907 | 908 | if node.orelse: 909 | if isinstance(node.orelse[0], ast.If): 910 | _elseif = node.orelse[0] 911 | elseif_test = self.visit_all(_elseif.test, inline=True) 912 | 913 | line = "elseif {} then".format(elseif_test) 914 | self.emit(line) 915 | 916 | output_length = len(self.output) 917 | self.visit_If(node.orelse[0]) 918 | 919 | del self.output[output_length] 920 | del self.output[-1] 921 | else: 922 | self.emit("else") 923 | self.visit_all(node.orelse) 924 | 925 | self.emit("end") 926 | 927 | def visit_IfExp(self, node): 928 | """Visit if expression""" 929 | line = "{cond} and {true_cond} or {false_cond}" 930 | values = { 931 | "cond": self.visit_all(node.test, inline=True), 932 | "true_cond": self.visit_all(node.body, inline=True), 933 | "false_cond": self.visit_all(node.orelse, inline=True), 934 | } 935 | 936 | self.emit(line.format(**values)) 937 | 938 | def visit_Import(self, node, og=True): 939 | """Visit import""" 940 | line = 'local {asname} = rcc.import("{name}", script) or require("{name}")' 941 | values = {"asname": "", "name": ""} 942 | 943 | if og: 944 | for v in node.names: 945 | self.visit_Import(v, False) 946 | return 947 | 948 | if node.name.startswith("game."): 949 | line = 'local {asname} = game:GetService("{name}")' 950 | values["name"] = node.name[5:] 951 | 952 | if node.asname is None: 953 | if not node.name.startswith("game."): 954 | values["name"] = node.name 955 | values["asname"] = values["name"] 956 | values["asname"] = values["asname"].split(".")[-1] 957 | else: 958 | values["asname"] = node.asname 959 | if not node.name.startswith("game."): 960 | values["name"] = node.name 961 | 962 | if node.name in lib.libs: 963 | self.emit(getattr(libs, node.name)) 964 | return 965 | 966 | self.emit(line.format(**values)) 967 | 968 | def visit_ImportFrom(self, node): 969 | """Visit import from""" 970 | module = node.module 971 | if module is None: 972 | module = "" 973 | else: 974 | module = module 975 | 976 | if module == "services" or module == "rbx.services": 977 | for name in node.names: 978 | if name.asname is None: 979 | if name.name == "*": 980 | error("import * is unsupproted") 981 | else: 982 | self.emit( 983 | 'local {name} = game:GetService("{name}")'.format( 984 | name=name.name, 985 | ) 986 | ) 987 | else: 988 | if name.name == "*": 989 | error("import * is unsupproted") 990 | else: 991 | self.emit( 992 | 'local {name} = game:GetService("{realname}")'.format( 993 | name=name.asname, 994 | realname=name.name, 995 | ) 996 | ) 997 | elif module == "rbx": 998 | for name in node.names: 999 | if name.asname is None: 1000 | if name.name == "*": 1001 | continue 1002 | else: 1003 | self.emit( 1004 | 'local {name} = game:GetService("{name}")'.format( 1005 | name=name.name, 1006 | ) 1007 | ) 1008 | else: 1009 | if name.name == "*": 1010 | continue 1011 | else: 1012 | self.emit( 1013 | 'local {name} = game:GetService("{realname}")'.format( 1014 | name=name.asname, 1015 | realname=name.name, 1016 | ) 1017 | ) 1018 | else: 1019 | for name in node.names: 1020 | if name.asname is None: 1021 | if name.name == "*": 1022 | error("import * is unsupproted") 1023 | else: 1024 | self.emit( 1025 | 'local {name} = (rcc.import("{module}") or require("{module}")).{name}'.format( 1026 | name=name.name, 1027 | module=module, 1028 | ) 1029 | ) 1030 | else: 1031 | if name.name == "*": 1032 | error("import * is unsupproted") 1033 | else: 1034 | self.emit( 1035 | 'local {name} = (rcc.import("{module}") or require("{module}")).{realname}'.format( 1036 | name=name.asname, 1037 | module=module, 1038 | realname=name.name, 1039 | ) 1040 | ) 1041 | 1042 | def visit_Index(self, node): 1043 | """Visit index""" 1044 | self.emit(self.visit_all(node.value, inline=True)) 1045 | 1046 | def visit_Lambda(self, node): 1047 | """Visit lambda""" 1048 | line = "function({arguments}) return" 1049 | 1050 | arguments = [arg.arg for arg in node.args.args] 1051 | 1052 | function_def = line.format(arguments=", ".join(arguments)) 1053 | 1054 | output = [] 1055 | output.append(function_def) 1056 | output.append(self.visit_all(node.body, inline=True)) 1057 | output.append("end") 1058 | 1059 | self.emit(" ".join(output)) 1060 | 1061 | def visit_List(self, node): 1062 | """Visit list""" 1063 | elements = [self.visit_all(item, inline=True) for item in node.elts] 1064 | line = "{{{}}}".format(", ".join(elements)) 1065 | self.emit(line) 1066 | 1067 | def visit_GeneratorExp(self, node): 1068 | """Visit generator expression""" 1069 | self.emit("(function()") 1070 | self.emit("local result = {}") 1071 | 1072 | ends_count = 0 1073 | 1074 | for comp in node.generators: 1075 | line = "for {target} in {iterator} do" 1076 | values = { 1077 | "target": self.visit_all(comp.target, inline=True), 1078 | "iterator": self.visit_all(comp.iter, inline=True), 1079 | } 1080 | line = line.format(**values) 1081 | self.emit(line) 1082 | ends_count += 1 1083 | 1084 | for if_ in comp.ifs: 1085 | line = "if {} then".format(self.visit_all(if_, inline=True)) 1086 | self.emit(line) 1087 | ends_count += 1 1088 | 1089 | line = "result.append({})" 1090 | line = line.format(self.visit_all(node.elt, inline=True)) 1091 | self.emit(line) 1092 | 1093 | self.emit(" ".join(["end"] * ends_count)) 1094 | 1095 | self.emit("return result") 1096 | self.emit("end)()") 1097 | 1098 | def visit_ListComp(self, node): 1099 | """Visit list comprehension""" 1100 | self.emit("(function()") 1101 | self.emit("local result = {}") 1102 | 1103 | ends_count = 0 1104 | 1105 | for comp in node.generators: 1106 | line = "for {target} in {iterator} do" 1107 | values = { 1108 | "target": self.visit_all(comp.target, inline=True), 1109 | "iterator": self.visit_all(comp.iter, inline=True), 1110 | } 1111 | line = line.format(**values) 1112 | self.emit(line) 1113 | ends_count += 1 1114 | 1115 | for if_ in comp.ifs: 1116 | line = "if {} then".format(self.visit_all(if_, inline=True)) 1117 | self.emit(line) 1118 | ends_count += 1 1119 | 1120 | line = ( 1121 | "table.insert(result._data," 1122 | + "{" 1123 | + str(self.visit_all(node.elt, inline=True)) 1124 | + "}" 1125 | ) 1126 | self.emit(line) 1127 | 1128 | self.emit(" ".join(["end"] * ends_count)) 1129 | 1130 | self.emit("return result") 1131 | self.emit("end)()") 1132 | 1133 | def visit_Module(self, node): 1134 | """Visit module""" 1135 | self.visit_all(node.body) 1136 | self.output = self.output[0] 1137 | 1138 | def visit_Name(self, node): 1139 | """Visit name""" 1140 | self.emit(node.id) 1141 | 1142 | def visit_NameConstant(self, node): 1143 | """Visit name constant""" 1144 | self.emit(NameConstantDesc.NAME[node.value]) 1145 | 1146 | def visit_Num(self, node): 1147 | """Visit number""" 1148 | self.emit(str(node.value)) 1149 | 1150 | def visit_Pass(self, node): 1151 | """Visit pass""" 1152 | pass 1153 | 1154 | def visit_Return(self, node): 1155 | """Visit return""" 1156 | line = "return " 1157 | if node.value: 1158 | self.functions[-1]["returns"] = ( 1159 | str(self.visit_all(node.value, inline=True)).strip("(").strip(")") 1160 | ) 1161 | line += self.visit_all(node.value, inline=True) 1162 | self.emit(line) 1163 | 1164 | def visit_Starred(self, node): 1165 | """Visit starred object""" 1166 | value = self.visit_all(node.value, inline=True) 1167 | line = "unpack({})".format(value) 1168 | self.emit(line) 1169 | 1170 | def visit_Str(self, node): 1171 | """Visit str""" 1172 | value = repr(node.s) 1173 | if value.startswith(NodeVisitor.LUACODE): 1174 | value = value[len(NodeVisitor.LUACODE) :] 1175 | self.emit(value) 1176 | elif self.context.last()["docstring"]: 1177 | self.emit("--[[ {} ]]".format(node.s)) 1178 | else: 1179 | self.emit(repr("{}".format(node.s))) 1180 | 1181 | def visit_Subscript(self, node): 1182 | """Visit subscript""" 1183 | line = "{name}{indexs}" 1184 | index = self.visit_all(node.slice, inline=True) 1185 | indexs = [] 1186 | final = "" 1187 | 1188 | # Split index by toplevel , and add each index to indexs 1189 | # This is done to support multiple indexes 1190 | # Example: a[1, 2, 3] -> a[1][2][3] 1191 | # but a[x(1,2), b(3, 4)] -> a[x(1,2)][b(3, 4)] 1192 | 1193 | depth = 0 1194 | for char in index: 1195 | if char == "(": 1196 | depth += 1 1197 | elif char == ")": 1198 | depth -= 1 1199 | elif char == "," and depth == 0: 1200 | indexs.append(index[: index.find(char)]) 1201 | index = index[index.find(char) + 1 :] 1202 | indexs.append(index) 1203 | 1204 | # Generate final 1205 | for i in indexs: 1206 | final += "[{}]".format(i) 1207 | 1208 | values = { 1209 | "name": self.visit_all(node.value, inline=True), 1210 | "indexs": final, 1211 | } 1212 | if values["name"] in reserves: 1213 | error(f"'{values['name']}'is a reserved Luau keyword.") 1214 | 1215 | self.emit(line.format(**values)) 1216 | 1217 | def visit_Tuple(self, node): 1218 | """Visit tuple""" 1219 | elements = [self.visit_all(item, inline=True) for item in node.elts] 1220 | line = "table.freeze({{{}}})".format(", ".join(elements)) 1221 | self.emit(line) 1222 | 1223 | def visit_UnaryOp(self, node): 1224 | """Visit unary operator""" 1225 | operation = UnaryOperationDesc.OPERATION[node.op.__class__] 1226 | value = self.visit_all(node.operand, inline=True) 1227 | 1228 | line = operation["format"] 1229 | 1230 | values = { 1231 | "value": value, 1232 | "operation": operation["value"], 1233 | } 1234 | 1235 | self.emit(line.format(**values)) 1236 | 1237 | def visit_Raise(self, node): 1238 | """Visit raise""" 1239 | line = "error({})".format(self.visit_all(node.exc, inline=True)) 1240 | self.emit(line) 1241 | 1242 | def visit_Set(self, node): 1243 | """Visit set""" 1244 | values = [self.visit_all(value, True) for value in node.elts] 1245 | self.emit("{" + ", ".join(values) + "}") 1246 | 1247 | def visit_Try(self, node): 1248 | """Visit try""" 1249 | self.emit("--> try statement start:") 1250 | self.emit("xpcall(function()") 1251 | 1252 | self.visit_all(node.body) 1253 | 1254 | self.emit("end, function(err)") 1255 | ifD = False 1256 | for i, handler in enumerate(node.handlers): 1257 | self.emit("--> try: {}".format(i)) 1258 | if ((handler.type) != None) and hasattr(handler.type, "id"): 1259 | if i == 0: 1260 | ifD = True 1261 | an = "if" 1262 | else: 1263 | an = "elseif" 1264 | if ( 1265 | handler.type.id != "Exception" 1266 | or handler.type.id != "BaseException" 1267 | or handler.type.id != "Error" 1268 | ): 1269 | self.emit(f"{an} err:find('{handler.type.id}') then") 1270 | else: 1271 | self.emit(f"{an} err then") 1272 | 1273 | if handler.name != None: 1274 | self.emit(f"\tlocal {handler.name} = err") 1275 | self.visit_all(handler.body) 1276 | if (i == len(node.handlers) - 1) and ifD: 1277 | self.emit("end") 1278 | 1279 | self.emit("end)") 1280 | self.visit_all(node.finalbody) 1281 | 1282 | self.emit("--> try statement end") 1283 | 1284 | def visit_While(self, node): 1285 | """Visit while""" 1286 | test = self.visit_all(node.test, inline=True) 1287 | 1288 | self.emit("while {} do".format(test)) 1289 | 1290 | continue_label = LoopCounter.get_next() 1291 | self.context.push( 1292 | { 1293 | "loop_label_name": continue_label, 1294 | } 1295 | ) 1296 | self.visit_all(node.body) 1297 | self.context.pop() 1298 | 1299 | self.emit("end") 1300 | 1301 | def visit_SetComp(self, node): 1302 | """Visit set comprehension""" 1303 | self.emit("(function()") 1304 | self.emit("local result = {}") 1305 | ends_count = 0 1306 | for comp in node.generators: 1307 | line = "for {target} in {iterator} do" 1308 | values = { 1309 | "target": self.visit_all(comp.target, inline=True), 1310 | "iterator": self.visit_all(comp.iter, inline=True), 1311 | } 1312 | line = line.format(**values) 1313 | self.emit(line) 1314 | ends_count += 1 1315 | for if_ in comp.ifs: 1316 | line = "if {} then".format(self.visit_all(if_, inline=True)) 1317 | self.emit(line) 1318 | ends_count += 1 1319 | line = "table.insert(result, {})" 1320 | line = line.format(self.visit_all(node.elt, inline=True)) 1321 | self.emit(line) 1322 | self.emit(" ".join(["end"] * ends_count)) 1323 | self.emit("return result") 1324 | self.emit("end)()") 1325 | 1326 | def visit_With(self, node): 1327 | """Visit with""" 1328 | self.emit("do") 1329 | 1330 | self.visit_all(node.body) 1331 | 1332 | body = self.output[-1] 1333 | lines = [] 1334 | for i in node.items: 1335 | line = "" 1336 | if i.optional_vars is not None: 1337 | line = "local {} = " 1338 | line = line.format(self.visit_all(i.optional_vars, inline=True)) 1339 | line += self.visit_all(i.context_expr, inline=True) 1340 | lines.append(line) 1341 | 1342 | for line in lines: 1343 | body.insert(0, line) 1344 | 1345 | self.emit("end") 1346 | 1347 | def visit_all(self, nodes, inline=False): 1348 | """Visit all nodes in the given list""" 1349 | 1350 | if not inline: 1351 | last_ctx = self.context.last() 1352 | last_ctx["locals"].push() 1353 | 1354 | visitor = NodeVisitor( 1355 | context=self.context, 1356 | config=self.config, 1357 | variables=self.variables, 1358 | functions=self.functions, 1359 | currentFunction=self.currentFunctionName, 1360 | generators=self.generators, 1361 | ) 1362 | 1363 | if isinstance(nodes, list): 1364 | for node in nodes: 1365 | visitor.visit(node) 1366 | if not inline: 1367 | self.output.append(visitor.output) 1368 | else: 1369 | visitor.visit(nodes) 1370 | if not inline: 1371 | self.output.extend(visitor.output) 1372 | 1373 | if not inline: 1374 | last_ctx = self.context.last() 1375 | last_ctx["locals"].pop() 1376 | 1377 | if inline: 1378 | return " ".join(visitor.output) 1379 | 1380 | def emit(self, value): 1381 | """Add translated value to the output""" 1382 | self.output.append(value) 1383 | 1384 | def depend(self, value): 1385 | if value != "": 1386 | dependencies.append(value) 1387 | 1388 | def get_dependencies(self): 1389 | return dependencies 1390 | 1391 | def get_exports(self): 1392 | return exports 1393 | -------------------------------------------------------------------------------- /src/rbxpy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | import subprocess 5 | import threading 6 | import json 7 | from pathlib import Path 8 | 9 | try: 10 | import shutil 11 | except ImportError: 12 | print("Please install the 'shutil' module.") 13 | exit(1) 14 | 15 | 16 | #### PYRIGHT #### 17 | def check_pyright(): # TODO: make this into mypy 18 | exists = shutil.which("pyright") is not None 19 | # if not exists: 20 | # warn("pyright is not installed, install it for more descriptive and compiler errors.") 21 | return exists 22 | 23 | 24 | #### COMPILER #### 25 | from luau import * 26 | from binop import * 27 | from compop import * 28 | from const import TAB, VERSION 29 | from context import * 30 | from loopcounter import * 31 | from symbols import * 32 | from tokenend import * 33 | from translator import * 34 | from unary import * 35 | from config import Config 36 | 37 | 38 | #### INTERFACE #### 39 | from log import error 40 | 41 | 42 | def usage(): 43 | print( 44 | "\n" 45 | + f"""usage: \033[1;33mrbxpy\033[0m [file] [options] -o [gen] 46 | \033[1mOptions:\033[0m 47 | {TAB}\033[1m-v\033[0m show version information 48 | {TAB}\033[1m-vd\033[0m show version number only 49 | {TAB}\033[1m-ast\033[0m show python ast tree before code 50 | {TAB}\033[1m-f\033[0m include standard python functions in generated code 51 | {TAB}\033[1m-fn\033[0m do not include standard python functions in generated code 52 | {TAB}\033[1m-ne\033[0m do not export functions 53 | {TAB}\033[1m-s\033[0m generate a require file 54 | {TAB}\033[1m-r\033[0m use require instead of rbxpy 55 | {TAB}\033[1m-clrtxt\033[0m modify system shell for a better experience 56 | {TAB}\033[1m-o\033[0m output file 57 | {TAB}\033[1m-u\033[0m open this 58 | 59 | \033[1mInputs:\033[0m 60 | {TAB}\033[1m-j\033[0m input is a jupyter notebook 61 | {TAB}\033[1m-py\033[0m input is python file (default) 62 | """ 63 | ) 64 | sys.exit() 65 | 66 | 67 | def version(): 68 | print( 69 | "\033[1;34m" 70 | + "copyright:" 71 | + "\033[0m" 72 | + " roblox-py " 73 | + "\033[1m" 74 | + VERSION 75 | + "\033[0m" 76 | + " licensed under the GNU Affero General Public License by " 77 | + "\033[1m" 78 | + "@AsynchronousAI" 79 | + "\033[0m" 80 | ) 81 | sys.exit(0) 82 | 83 | 84 | def provideerr(err): 85 | global proverr 86 | proverr = err 87 | 88 | 89 | """The main entry point to the translator""" 90 | 91 | 92 | def main(): 93 | """Entry point function to the translator""" 94 | 95 | args = sys.argv[1:] 96 | ast = False 97 | input_filename = "NONE" 98 | out = "NONE" 99 | type = 1 # 1: py->lua, 2: lua->py 100 | includeSTD = False 101 | export = True 102 | skip = False 103 | reqfile = None 104 | useRequire = False 105 | notebook = False 106 | bython = False 107 | 108 | for arg in args: 109 | if skip: 110 | skip = False 111 | continue 112 | 113 | if arg == "-v": 114 | version() 115 | elif arg == "p": 116 | error("Plugin is discontinued") 117 | elif arg == "-vd": 118 | print(VERSION) 119 | sys.exit() 120 | elif arg == "-j": 121 | notebook = True 122 | elif arg == "-b": 123 | bython = True 124 | elif arg == "-c": 125 | continue 126 | elif arg == "-u": 127 | usage() 128 | elif arg == "-f": 129 | includeSTD = True 130 | elif arg == "-s": 131 | reqfile = True 132 | elif arg == "-r": 133 | useRequire = True 134 | elif arg == "-fn": 135 | includeSTD = False 136 | elif arg == "-ne": 137 | export = False 138 | elif arg == "-ast": 139 | ast = True 140 | elif arg == "-py": 141 | type = 1 142 | elif arg == "-o": 143 | out = args[args.index(arg) + 1] 144 | skip = True 145 | elif arg == "-lua": 146 | type = 2 147 | elif arg == "-clrtxt": 148 | # Enable support for ANSI escape sequences 149 | if os.name == "nt": 150 | os.system('cmd /c "setx ENABLE_VIRTUAL_TERMINAL_PROCESSING 1"') 151 | else: 152 | error("Not required on this platform") 153 | sys.exit(0) 154 | else: 155 | if input_filename != "NONE": 156 | error("Unexpected argument: '{}'".format(arg)) 157 | input_filename = arg 158 | 159 | if type == 1: 160 | if (input_filename == "NONE") and not reqfile: 161 | usage() 162 | if (not Path(input_filename).is_file()) and not reqfile: 163 | error("The given filename ('{}') is not a file.".format(input_filename)) 164 | 165 | if not reqfile: 166 | content = None 167 | with open(input_filename, "r") as file: 168 | content = file.read() 169 | 170 | if not content: 171 | error("The input file is empty.") 172 | 173 | translator = Translator(Config(".robloxpy.json"), show_ast=ast) 174 | if reqfile: 175 | reqcode = translator.translate("", True, False, False, True) 176 | if out != "NONE": 177 | with open(out, "w") as file: 178 | file.write(reqcode) 179 | else: 180 | print(reqcode) 181 | sys.exit(0) 182 | else: 183 | if not notebook: 184 | pyright = check_pyright() 185 | if pyright and "-c" not in args: 186 | 187 | def check(): 188 | os.environ["PYRIGHT_PYTHON_FORCE_VERSION"] = "latest" 189 | success = ( 190 | subprocess.Popen(["pyright", input_filename]).wait() == 0 191 | ) 192 | 193 | if not success: 194 | print( 195 | "-----------------------------------------------------" 196 | ) 197 | error("compilation failed") 198 | sys.exit(1) 199 | 200 | threading.Thread(target=check).start() 201 | 202 | lua_code = translator.translate( 203 | content, includeSTD, False, export, False, useRequire, pyright 204 | ) 205 | else: 206 | nb = json.loads(content) 207 | cells = nb["cells"] 208 | code = "" 209 | for i, cell in enumerate(cells): 210 | code += '\n\n""" Cell: ' + str(i + 1) + ': """ \n\n' 211 | if cell["cell_type"] == "code": 212 | code += "\n" 213 | code += "".join(cell["source"]) 214 | elif cell["cell_type"] == "markdown": 215 | code += '\n""" MD:\n\t' + "\t".join(cell["source"]) + '\n"""\n' 216 | lua_code = translator.translate( 217 | code, includeSTD, False, export, False, useRequire, False 218 | ) 219 | 220 | if not ast: 221 | if out != "NONE": 222 | with open(out, "w") as file: 223 | file.write(lua_code) 224 | else: 225 | print(lua_code) 226 | else: 227 | error("please use luau2py instead of rbxpy for lua to python conversion.") 228 | 229 | return 0 230 | 231 | 232 | if __name__ == "__main__": 233 | main() 234 | -------------------------------------------------------------------------------- /src/symbols.py: -------------------------------------------------------------------------------- 1 | """Class for the symbols stack""" 2 | 3 | 4 | class SymbolsStack: 5 | """Class for the symbols stack""" 6 | 7 | def __init__(self): 8 | self.symbols = [[]] 9 | 10 | def add_symbol(self, name): 11 | """Add a new symbol to the curent stack""" 12 | self.symbols[-1].append(name) 13 | 14 | def exists(self, name): 15 | """Check symbol is exists in the current stack""" 16 | for stack in self.symbols: 17 | if name in stack: 18 | return True 19 | return False 20 | 21 | def push(self): 22 | """Push the symbols stack""" 23 | self.symbols.append([]) 24 | 25 | def pop(self): 26 | """Pop the symbols stack""" 27 | self.symbols.pop() 28 | -------------------------------------------------------------------------------- /src/tokenend.py: -------------------------------------------------------------------------------- 1 | """Token end mode""" 2 | 3 | from enum import Enum 4 | 5 | 6 | class TokenEndMode(Enum): 7 | """This enum represents token end mode""" 8 | 9 | LINE_FEED = 0 10 | LINE_CONTINUE = 1 11 | -------------------------------------------------------------------------------- /src/translator.py: -------------------------------------------------------------------------------- 1 | """Python to lua translator class""" 2 | 3 | import ast 4 | import sys 5 | from config import Config 6 | from nodevisitor import NodeVisitor 7 | from log import error 8 | from const import HEADER 9 | from lib import * 10 | import lib 11 | 12 | DEPEND = lib.DEPENDENCY 13 | 14 | 15 | class Translator: 16 | """Python to lua main class translator""" 17 | 18 | def __init__(self, config=None, show_ast=False): 19 | self.config = config if config is not None else Config() 20 | self.show_ast = show_ast 21 | 22 | self.output = [] 23 | 24 | @staticmethod 25 | def reset_dependencies(): 26 | global DEPEND 27 | DEPEND = lib.DEPENDENCY 28 | def translate( 29 | self, 30 | pycode, 31 | fn, 32 | isAPI=False, 33 | export=True, 34 | reqfile=False, 35 | useRequire=False, 36 | pyRight=False, 37 | ): 38 | """Translate python code to lua code""" 39 | global DEPEND 40 | if not reqfile: 41 | if isAPI: 42 | py_ast_tree = ast.parse(pycode) 43 | else: 44 | try: 45 | # code that uses ast 46 | py_ast_tree = ast.parse(pycode) 47 | except SyntaxError as err: 48 | sys.stderr.write( 49 | "\033[1;31m" + "syntax error: " + "\033[0m" + str(err) + "\n" 50 | ) 51 | sys.exit(1) 52 | 53 | visitor = NodeVisitor(config=self.config) 54 | 55 | if self.show_ast: 56 | print(ast.dump(py_ast_tree)) 57 | 58 | visitor.visit(py_ast_tree) 59 | 60 | self.output = visitor.output 61 | 62 | # Remove duplicates from dependencies (list) 63 | dependencies = list(set(visitor.get_dependencies())) 64 | 65 | exports = list(set(visitor.get_exports())) 66 | 67 | if fn: 68 | dependencies.append("fn") 69 | if export and exports != []: 70 | FOOTER = "\n\n--> exports\n" 71 | FOOTER += 'if not script:IsA("BaseScript") then\n\treturn {\n' 72 | for export in exports: 73 | FOOTER += f'\t\t["{export}"] = {export},\n' 74 | FOOTER += "\t}\nend" 75 | else: 76 | FOOTER = "" 77 | 78 | if reqfile: 79 | dependencies = [ 80 | "class", 81 | "dict", 82 | "fn", 83 | "complex", 84 | ] 85 | DEPEND = "" 86 | if not useRequire: 87 | for depend in dependencies: 88 | # set 89 | if depend == "complex": 90 | DEPEND += COMPLEX 91 | elif depend == "dict": 92 | DEPEND += DICT 93 | elif depend == "class": 94 | DEPEND += CLASS 95 | elif depend == "fn": 96 | DEPEND += FN 97 | elif depend == "generator": 98 | DEPEND += GENERATOR 99 | else: 100 | error( 101 | "Auto-generated dependency unhandled '{}', please report this issue on Discord or Github".format( 102 | depend 103 | ) 104 | ) 105 | 106 | if reqfile: 107 | allDepends = "" 108 | for depend in lib.libs: 109 | allDepends += f'["{depend}"] = {depend},' 110 | DEPEND += "\n\nreturn {" + allDepends + "}\n" 111 | return DEPEND 112 | 113 | CODE = self.to_code() 114 | ERRS = "\n\n--> error handling\n" 115 | 116 | for i in errs: 117 | if ("error(" + i + "(") in CODE: 118 | ERRS += f"""function {i}(errorMessage) 119 | return ("[roblox-py] {i}: " .. errorMessage) 120 | end 121 | """ 122 | 123 | for i in lib.libs: 124 | if i in CODE: 125 | DEPEND += f"\n{i} = py.{i}" 126 | 127 | DEPEND += "\n\n--> code start\n" 128 | 129 | return HEADER + ERRS + DEPEND + CODE + FOOTER 130 | 131 | def to_code(self, code=None, indent=0): 132 | """Create a lua code from the compiler output""" 133 | code = code if code is not None else self.output 134 | 135 | def add_indentation(line): 136 | """Add indentation to the given line""" 137 | indentation_width = 4 138 | indentation_space = " " 139 | 140 | indent_copy = max(indent, 0) 141 | 142 | return indentation_space * indentation_width * indent_copy + line 143 | 144 | lines = [] 145 | for line in code: 146 | if isinstance(line, str): 147 | lines.append(add_indentation(line)) 148 | elif isinstance(line, list): 149 | sub_code = self.to_code(line, indent + 1) 150 | lines.append(sub_code) 151 | 152 | return "\n".join(lines) 153 | 154 | @staticmethod 155 | def get_luainit(): # Return STDlib 156 | return """""" 157 | -------------------------------------------------------------------------------- /src/unary.py: -------------------------------------------------------------------------------- 1 | """Unary operation description""" 2 | 3 | import ast 4 | 5 | _DEFAULT_UN_FORMAT = "{operation}{value}" 6 | 7 | 8 | class UnaryOperationDesc: 9 | """Unary operation description""" 10 | 11 | OPERATION = { 12 | ast.USub: { 13 | "value": "-", 14 | "format": _DEFAULT_UN_FORMAT, 15 | }, 16 | ast.UAdd: { 17 | "value": "", 18 | "format": _DEFAULT_UN_FORMAT, 19 | }, 20 | ast.Not: { 21 | "value": "not", 22 | "format": "not {value}", 23 | }, 24 | ast.Invert: { 25 | "value": "~", 26 | "format": "bit32.bnot({value})", 27 | }, 28 | } 29 | --------------------------------------------------------------------------------