Repository: Mercerenies/gdlisp Branch: master Commit: 04b1395a46ab Files: 267 Total size: 1.7 MB Directory structure: gitextract_8xnoo2xc/ ├── .github/ │ └── workflows/ │ ├── style-clippy.yml │ ├── style-docs.yml │ ├── test-Godotv3.3.3-Ubuntu.yml │ ├── test-Godotv3.4-Ubuntu.yml │ ├── test-Godotv3.5-Minimal-Ubuntu.yml │ ├── test-Godotv3.5-Mono-Ubuntu.yml │ └── test-Godotv3.5-Ubuntu.yml ├── .gitignore ├── .readthedocs.yaml ├── COPYING ├── Cargo.toml ├── GDLisp.lisp ├── MacroServer/ │ ├── TestLoadedFile.gd │ ├── main.gd │ ├── main.tscn │ └── project.godot ├── README.md ├── Rakefile ├── build.rs ├── doc/ │ ├── README.md │ ├── internal/ │ │ ├── CompilationStages.md │ │ └── README.md │ └── readthedocs/ │ ├── Makefile │ ├── conf.py │ ├── index.rst │ ├── make.bat │ ├── reference/ │ │ ├── classes.rst │ │ ├── command-line.rst │ │ ├── datatypes.rst │ │ ├── expressions.rst │ │ ├── imports.rst │ │ ├── index.rst │ │ ├── lambda-lists.rst │ │ ├── macros.rst │ │ ├── parser.rst │ │ ├── quoting.rst │ │ ├── source-file.rst │ │ └── standard-library/ │ │ ├── functions.rst │ │ ├── index.rst │ │ ├── macros.rst │ │ ├── types.rst │ │ └── values.rst │ ├── requirements.txt │ └── tutorial/ │ ├── classes.rst │ ├── control-flow.rst │ ├── index.rst │ ├── lisping.rst │ ├── macros.rst │ ├── modules.rst │ └── what-now.rst ├── minimal.txt ├── src/ │ ├── command_line.rs │ ├── compile/ │ │ ├── args.rs │ │ ├── body/ │ │ │ ├── builder.rs │ │ │ ├── class_initializer.rs │ │ │ ├── class_scope.rs │ │ │ ├── mod.rs │ │ │ ├── super_proxy.rs │ │ │ └── synthetic_field.rs │ │ ├── constant.rs │ │ ├── error.rs │ │ ├── factory.rs │ │ ├── frame.rs │ │ ├── import.rs │ │ ├── mod.rs │ │ ├── names/ │ │ │ ├── contextual.rs │ │ │ ├── fresh.rs │ │ │ ├── generator.rs │ │ │ ├── mod.rs │ │ │ ├── registered.rs │ │ │ └── reserved.rs │ │ ├── preload_resolver.rs │ │ ├── resource_type.rs │ │ ├── special_form/ │ │ │ ├── closure.rs │ │ │ ├── flet.rs │ │ │ ├── lambda.rs │ │ │ ├── lambda_class.rs │ │ │ ├── lambda_vararg/ │ │ │ │ ├── builder.rs │ │ │ │ └── mod.rs │ │ │ ├── let_block.rs │ │ │ └── mod.rs │ │ ├── stateful.rs │ │ ├── stmt_wrapper.rs │ │ └── symbol_table/ │ │ ├── call_magic/ │ │ │ ├── mod.rs │ │ │ └── table.rs │ │ ├── function_call.rs │ │ ├── inner.rs │ │ ├── local_var.rs │ │ └── mod.rs │ ├── gdscript/ │ │ ├── arglist.rs │ │ ├── class_extends.rs │ │ ├── decl.rs │ │ ├── expr.rs │ │ ├── expr_wrapper.rs │ │ ├── inner_class.rs │ │ ├── library/ │ │ │ ├── cell.rs │ │ │ ├── class_loader.rs │ │ │ ├── constant_loader.rs │ │ │ ├── gdnative/ │ │ │ │ ├── api_type.rs │ │ │ │ ├── argument.rs │ │ │ │ ├── class.rs │ │ │ │ ├── gdnative_enum.rs │ │ │ │ ├── method.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── property.rs │ │ │ │ └── signal.rs │ │ │ ├── magic.rs │ │ │ └── mod.rs │ │ ├── literal.rs │ │ ├── metadata.rs │ │ ├── mod.rs │ │ ├── op.rs │ │ ├── pattern.rs │ │ ├── spacing.rs │ │ └── stmt.rs │ ├── graph/ │ │ ├── mod.rs │ │ ├── tarjan.rs │ │ └── top_sort.rs │ ├── ir/ │ │ ├── access_type.rs │ │ ├── arglist/ │ │ │ ├── constructor.rs │ │ │ ├── error.rs │ │ │ ├── general.rs │ │ │ ├── mod.rs │ │ │ ├── ordinary.rs │ │ │ ├── parser.rs │ │ │ ├── simple.rs │ │ │ └── vararg.rs │ │ ├── bootstrapping.rs │ │ ├── call_name.rs │ │ ├── classification.rs │ │ ├── closure_names.rs │ │ ├── decl.rs │ │ ├── declaration_table.rs │ │ ├── depends.rs │ │ ├── export.rs │ │ ├── expr.rs │ │ ├── identifier.rs │ │ ├── import.rs │ │ ├── incremental.rs │ │ ├── literal.rs │ │ ├── loops/ │ │ │ ├── error.rs │ │ │ └── mod.rs │ │ ├── macros.rs │ │ ├── main_function.rs │ │ ├── mod.rs │ │ ├── modifier/ │ │ │ ├── class.rs │ │ │ ├── constant.rs │ │ │ ├── declare.rs │ │ │ ├── enums.rs │ │ │ ├── file.rs │ │ │ ├── function.rs │ │ │ ├── instance_method.rs │ │ │ ├── macros.rs │ │ │ ├── magic.rs │ │ │ ├── mod.rs │ │ │ ├── var.rs │ │ │ └── visibility.rs │ │ ├── quasiquote.rs │ │ ├── scope/ │ │ │ ├── decl.rs │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ └── name_table/ │ │ │ ├── builder.rs │ │ │ └── mod.rs │ │ ├── special_form/ │ │ │ ├── access_slot.rs │ │ │ ├── assignment.rs │ │ │ ├── local_binding.rs │ │ │ └── mod.rs │ │ └── special_ref.rs │ ├── lib.rs │ ├── main.rs │ ├── optimize/ │ │ ├── gdscript/ │ │ │ ├── assignment.rs │ │ │ ├── basic_math_ops.rs │ │ │ ├── constant.rs │ │ │ ├── constant_conditional_branch.rs │ │ │ ├── dead_code_elimination.rs │ │ │ ├── dead_decl_elimination.rs │ │ │ ├── dead_var_elimination.rs │ │ │ ├── direct_var_substitute.rs │ │ │ ├── else_then_if_fold.rs │ │ │ ├── expr_walker.rs │ │ │ ├── mod.rs │ │ │ ├── noop.rs │ │ │ ├── redundant_assignment_elimination.rs │ │ │ ├── stmt_walker.rs │ │ │ ├── ternary_if_fold.rs │ │ │ └── variables.rs │ │ ├── ir/ │ │ │ ├── expr_walker.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── parser.lalrpop │ ├── parser_test.rs │ ├── pipeline/ │ │ ├── can_load.rs │ │ ├── config.rs │ │ ├── error.rs │ │ ├── loader.rs │ │ ├── mod.rs │ │ ├── resolver.rs │ │ ├── source.rs │ │ ├── stdlib_unit.rs │ │ └── translation_unit.rs │ ├── repl.rs │ ├── runner/ │ │ ├── godot.rs │ │ ├── into_gd_file.rs │ │ ├── macro_server/ │ │ │ ├── command.rs │ │ │ ├── lazy.rs │ │ │ ├── mod.rs │ │ │ ├── named_file_server.rs │ │ │ └── response.rs │ │ ├── mod.rs │ │ ├── named_file.rs │ │ ├── path.rs │ │ └── version.rs │ ├── sxp/ │ │ ├── ast.rs │ │ ├── dotted.rs │ │ ├── literal.rs │ │ ├── mod.rs │ │ ├── reify/ │ │ │ ├── mod.rs │ │ │ └── pretty.rs │ │ ├── string.rs │ │ └── syntax.rs │ └── util/ │ ├── debug_wrapper.rs │ ├── group_by.rs │ ├── lattice.rs │ ├── lazy.rs │ ├── mod.rs │ ├── one.rs │ ├── prefix_matcher.rs │ └── recursive.rs └── tests/ ├── integration_tests.rs └── test/ ├── builtin_function_test.rs ├── class_test.rs ├── collection_conversion_test.rs ├── common/ │ ├── import.rs │ └── mod.rs ├── concurrency_test.rs ├── cond_if_test.rs ├── const_test.rs ├── declaration_test.rs ├── dependencies_test.rs ├── dictionary_test.rs ├── enum_test.rs ├── error_test.rs ├── even_odd_test.rs ├── factorial_test.rs ├── flet_test.rs ├── floating_test.rs ├── for_test.rs ├── import_test.rs ├── labels_test.rs ├── lambda_class_test.rs ├── lambda_test.rs ├── lazy_val_test.rs ├── let_var_test.rs ├── list_operator_test.rs ├── macro_test.rs ├── meta_test.rs ├── mod.rs ├── name_translation_test.rs ├── object_test.rs ├── operator_test.rs ├── quoting_test.rs ├── signal_test.rs ├── simple_expr_test.rs ├── string_test.rs ├── type_checking_test.rs ├── typecast_test.rs └── while_test.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/style-clippy.yml ================================================ name: Clippy Style Checker on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: style-checker: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Install Clippy run: | rustup update rustup component add clippy - name: Build run: rake build_rs[--verbose] # Only build Rust, not stdlib - name: Run Clippy run: rake clippy[--verbose] ================================================ FILE: .github/workflows/style-docs.yml ================================================ name: Rustdoc Generation on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: style-checker: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Generate Documentation run: rake doc ================================================ FILE: .github/workflows/test-Godotv3.3.3-Ubuntu.yml ================================================ name: "Ubuntu 20.04 (Godot v3.3.3)" on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: test-suite: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Install Godot run: | mkdir godot-build cd godot-build wget https://downloads.tuxfamily.org/godotengine/3.3.3/Godot_v3.3.3-stable_linux_headless.64.zip unzip Godot_v3.3.3-stable_linux_headless.64.zip ln -s Godot_v3.3.3-stable_linux_headless.64 godot ls -l cd .. - name: Build run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake build[--verbose] - name: Run Standard Test Suite run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake 'test[--verbose]' ================================================ FILE: .github/workflows/test-Godotv3.4-Ubuntu.yml ================================================ name: "Ubuntu 20.04 (Godot v3.4)" on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: test-suite: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Install Godot run: | mkdir godot-build cd godot-build wget https://downloads.tuxfamily.org/godotengine/3.4/Godot_v3.4-stable_linux_headless.64.zip unzip Godot_v3.4-stable_linux_headless.64.zip ln -s Godot_v3.4-stable_linux_headless.64 godot ls -l cd .. - name: Build run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake build[--verbose] - name: Run Standard Test Suite run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake 'test[--verbose]' ================================================ FILE: .github/workflows/test-Godotv3.5-Minimal-Ubuntu.yml ================================================ name: "Ubuntu 20.04 (Godot v3.5 Minimal)" on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: test-suite: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Install Godot run: | mkdir godot-build cd godot-build wget https://www.dropbox.com/s/9zi0ij7cqetaw34/godot_server.x11.opt.tools.64.zip?dl=1 -O godot_server.x11.opt.tools.64.zip unzip godot_server.x11.opt.tools.64.zip ln -s godot_server.x11.opt.tools.64 godot ls -l cd .. - name: Build run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake build[--verbose] - name: Run Standard Test Suite run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake 'test[--verbose]' ================================================ FILE: .github/workflows/test-Godotv3.5-Mono-Ubuntu.yml ================================================ name: "Ubuntu 20.04 (Godot v3.5 Mono)" on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: test-suite: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Install Godot run: | mkdir godot-build cd godot-build wget https://downloads.tuxfamily.org/godotengine/3.5/mono/Godot_v3.5-stable_mono_linux_headless_64.zip unzip Godot_v3.5-stable_mono_linux_headless_64.zip ln -s Godot_v3.5-stable_mono_linux_headless_64/Godot_v3.5-stable_mono_linux_headless.64 godot ls -l . ls -l Godot_v3.5-stable_mono_linux_headless_64/ cd .. - name: Build run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake build[--verbose] - name: Run Standard Test Suite run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake 'test[--verbose]' ================================================ FILE: .github/workflows/test-Godotv3.5-Ubuntu.yml ================================================ name: "Ubuntu 20.04 (Godot v3.5)" on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: test-suite: runs-on: "ubuntu-20.04" steps: - uses: actions/checkout@v2 - name: Install Godot run: | mkdir godot-build cd godot-build wget https://downloads.tuxfamily.org/godotengine/3.5/Godot_v3.5-stable_linux_headless.64.zip unzip Godot_v3.5-stable_linux_headless.64.zip ln -s Godot_v3.5-stable_linux_headless.64 godot ls -l cd .. - name: Build run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake build[--verbose] - name: Run Standard Test Suite run: | PATH="$PWD/godot-build/:$PATH" echo "$PATH" rake 'test[--verbose]' ================================================ FILE: .gitignore ================================================ /target /bin *~ GDLisp.gd GDLisp.msgpack # Generated file that is always identical to /GDLisp.gd /MacroServer/GDLisp.gd /tmp/ /etc/ /doc/readthedocs/_build/ ================================================ FILE: .readthedocs.yaml ================================================ version: 2 build: os: ubuntu-20.04 tools: python: "3.9" sphinx: configuration: doc/readthedocs/conf.py python: install: - requirements: doc/readthedocs/requirements.txt ================================================ FILE: COPYING ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: Cargo.toml ================================================ [package] name = "gdlisp" version = "1.0.0" authors = ["Silvio Mayolo "] edition = "2018" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] lalrpop-util = "0.19.0" regex = "1" phf = { version = "0.8.0", features = ["macros"] } ordered-float = "2.0.0" tempfile = "3.0.7" dyn-clone = "1.0.4" getopts = "0.2" walkdir = "2" json = "0.11.13" lazy_static = "1.4.0" serde = { version = "1.0", features = ["derive"] } rmp-serde = "0.14.4" serde_json = "1.0.82" [build-dependencies] lalrpop = { version = "0.19.0", features = ["lexer"] } ================================================ FILE: GDLisp.lisp ================================================ ;;;; Copyright 2023 Silvio Mayolo ;;;; ;;;; This file is part of GDLisp. ;;;; ;;;; GDLisp is free software: you can redistribute it and/or modify it ;;;; under the terms of the GNU General Public License as published by ;;;; the Free Software Foundation, either version 3 of the License, or ;;;; (at your option) any later version. ;;;; ;;;; GDLisp is distributed in the hope that it will be useful, but ;;;; WITHOUT ANY WARRANTY; without even the implied warranty of ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;;;; General Public License for more details. ;;;; ;;;; You should have received a copy of the GNU General Public License ;;;; along with GDLisp. If not, see . ;;;; ;;;; As a special exception to the terms of the GNU General Public ;;;; License, you may use this GDLisp.lisp source file in your own ;;;; projects without restriction. ;;;; Library file for GDLisp. This file must be included as a singleton ;;;; in any project which uses GDLisp compiled files. (sys/nostdlib) ;;; Global GDLisp constants and enums (defconst nil ()) (defconst GODOT-VERSION (sys/special-ref godot-version)) (sys/declare superglobal GDLisp public) ; This file :) (sys/bootstrap constants) (sys/bootstrap constant-enums) (sys/declare superglobal (PI PI) public) (sys/declare superglobal (TAU TAU) public) (sys/declare superglobal (INF INF) public) (defenum ConnectFlags (DEFERRED 1) (PERSIST 2) (ONESHOT 4) (REFERENCE_COUNTED 8)) (defenum Notification (POSTINITIALIZE 0) (PREDELETE 1)) ;; Note: Godot doesn't consider the below names constant for some ;; reason, since they're apparently defined *on* `Object` in some ;; weird way. So we have to hardcode the numerical values in the ;; enums. We have test cases to make sure these numbers are consistent ;; with the Godot values for the same. (sys/declare value CONNECT_DEFERRED public) (sys/declare value CONNECT_PERSIST public) (sys/declare value CONNECT_ONESHOT public) (sys/declare value CONNECT_REFERENCE_COUNTED public) (sys/declare value NOTIFICATION_POSTINITIALIZE public) (sys/declare value NOTIFICATION_PREDELETE public) ;;; GDScript global types (sys/bootstrap non-singleton-types) (sys/bootstrap singleton-types) ;;; GDLisp main class initialization (defclass _GDLisp (Node) main (defvar __gdlisp_Global_name_generator (sys/FreshNameGenerator:new [] 0)) (defvar __gdlisp_Global_symbol_table {}) ;; Primitive types (defvar Null (PrimitiveType:new TYPE_NIL)) (defvar Bool (PrimitiveType:new TYPE_BOOL)) (defvar Int (PrimitiveType:new TYPE_INT)) (defvar Float (PrimitiveType:new TYPE_REAL)) (defvar String (PrimitiveType:new TYPE_STRING)) (defvar Vector2 (Vector2PrimitiveType:new)) (defvar Rect2 (PrimitiveType:new TYPE_RECT2)) (defvar Vector3 (Vector3PrimitiveType:new)) (defvar Transform2D (Transform2DPrimitiveType:new)) (defvar Plane (PlanePrimitiveType:new)) (defvar Quat (QuatPrimitiveType:new)) (defvar AABB (PrimitiveType:new TYPE_AABB)) (defvar Basis (BasisPrimitiveType:new)) (defvar Transform (TransformPrimitiveType:new)) (defvar Color (ColorPrimitiveType:new)) (defvar NodePath (PrimitiveType:new TYPE_NODE_PATH)) (defvar RID (PrimitiveType:new TYPE_RID)) (defvar Object (ObjectPrimitiveType:new)) (defvar Dictionary (PrimitiveType:new TYPE_DICTIONARY)) (defvar Array (PrimitiveType:new TYPE_ARRAY)) (defvar PoolByteArray (PrimitiveType:new TYPE_RAW_ARRAY)) (defvar PoolIntArray (PrimitiveType:new TYPE_INT_ARRAY)) (defvar PoolRealArray (PrimitiveType:new TYPE_REAL_ARRAY)) (defvar PoolStringArray (PrimitiveType:new TYPE_STRING_ARRAY)) (defvar PoolVector2Array (PrimitiveType:new TYPE_VECTOR2_ARRAY)) (defvar PoolVector3Array (PrimitiveType:new TYPE_VECTOR3_ARRAY)) (defvar PoolColorArray (PrimitiveType:new TYPE_COLOR_ARRAY)) ;; Synthetic types (defvar Any (AnyType:new)) (defvar AnyRef (AnyRefType:new)) (defvar AnyVal (AnyValType:new)) (defvar Number (NumberType:new)) (defvar BaseArray (BaseArrayType:new)) (defvar Nothing (NothingType:new)) ;; GDScript native types lookup table (defvar __gdlisp_Global_native_types_lookup) (defvar __gdlisp_Global_primitive_types_lookup) (sys/bootstrap singleton-backing-types) (defn _init () (set self:__gdlisp_Global_primitive_types_lookup {TYPE_NIL @Null TYPE_BOOL @Bool TYPE_INT @Int TYPE_REAL @Float TYPE_STRING @String TYPE_VECTOR2 @Vector2 TYPE_RECT2 @Rect2 TYPE_VECTOR3 @Vector3 TYPE_TRANSFORM2D @Transform2D TYPE_PLANE @Plane TYPE_QUAT @Quat TYPE_AABB @AABB TYPE_BASIS @Basis TYPE_TRANSFORM @Transform TYPE_COLOR @Color TYPE_NODE_PATH @NodePath TYPE_RID @RID TYPE_OBJECT @Object TYPE_DICTIONARY @Dictionary TYPE_ARRAY @Array TYPE_RAW_ARRAY @PoolByteArray TYPE_INT_ARRAY @PoolIntArray TYPE_REAL_ARRAY @PoolRealArray TYPE_STRING_ARRAY @PoolStringArray TYPE_VECTOR2_ARRAY @PoolVector2Array TYPE_VECTOR3_ARRAY @PoolVector3Array TYPE_COLOR_ARRAY @PoolColorArray}) (sys/bootstrap native-types-table) (set (elt self:__gdlisp_Global_native_types_lookup "Object") @Object)) (defn typeof (value) (let ((t ((literally typeof) value))) (cond ((/= t TYPE_OBJECT) (elt self:__gdlisp_Global_primitive_types_lookup t)) ((value:get_script)) (#t (self:__gdlisp_Global_native_types_lookup:get (value:get_class) self:Any)))))) ;;; Public support classes (defclass Cons (Reference) (defvar car) (defvar cdr) (defn _init (@car @cdr) (self:set-meta "__gdlisp_Primitive_Cons" 1)) (defn (get caar) () self:car:car) (defn (get cadr) () self:car:cdr) (defn (get cdar) () self:cdr:car) (defn (get cddr) () self:cdr:cdr) (defn (get caaar) () self:car:car:car) (defn (get caadr) () self:car:car:cdr) (defn (get cadar) () self:car:cdr:car) (defn (get caddr) () self:car:cdr:cdr) (defn (get cdaar) () self:cdr:car:car) (defn (get cdadr) () self:cdr:car:cdr) (defn (get cddar) () self:cdr:cdr:car) (defn (get cdddr) () self:cdr:cdr:cdr)) (defclass Function (Reference) (defvar __gdlisp_is_function #t) (defvar __gdlisp_required 0) (defvar __gdlisp_optional 0) (defvar __gdlisp_rest 1) ;; Constants for the different types of "rest" arguments a function ;; can take. (i.e. the valid values for __gdlisp_rest) (defconst __gdlisp_vararg_no 0) (defconst __gdlisp_vararg_rest 1) (defconst __gdlisp_vararg_arr 2)) (defclass Cell (Reference) (defvar contents) (defn _init (@contents))) (defclass Symbol (Reference) ;; Note: This will be obsolete once we have StringName in GDScript, ;; which seems to be coming in Godot 4. For now, this manual wrapper ;; stores symbols in the least efficient way possible. (defvar __gdlisp_contents) (defn _init (@__gdlisp_contents) (self:set-meta "__gdlisp_Primitive_Symbol" 1))) (defclass sys/FreshNameGenerator (Reference) ;; This is meant to be identical to FreshNameGenerator in the Rust ;; source. We want to be able to consistently generate names in the ;; same way on both Rust and Godot and to be able to communicate ;; FreshNameGenerator state between the two (via to_json / ;; from_json) (defconst DEFAULT_PREFIX "_G") (defvar reserved) (defvar index) (defn _init (@reserved @index)) (defn generate () (self:generate_with sys/FreshNameGenerator:DEFAULT_PREFIX)) (defn generate_with (prefix) (let ((name ("{}_{}":format [prefix self:index] "{}"))) (set self:index (+ self:index 1)) (cond ((member? name self:reserved) (self:generate_with prefix)) (#t name)))) (defn to_json () {"reserved" self:reserved "index" self:index}) (defn from_json (json) static (sys/FreshNameGenerator:new (elt json "reserved") (elt json "index")))) ;;; GDLisp type constants and support types (defclass GDLispSpecialType (Reference) private) (defclass PrimitiveType (GDLispSpecialType) private (defvar primitive-value) (defn _init (@primitive-value)) (defn satisfies? (value) (= ((literally typeof) value) self:primitive-value))) (defclass ObjectPrimitiveType (PrimitiveType) private (defn _init () (super TYPE_OBJECT))) ;; NOTE: Due to current Godot bugs, we can't actually define this, ;; since GDScript fails to parse constructor calls on Object directly. ;; See https://github.com/godotengine/godot/issues/41462. Has been ;; fixed in 4.0 and it doesn't look like they're planning to backport. ;; ;; (defn new () ;; ((literally Object):new))) (defclass Vector2PrimitiveType (PrimitiveType) private (defconst AXIS_X 0) (defconst AXIS_Y 1) (defconst ZERO V{0 0}) (defconst ONE V{1 1}) (defconst INF V{INF INF}) (defconst LEFT V{-1 0}) (defconst RIGHT V{1 0}) (defconst UP V{0 -1}) (defconst DOWN V{0 1}) (defn _init () (super TYPE_VECTOR2))) (defclass Vector3PrimitiveType (PrimitiveType) private (defconst AXIS_X 0) (defconst AXIS_Y 1) (defconst AXIS_Z 2) (defconst ZERO V{0 0 0}) (defconst ONE V{1 1 1}) (defconst INF V{INF INF INF}) (defconst LEFT V{-1 0 0}) (defconst RIGHT V{1 0 0}) (defconst UP V{0 1 0}) (defconst DOWN V{0 -1 0}) (defconst FORWARD V{0 0 -1}) (defconst BACK V{0 0 1}) (defn _init () (super TYPE_VECTOR3))) (defclass Transform2DPrimitiveType (PrimitiveType) private (defconst IDENTITY (Transform2D V{1 0} V{0 1} V{0 0})) (defconst FLIP_X (Transform2D V{-1 0} V{0 1} V{0 0})) (defconst FLIP_Y (Transform2D V{1 0} V{0 -1} V{0 0})) (defn _init () (super TYPE_TRANSFORM2D))) (defclass PlanePrimitiveType (PrimitiveType) private (defconst PLANE_YZ (Plane 1 0 0 0)) (defconst PLANE_XZ (Plane 0 1 0 0)) (defconst PLANE_XY (Plane 0 0 1 0)) (defn _init () (super TYPE_PLANE))) (defclass QuatPrimitiveType (PrimitiveType) private (defconst IDENTITY (Quat 0 0 0 1)) (defn _init () (super TYPE_QUAT))) (defclass BasisPrimitiveType (PrimitiveType) private (defconst IDENTITY (Basis V{1 0 0} V{0 1 0} V{0 0 1})) (defconst FLIP_X (Basis V{-1 0 0} V{0 1 0} V{0 0 1})) (defconst FLIP_Y (Basis V{1 0 0} V{0 -1 0} V{0 0 -1})) (defconst FLIP_Z (Basis V{1 0 0} V{0 -1 0} V{0 0 -1})) (defn _init () (super TYPE_BASIS))) (defclass TransformPrimitiveType (PrimitiveType) private (defconst IDENTITY (Transform V{1 0 0} V{0 1 0} V{0 0 1} V{0 0 0})) (defconst FLIP_X (Transform V{-1 0 0} V{0 1 0} V{0 0 1} V{0 0 0})) (defconst FLIP_Y (Transform V{1 0 0} V{0 -1 0} V{0 0 -1} V{0 0 0})) (defconst FLIP_Z (Transform V{1 0 0} V{0 -1 0} V{0 0 -1} V{0 0 0})) (defn _init () (super TYPE_TRANSFORM))) (defclass ColorPrimitiveType (PrimitiveType) private (defconst aliceblue (Color 0.941176 0.972549 1 1)) (defconst antiquewhite (Color 0.980392 0.921569 0.843137 1)) (defconst aqua (Color 0 1 1 1)) (defconst aquamarine (Color 0.498039 1 0.831373 1)) (defconst azure (Color 0.941176 1 1 1)) (defconst beige (Color 0.960784 0.960784 0.862745 1)) (defconst bisque (Color 1 0.894118 0.768627 1)) (defconst black (Color 0 0 0 1)) (defconst blanchedalmond (Color 1 0.921569 0.803922 1)) (defconst blue (Color 0 0 1 1)) (defconst blueviolet (Color 0.541176 0.168627 0.886275 1)) (defconst brown (Color 0.647059 0.164706 0.164706 1)) (defconst burlywood (Color 0.870588 0.721569 0.529412 1)) (defconst cadetblue (Color 0.372549 0.619608 0.627451 1)) (defconst chartreuse (Color 0.498039 1 0 1)) (defconst chocolate (Color 0.823529 0.411765 0.117647 1)) (defconst coral (Color 1 0.498039 0.313726 1)) (defconst cornflower (Color 0.392157 0.584314 0.929412 1)) (defconst cornsilk (Color 1 0.972549 0.862745 1)) (defconst crimson (Color 0.862745 0.0784314 0.235294 1)) (defconst cyan (Color 0 1 1 1)) (defconst darkblue (Color 0 0 0.545098 1)) (defconst darkcyan (Color 0 0.545098 0.545098 1)) (defconst darkgoldenrod (Color 0.721569 0.52549 0.0431373 1)) (defconst darkgray (Color 0.662745 0.662745 0.662745 1)) (defconst darkgreen (Color 0 0.392157 0 1)) (defconst darkkhaki (Color 0.741176 0.717647 0.419608 1)) (defconst darkmagenta (Color 0.545098 0 0.545098 1)) (defconst darkolivegreen (Color 0.333333 0.419608 0.184314 1)) (defconst darkorange (Color 1 0.54902 0 1)) (defconst darkorchid (Color 0.6 0.196078 0.8 1)) (defconst darkred (Color 0.545098 0 0 1)) (defconst darksalmon (Color 0.913725 0.588235 0.478431 1)) (defconst darkseagreen (Color 0.560784 0.737255 0.560784 1)) (defconst darkslateblue (Color 0.282353 0.239216 0.545098 1)) (defconst darkslategray (Color 0.184314 0.309804 0.309804 1)) (defconst darkturquoise (Color 0 0.807843 0.819608 1)) (defconst darkviolet (Color 0.580392 0 0.827451 1)) (defconst deeppink (Color 1 0.0784314 0.576471 1)) (defconst deepskyblue (Color 0 0.74902 1 1)) (defconst dimgray (Color 0.411765 0.411765 0.411765 1)) (defconst dodgerblue (Color 0.117647 0.564706 1 1)) (defconst firebrick (Color 0.698039 0.133333 0.133333 1)) (defconst floralwhite (Color 1 0.980392 0.941176 1)) (defconst forestgreen (Color 0.133333 0.545098 0.133333 1)) (defconst fuchsia (Color 1 0 1 1)) (defconst gainsboro (Color 0.862745 0.862745 0.862745 1)) (defconst ghostwhite (Color 0.972549 0.972549 1 1)) (defconst gold (Color 1 0.843137 0 1)) (defconst goldenrod (Color 0.854902 0.647059 0.12549 1)) (defconst gray (Color 0.745098 0.745098 0.745098 1)) (defconst green (Color 0 1 0 1)) (defconst greenyellow (Color 0.678431 1 0.184314 1)) (defconst honeydew (Color 0.941176 1 0.941176 1)) (defconst hotpink (Color 1 0.411765 0.705882 1)) (defconst indianred (Color 0.803922 0.360784 0.360784 1)) (defconst indigo (Color 0.294118 0 0.509804 1)) (defconst ivory (Color 1 1 0.941176 1)) (defconst khaki (Color 0.941176 0.901961 0.54902 1)) (defconst lavender (Color 0.901961 0.901961 0.980392 1)) (defconst lavenderblush (Color 1 0.941176 0.960784 1)) (defconst lawngreen (Color 0.486275 0.988235 0 1)) (defconst lemonchiffon (Color 1 0.980392 0.803922 1)) (defconst lightblue (Color 0.678431 0.847059 0.901961 1)) (defconst lightcoral (Color 0.941176 0.501961 0.501961 1)) (defconst lightcyan (Color 0.878431 1 1 1)) (defconst lightgoldenrod (Color 0.980392 0.980392 0.823529 1)) (defconst lightgray (Color 0.827451 0.827451 0.827451 1)) (defconst lightgreen (Color 0.564706 0.933333 0.564706 1)) (defconst lightpink (Color 1 0.713726 0.756863 1)) (defconst lightsalmon (Color 1 0.627451 0.478431 1)) (defconst lightseagreen (Color 0.12549 0.698039 0.666667 1)) (defconst lightskyblue (Color 0.529412 0.807843 0.980392 1)) (defconst lightslategray (Color 0.466667 0.533333 0.6 1)) (defconst lightsteelblue (Color 0.690196 0.768627 0.870588 1)) (defconst lightyellow (Color 1 1 0.878431 1)) (defconst lime (Color 0 1 0 1)) (defconst limegreen (Color 0.196078 0.803922 0.196078 1)) (defconst linen (Color 0.980392 0.941176 0.901961 1)) (defconst magenta (Color 1 0 1 1)) (defconst maroon (Color 0.690196 0.188235 0.376471 1)) (defconst mediumaquamarine (Color 0.4 0.803922 0.666667 1)) (defconst mediumblue (Color 0 0 0.803922 1)) (defconst mediumorchid (Color 0.729412 0.333333 0.827451 1)) (defconst mediumpurple (Color 0.576471 0.439216 0.858824 1)) (defconst mediumseagreen (Color 0.235294 0.701961 0.443137 1)) (defconst mediumslateblue (Color 0.482353 0.407843 0.933333 1)) (defconst mediumspringgreen (Color 0 0.980392 0.603922 1)) (defconst mediumturquoise (Color 0.282353 0.819608 0.8 1)) (defconst mediumvioletred (Color 0.780392 0.0823529 0.521569 1)) (defconst midnightblue (Color 0.0980392 0.0980392 0.439216 1)) (defconst mintcream (Color 0.960784 1 0.980392 1)) (defconst mistyrose (Color 1 0.894118 0.882353 1)) (defconst moccasin (Color 1 0.894118 0.709804 1)) (defconst navajowhite (Color 1 0.870588 0.678431 1)) (defconst navyblue (Color 0 0 0.501961 1)) (defconst oldlace (Color 0.992157 0.960784 0.901961 1)) (defconst olive (Color 0.501961 0.501961 0 1)) (defconst olivedrab (Color 0.419608 0.556863 0.137255 1)) (defconst orange (Color 1 0.647059 0 1)) (defconst orangered (Color 1 0.270588 0 1)) (defconst orchid (Color 0.854902 0.439216 0.839216 1)) (defconst palegoldenrod (Color 0.933333 0.909804 0.666667 1)) (defconst palegreen (Color 0.596078 0.984314 0.596078 1)) (defconst paleturquoise (Color 0.686275 0.933333 0.933333 1)) (defconst palevioletred (Color 0.858824 0.439216 0.576471 1)) (defconst papayawhip (Color 1 0.937255 0.835294 1)) (defconst peachpuff (Color 1 0.854902 0.72549 1)) (defconst peru (Color 0.803922 0.521569 0.247059 1)) (defconst pink (Color 1 0.752941 0.796078 1)) (defconst plum (Color 0.866667 0.627451 0.866667 1)) (defconst powderblue (Color 0.690196 0.878431 0.901961 1)) (defconst purple (Color 0.627451 0.12549 0.941176 1)) (defconst rebeccapurple (Color 0.4 0.2 0.6 1)) (defconst red (Color 1 0 0 1)) (defconst rosybrown (Color 0.737255 0.560784 0.560784 1)) (defconst royalblue (Color 0.254902 0.411765 0.882353 1)) (defconst saddlebrown (Color 0.545098 0.270588 0.0745098 1)) (defconst salmon (Color 0.980392 0.501961 0.447059 1)) (defconst sandybrown (Color 0.956863 0.643137 0.376471 1)) (defconst seagreen (Color 0.180392 0.545098 0.341176 1)) (defconst seashell (Color 1 0.960784 0.933333 1)) (defconst sienna (Color 0.627451 0.321569 0.176471 1)) (defconst silver (Color 0.752941 0.752941 0.752941 1)) (defconst skyblue (Color 0.529412 0.807843 0.921569 1)) (defconst slateblue (Color 0.415686 0.352941 0.803922 1)) (defconst slategray (Color 0.439216 0.501961 0.564706 1)) (defconst snow (Color 1 0.980392 0.980392 1)) (defconst springgreen (Color 0 1 0.498039 1)) (defconst steelblue (Color 0.27451 0.509804 0.705882 1)) (defconst tan (Color 0.823529 0.705882 0.54902 1)) (defconst teal (Color 0 0.501961 0.501961 1)) (defconst thistle (Color 0.847059 0.74902 0.847059 1)) (defconst tomato (Color 1 0.388235 0.278431 1)) (defconst transparent (Color 1 1 1 0)) (defconst turquoise (Color 0.25098 0.878431 0.815686 1)) (defconst violet (Color 0.933333 0.509804 0.933333 1)) (defconst webgray (Color 0.501961 0.501961 0.501961 1)) (defconst webgreen (Color 0 0.501961 0 1)) (defconst webmaroon (Color 0.501961 0 0 1)) (defconst webpurple (Color 0.501961 0 0.501961 1)) (defconst wheat (Color 0.960784 0.870588 0.701961 1)) (defconst white (Color 1 1 1 1)) (defconst whitesmoke (Color 0.960784 0.960784 0.960784 1)) (defconst yellow (Color 1 1 0 1)) (defconst yellowgreen (Color 0.603922 0.803922 0.196078 1)) (defn _init () (super TYPE_COLOR))) ;; Named types like _Engine whose name can be returned from get_class ;; but which do not exist in the runtime namespace exposed to ;; GDScript. (defclass NamedSyntheticType (GDLispSpecialType) private (defvar name) (defn _init (@name)) (defn satisfies? (value) (= (value:get-class) self:name))) ;; Note: All of these synthetic types would theoretically be defobject ;; if we weren't writing them in the standard library. But for ;; complicated reasons, we can't use macro expansion at all in stdlib, ;; and defobject is behind three layers of macro (defobject and ;; deflazy, and the expansion includes define-symbol-macro), so it's ;; off-limits. (defclass AnyType (GDLispSpecialType) private (defn satisfies? (_value) #t)) (defclass AnyRefType (GDLispSpecialType) private (defn satisfies? (value) (= ((literally typeof) value) TYPE_OBJECT))) (defclass AnyValType (GDLispSpecialType) private (defn satisfies? (value) (/= ((literally typeof) value) TYPE_OBJECT))) (defclass NumberType (GDLispSpecialType) private (defn satisfies? (value) (let ((t ((literally typeof) value))) (cond ((= t TYPE_INT) #t) ((= t TYPE_REAL) #t) (#t #f))))) (defclass BaseArrayType (GDLispSpecialType) private (defn satisfies? (value) (<= TYPE_ARRAY ((literally typeof) value) TYPE_COLOR_ARRAY))) (defclass NothingType (GDLispSpecialType) private (defn satisfies? (_value) #f)) (sys/declare value Any public) (sys/declare value AnyRef public) (sys/declare value AnyVal public) (sys/declare value Number public) (sys/declare value BaseArray public) (sys/declare value Nothing public) (sys/declare value Null public) (sys/declare value Bool public) (sys/declare value Int public) (sys/declare value Float public) (sys/declare value String public) (sys/declare value Vector2 public) (sys/declare value Rect2 public) (sys/declare value Vector3 public) (sys/declare value Transform2D public) (sys/declare value Plane public) (sys/declare value Quat public) (sys/declare value AABB public) (sys/declare value Basis public) (sys/declare value Transform public) (sys/declare value Color public) (sys/declare value NodePath public) (sys/declare value RID public) (sys/declare value Object public) (sys/declare value Dictionary public) (sys/declare value Array public) (sys/declare value PoolByteArray public) (sys/declare value PoolIntArray public) (sys/declare value PoolRealArray public) (sys/declare value PoolStringArray public) (sys/declare value PoolVector2Array public) (sys/declare value PoolVector3Array public) (sys/declare value PoolColorArray public) ;;; Polymorphic functions (defn len (x) (cond ((= x nil) 0) ((sys/instance-direct? x Cons) (let ((result 0)) (while (sys/instance-direct? x Cons) (set result (+ result 1)) (set x x:cdr)) result)) (#t ((literally len) x)))) ;;; List functions (defn list (&rest args) (sys/call-magic LIST) ;; The argument list may be shared with some other code, but `list` ;; should always allocate a new list, so defensively copy the ;; argument list. (list/copy args)) (defn list/copy (xs) (list/map (lambda (x) x) xs)) (defn cons (a b) (Cons:new a b)) (defn snoc (a b) (append a (list b))) (defn init (a) (cond ((sys/instance-direct? a:cdr Cons) (cons a:car (init a:cdr))) (#t nil))) (defn last (a) (cond ((sys/instance-direct? a:cdr Cons) (last a:cdr)) (#t a:car))) (defn list->array (list) (let ((arr [])) (while (sys/instance-direct? list Cons) (arr:push_back list:car) (set list list:cdr)) arr)) (defn array->list (arr) (let ((outer (cons () ()))) (let ((curr outer)) (for elem arr (set curr:cdr (cons elem ())) (set curr curr:cdr)) outer:cdr))) (defn list/elt (list n) (list/tail list n):car) (defn list/fold (f xs &opt x) (cond ((= x nil) (set x xs:car) (set xs xs:cdr))) (while (/= xs nil) (set x (funcall f x xs:car)) (set xs xs:cdr)) x) (defn list/map (f xs) (let ((outer (cons nil nil))) (let ((curr outer)) (while (/= xs nil) (set curr:cdr (cons (funcall f xs:car) nil)) (set curr curr:cdr) (set xs xs:cdr)) outer:cdr))) (defn list/filter (p xs) (let ((outer (cons nil nil))) (let ((curr outer)) (while (/= xs nil) (cond ((funcall p xs:car) (set curr:cdr (cons xs:car nil)) (set curr curr:cdr))) (set xs xs:cdr)) outer:cdr))) (defn list/find (f xs &opt default) (while (/= xs nil) (cond ((funcall f xs:car) (return xs:car))) (set xs xs:cdr)) default) (defn list/reverse (arg) (let ((rev nil)) (while (/= arg nil) (set rev `(,arg:car . ,rev)) (set arg arg:cdr)) rev)) (defn append (&rest args) (cond ((= args nil) nil) (#t (let ((outer (cons nil nil))) (let ((curr outer)) (while (/= args:cdr nil) (let ((inner-value args:car)) (while (/= inner-value nil) (set curr:cdr (cons inner-value:car nil)) (set curr curr:cdr) (set inner-value inner-value:cdr))) (set args args:cdr)) (set curr:cdr args:car) outer:cdr))))) (defn list/tail (list k) (for _i (range k) (set list list:cdr)) list) (defn sys/qq-smart-list (a) (cond ((instance? a GDLisp:BaseArray) (array->list a)) (#t a))) ;;; Array functions (defn elt (arr n) (sys/call-magic ARRAY-SUBSCRIPT) (elt arr n)) (defn set-elt (x arr n) (sys/call-magic ARRAY-SUBSCRIPT-ASSIGNMENT) (set-elt x arr n)) (defn member? (value arr) (sys/call-magic ARRAY-MEMBER-CHECK) (member? value arr)) (defn array/fold (f xs &opt x) (let ((starting-index 0)) (cond ((= x nil) (set x (elt xs 0)) (set starting-index 1))) (for i (range starting-index (len xs)) (set x (funcall f x (elt xs i)))) x)) (defn array/map (f xs) (let ((result [])) (for i (len xs) (result:push_back (funcall f (elt xs i)))) result)) (defn array/filter (p xs) (let ((result [])) (for i (len xs) (cond ((funcall p (elt xs i)) (result:push_back (elt xs i))))) result)) (defn array/reverse (arr) (let ((len (len arr)) (result (arr:duplicate))) (for i (range len) (set (elt result i) (elt arr (- len i 1)))) result)) (defn array/find (f arr &opt default) (for elem arr (cond ((funcall f elem) (return elem)))) default) ;;; Higher-order functions (defn funcall (f &rest args) (apply f args)) ;; funcall alias used in IIFEs. The user can shadow funcall (though ;; it's a bad idea), but shadowing names in sys/ is undefined behavior. (defn sys/funcall (f &rest args) (apply f args)) (defn apply (f &rest args) (let ((args1 (init args)) (args2 (last args))) (cond ((sys/instance-direct? f Function) (f:call_funcv (append args1 args2))) (#t (push-error "Attempt to call non-function"))))) ;;; Array functions (defn array/concat (&rest arrays) (cond ((= (len arrays) 0) []) (#t (apply #'+ arrays)))) ;;; Vector functions (defn vector/map (f arg &rest args) (flet ((-x (v) v:x) (-y (v) v:y) (-z (v) v:z)) (let ((args (cons arg args))) (cond ((instance? arg GDLisp:Vector2) (Vector2 (apply f (list/map #'-x args)) (apply f (list/map #'-y args)))) ((instance? arg GDLisp:Vector3) (Vector3 (apply f (list/map #'-x args)) (apply f (list/map #'-y args)) (apply f (list/map #'-z args)))) (#t (push-error "Attempted vector/map on non-vector")))))) ;;; Node functions (defclass SignalAdaptor (Reference) private (defvar function) (defn _init (@function)) (defn invoke (a b c d e f) sys/nullargs ;; Identify the first non-null argument. (let ((arglist (cond ((/= f ()) (list a b c d e f)) ((/= e ()) (list a b c d e)) ((/= d ()) (list a b c d)) ((/= c ()) (list a b c)) ((/= b ()) (list a b)) ((/= a ()) (list a)) (#t (list))))) (apply @function arglist)))) (defn get-signals-meta (obj) private (cond ((obj:has-meta "__gdlisp_signals") (obj:get-meta "__gdlisp_signals")) (#t (let ((new-meta {"__key" 0})) (obj:set-meta "__gdlisp_signals" new-meta) new-meta)))) (defn connect>> (obj signal-name function) (let ((function (SignalAdaptor:new function))) (obj:connect signal-name function "invoke") (let ((meta (get-signals-meta obj))) (let ((key (dict/elt meta "__key"))) (set (dict/elt meta key) function) (set (dict/elt meta "__key") (+ (dict/elt meta "__key") 1)) key)))) (defn connect1>> (obj signal-name function) (let ((key nil) (weakref (weakref obj))) (flet ((oneshot-function (&rest args) (apply function args) (let ((obj (weakref:get-ref))) (cond (obj (disconnect>> obj signal-name key)))))) (set key (connect>> obj signal-name #'oneshot-function))))) (defn disconnect>> (obj signal-name index) (let ((meta (get-signals-meta obj))) (meta:erase index))) ;;; Miscellaneous simple functions (defn bool (x) ;; Like the GDScript primitive but works for all types, not just ;; strings and numbers. (cond (x #t) (#t #f))) (defn vector (x y &opt z) (sys/call-magic VECTOR) (cond ((= z ()) V{x y}) (#t V{x y z}))) (defn array (&arr xs) (sys/call-magic ARRAY) xs) (defn dict (&arr xs) (sys/call-magic DICT) (let ((resulting-dict {})) (for i (range 0 (len xs) 2) (let ((k (elt xs i)) (v (elt xs (+ i 1)))) (set (elt resulting-dict k) v))) resulting-dict)) (defn NodePath (s) (sys/call-magic NODEPATH-SYNTAX) ((literally NodePath) s)) (defn not (x) (sys/call-magic BOOLEAN-NOT) (not x)) (defn intern (a) (cond ((member? a GDLisp:__gdlisp_Global_symbol_table) (elt GDLisp:__gdlisp_Global_symbol_table a)) (#t (set (elt GDLisp:__gdlisp_Global_symbol_table a) (Symbol:new a))))) (defn gensym (&opt prefix) (cond ((= prefix ()) (Symbol:new (GDLisp:__gdlisp_Global_name_generator:generate))) (#t (Symbol:new (GDLisp:__gdlisp_Global_name_generator:generate_with prefix))))) (defn sys/get-node (obj path) (sys/call-magic GET-NODE-SYNTAX) (obj:get-node path)) (defn instance? (value type) (cond ((sys/instance-direct? type GDLispSpecialType) (type:satisfies? value)) (#t (sys/instance-direct? value type)))) (defn sys/instance-direct? (value type) (sys/call-magic DIRECT-INSTANCE-CHECK) (sys/instance-direct? value type)) (sys/declare function typeof (value) public) (defn convert (what type) (cond ((sys/instance-direct? type PrimitiveType) ((literally convert) what type:primitive-value)) (#t ((literally convert) what type)))) (defn dict/elt (dict n) (sys/call-magic DICT-SUBSCRIPT) (dict/elt dict n)) (defn set-dict/elt (x dict n) (sys/call-magic DICT-SUBSCRIPT-ASSIGNMENT) (set-dict/elt x dict n)) (defn dict/find (f dict &opt default) (for key dict (cond ((funcall f key (dict/elt dict key)) (return key)))) default) ;;; Math operators (defn + (&rest args) (sys/call-magic ADDITION) (cond ((sys/instance-direct? args Cons) (let ((result args:car)) (set args args:cdr) (while (sys/instance-direct? args Cons) (set result (+ result args:car)) (set args args:cdr)) result)) (#t 0))) (defn * (&rest args) (sys/call-magic MULTIPLICATION) (let ((result 1)) (while (sys/instance-direct? args Cons) (set result (* result args:car)) (set args args:cdr)) result)) (defn - (x &rest args) (sys/call-magic SUBTRACTION) (cond ((sys/instance-direct? args Cons) (let ((result x)) (while (sys/instance-direct? args Cons) (set result (- result args:car)) (set args args:cdr)) result)) (#t (- x)))) (defn / (x &rest args) (sys/call-magic DIVISION) (cond ((sys/instance-direct? args Cons) (let ((result x)) (while (sys/instance-direct? args Cons) (set result (/ result args:car)) (set args args:cdr)) result)) (#t (/ x)))) (defn mod (x y) (sys/call-magic MODULO) (mod x y)) (defn min (&rest args) (sys/call-magic MIN-FUNCTION) (cond ((sys/instance-direct? args Cons) (let ((result args:car)) (set args args:cdr) (while (sys/instance-direct? args Cons) (set result (min result args:car)) (set args args:cdr)) result)) (#t (literally INF)))) (defn max (&rest args) (sys/call-magic MAX-FUNCTION) (cond ((sys/instance-direct? args Cons) (let ((result args:car)) (set args args:cdr) (while (sys/instance-direct? args Cons) (set result (max result args:car)) (set args args:cdr)) result)) (#t (- (literally INF))))) (defn gcd (&rest args) (list/fold #'binary-gcd args 0)) (defn lcm (&rest args) (list/fold #'binary-lcm args 1)) (defn binary-gcd (a b) private (while (/= b 0) (let ((tmp a)) (set a b) (set b (mod tmp b)))) a) (defn binary-lcm (a b) private (/ (* a b) (binary-gcd a b))) ;;; Comparison operators (defn = (x &rest args) (sys/call-magic EQUAL) (while (sys/instance-direct? args Cons) (cond ((= x args:car) ()) (#t (return #f))) (set x args:car) (set args args:cdr)) #t) (defn < (x &rest args) (sys/call-magic LESS-THAN) (while (sys/instance-direct? args Cons) (cond ((< x args:car) ()) (#t (return #f))) (set x args:car) (set args args:cdr)) #t) (defn > (x &rest args) (sys/call-magic GREATER-THAN) (while (sys/instance-direct? args Cons) (cond ((> x args:car) ()) (#t (return #f))) (set x args:car) (set args args:cdr)) #t) (defn <= (x &rest args) (sys/call-magic LESS-THAN-OR-EQUAL) (while (sys/instance-direct? args Cons) (cond ((<= x args:car) ()) (#t (return #f))) (set x args:car) (set args args:cdr)) #t) (defn >= (x &rest args) (sys/call-magic GREATER-THAN-OR-EQUAL) (while (sys/instance-direct? args Cons) (cond ((>= x args:car) ()) (#t (return #f))) (set x args:car) (set args args:cdr)) #t) (defn /= (x &rest args) (sys/call-magic NOT-EQUAL) (let ((outer (cons x args))) (while (sys/instance-direct? outer Cons) (let ((inner outer:cdr)) (while (sys/instance-direct? inner Cons) (cond ((/= outer:car inner:car) ()) (#t (return #f))) (set inner inner:cdr))) (set outer outer:cdr))) #t) (defn equal? (x &rest args) (while (sys/instance-direct? args Cons) (cond ((bin-equal x args:car) ()) (#t (return #f))) (set x args:car) (set args args:cdr)) #t) (defn bin-equal (a b) private (cond ((cond ((instance? a GDLisp:BaseArray) (instance? b GDLisp:BaseArray)) (#t #f)) (array-equal a b)) ((cond ((instance? a GDLisp:Dictionary) (instance? b GDLisp:Dictionary)) (#t #f)) (dict-equal a b)) ((cond ((instance? a Cons) (instance? b Cons)) (#t #f)) (cons-equal a b)) ((cond ((instance? a GDLisp:Number) (instance? b GDLisp:Number)) (#t #f)) (= a b)) ((= (GDLisp:typeof a) (GDLisp:typeof b)) (= a b)) (#t #f))) (defn array-equal (a b) private (cond ((/= (len a) (len b)) #f) (#t (let ((i 0) (upper (len a))) (while (< i upper) (cond ((not (bin-equal (elt a i) (elt b i))) (return #f))) (set i (+ i 1))) #t)))) (defn dict-equal (a b) private (cond ((/= (len a) (len b)) #f) (#t (for key (a:keys) (cond ((not (bin-equal (elt a key) (elt b key))) (return #f)))) #t))) (defn cons-equal (a b) private (cond ((bin-equal a:car b:car) (bin-equal a:cdr b:cdr)) (#t #f))) ;;; GDScript built-ins that we use unmodified ;; Note: These all repeat the name of the function. By default, ;; `sys/declare` will refuse to declare a name that conflicts with a ;; GDScript language keyword (adding an `_` to the name ;; automatically). But in our case, we want these names to represent ;; the GDScript keywords for real, so we override this using the ;; explicit `sys/declare` naming syntax and force it to use the name ;; anyway. (sys/declare superfunction (int int) (a) public) (sys/declare superfunction (randomize randomize) () public) (sys/declare superfunction (randi randi) () public) (sys/declare superfunction (randf randf) () public) (sys/declare superfunction (rand-range rand-range) (a b) public) (sys/declare superfunction (clamp clamp) (a b c) public) (sys/declare superfunction (abs abs) (a) public) (sys/declare superfunction (get-global-mouse-position get-global-mouse-position) () public) ; TODO Definitely want to wrap this (and all of the mouse functions) in a nice namespace or module or something (sys/declare superfunction (push-error push-error) (a) public) (sys/declare superfunction (push-warning push-warning) (a) public) (sys/declare superfunction (load load) (a) public) (sys/declare superfunction (acos acos) (a) public) (sys/declare superfunction (asin asin) (a) public) (sys/declare superfunction (atan atan) (a) public) (sys/declare superfunction (atan2 atan2) (a b) public) (sys/declare superfunction (cos cos) (a) public) (sys/declare superfunction (cosh cosh) (a) public) (sys/declare superfunction (sin sin) (a) public) (sys/declare superfunction (sinh sinh) (a) public) (sys/declare superfunction (tan tan) (a) public) (sys/declare superfunction (tanh tanh) (a) public) (sys/declare superfunction (ceil ceil) (a) public) (sys/declare superfunction (char char) (a) public) (sys/declare superfunction (exp exp) (a) public) (sys/declare superfunction (floor floor) (a) public) (sys/declare superfunction (sqrt sqrt) (a) public) (sys/declare superfunction (fmod fmod) (a b) public) (sys/declare superfunction (fposmod fposmod) (a b) public) (sys/declare superfunction (posmod posmod) (a b) public) (sys/declare superfunction (sign sign) (a) public) (sys/declare superfunction (ord ord) (a) public) (sys/declare superfunction (hash hash) (a) public) (sys/declare superfunction (get-stack get-stack) () public) (sys/declare superfunction (is-nan is-nan) (a) public) (sys/declare superfunction (is-inf is-inf) (a) public) (sys/declare superfunction (is-equal-approx is-equal-approx) (a b) public) (sys/declare superfunction (is-zero-approx is-zero-approx) (a) public) (sys/declare superfunction (inverse-lerp inverse-lerp) (a b c) public) (sys/declare superfunction (lerp lerp) (a b c) public) (sys/declare superfunction (lerp-angle lerp-angle) (a b c) public) (sys/declare superfunction (pow pow) (a b) public) (sys/declare superfunction (stepify stepify) (a b) public) (sys/declare superfunction (step-decimals step-decimals) (a) public) (sys/declare superfunction (seed seed) (a) public) (sys/declare superfunction (rand-seed rand-seed) (a) public) (sys/declare superfunction (deg2rad deg2rad) (a) public) (sys/declare superfunction (rad2deg rad2deg) (a) public) (sys/declare superfunction (db2linear db2linear) (a) public) (sys/declare superfunction (linear2db linear2db) (a) public) (sys/declare superfunction (is-instance-valid is-instance-valid) (a) public) (sys/declare superfunction (log log) (a) public) (sys/declare superfunction (wrapf wrapf) (a b c) public) (sys/declare superfunction (wrapi wrapi) (a b c) public) (sys/declare superfunction (print-stack print-stack) () public) (sys/declare superfunction (round round) (a) public) (sys/declare superfunction (cartesian2polar cartesian2polar) (a b) public) (sys/declare superfunction (polar2cartesian polar2cartesian) (a b) public) (sys/declare superfunction (range-lerp range-lerp) (a b c d e) public) (sys/declare superfunction (move-toward move-toward) (a b c) public) (sys/declare superfunction (nearest-po2 nearest-po2) (a) public) (sys/declare superfunction (instance-from-id instance-from-id) (a) public) (sys/declare superfunction (parse-json parse-json) (a) public) (sys/declare superfunction (to-json to-json) (a) public) (sys/declare superfunction (validate-json validate-json) (a) public) (sys/declare superfunction (dict2inst dict2inst) (a) public) (sys/declare superfunction (inst2dict inst2dict) (a) public) (sys/declare superfunction (str2var str2var) (a) public) (sys/declare superfunction (var2str var2str) (a) public) (sys/declare superfunction (weakref weakref) (a) public) (sys/declare superfunction (ease ease) (a b) public) (sys/declare superfunction (funcref funcref) (a b) public) (sys/declare superfunction (type-exists type-exists) (a) public) (sys/declare superfunction (smoothstep smoothstep) (a b c) public) ;; Note that, in the spirit of internal consistency with the type ;; names, we do name some of these functions differently. For ;; instance, the GDScript function `bool` is mapped to the GDLisp ;; function `Bool`, for consistency with the `Bool` type. (sys/declare superfunction (Bool bool) (a) public) (sys/declare superfunction (Int int) (a) public) (sys/declare superfunction (Float float) (a) public) (sys/declare superfunction (String String) (a) public) (sys/declare superfunction (AABB AABB) (a b) public) (sys/declare superfunction (RID RID) (a) public) (sys/declare superfunction (Dictionary Dictionary) (a) public) (sys/declare superfunction (Array Array) (a) public) (sys/declare superfunction (PoolColorArray PoolColorArray) (a) public) (sys/declare superfunction (PoolByteArray PoolByteArray) (a) public) (sys/declare superfunction (PoolIntArray PoolIntArray) (a) public) (sys/declare superfunction (PoolRealArray PoolRealArray) (a) public) (sys/declare superfunction (PoolVector2Array PoolVector2Array) (a) public) (sys/declare superfunction (PoolVector3Array PoolVector3Array) (a) public) (sys/declare superfunction (Vector2 Vector2) (a b) public) (sys/declare superfunction (Vector3 Vector3) (a b c) public) (sys/min-godot-version 3050000 (sys/declare superfunction (deep-equal deep-equal) (a b) public)) ;;; Varargs functions ;; (See https://github.com/Mercerenies/gdlisp/issues/79 for details on ;; why we have to wrap these ourselves) (defn str (x &arr args) (sys/call-magic VARARG-STR) (let ((result (str x))) (for arg args (set result (+ result (str arg)))) result)) (defn printerr (&arr args) (sys/call-magic VARARG-PRINTERR) (let ((result "")) (for arg args (set result (+ result (str arg)))) (printerr result))) (defn printraw (&arr args) (sys/call-magic VARARG-PRINTRAW) (let ((result "")) (for arg args (set result (+ result (str arg)))) (printraw result))) (defn print-debug (&arr args) (sys/call-magic VARARG-PRINTDEBUG) (let ((result "")) (for arg args (set result (+ result (str arg)))) (print-debug result))) (defn print (&arr args) (sys/call-magic VARARG-PRINT) (let ((result "")) (for arg args (set result (+ result (str arg)))) (print result))) (defn prints (&arr args) (sys/call-magic VARARG-PRINTS) (let ((result "") (first #t)) (for arg args (set result (+ result (cond (first "") (#t " ")) (str arg))) (set first #f)) (print result))) (defn printt (&arr args) (sys/call-magic VARARG-PRINTT) (let ((result "") (first #t)) (for arg args (set result (+ result (cond (first "") (#t "\t")) (str arg))) (set first #f)) (print result))) (defn range (a &opt b c) (sys/call-magic VARARG-RANGE) (cond ((= b nil) (range a)) ((= c nil) (range a b)) (#t (range a b c)))) (defn Color8 (a b c &opt d) (sys/call-magic VARARG-COLOR8) (cond ((= d nil) (Color8 a b c)) (#t (Color8 a b c d)))) (defn ColorN (a &opt b) (sys/call-magic VARARG-COLORN) (cond ((= b nil) (ColorN a)) (#t (ColorN a b)))) (defn Rect2 (a b &opt c d) ; TODO Not a perfect translation of the pair of overloads provided (sys/call-magic VARARG-RECT2) (cond ((= c nil) (Rect2 a b)) (#t (Rect2 a b c d)))) (defn Transform2D (a &opt b c) (sys/call-magic VARARG-TRANSFORM2D) (cond ((= b nil) (Transform2D a)) ((= c nil) (Transform2D a b)) (#t (Transform2D a b c)))) (defn Plane (a b &opt c d) (sys/call-magic VARARG-PLANE) (cond ((= c nil) (Plane a b)) ((= d nil) (Plane a b c)) (#t (Plane a b c d)))) (defn Quat (a &opt b c d) ; TODO Not a perfect translation of the overloads provided (sys/call-magic VARARG-QUAT) (cond ((= b nil) (Quat a)) ((= c nil) (Quat a b)) (#t (Quat a b c d)))) (defn Basis (a &opt b c) (sys/call-magic VARARG-BASIS) (cond ((= b nil) (Basis a)) ((= c nil) (Basis a b)) (#t (Basis a b c)))) (defn Transform (a &opt b c d) ; TODO Not a perfect translation of the overloads provided (sys/call-magic VARARG-TRANSFORM) (cond ((= b nil) (Transform a)) ((= c nil) (Transform a b)) (#t (Transform a b c d)))) (defn Color (a &opt b c d) ; TODO Not a perfect translation of the overloads provided (sys/call-magic VARARG-COLOR) (cond ((= b nil) (Color a)) ((= d nil) (Color a b c)) (#t (Color a b c d)))) (defn bytes2var (a &opt b) (sys/call-magic VARARG-BYTES2VAR) (cond ((= b nil) (bytes2var a)) (#t (bytes2var a b)))) (defn var2bytes (a &opt b) (sys/call-magic VARARG-VAR2BYTES) (cond ((= b nil) (var2bytes a)) (#t (var2bytes a b)))) ;; TYPE_* Constants ;;; Built-In Macros (defmacro or (&rest args) (let ((args (list/reverse args))) (cond (args (let ((result `((#t ,args:car)))) (set args args:cdr) (while (/= args nil) (set result `((,args:car) . ,result)) (set args args:cdr)) `(cond . ,result))) (#t #f)))) (defmacro and (&rest args) (let ((args (list/reverse args))) (cond (args (let ((result `((#t ,args:car)))) (set args args:cdr) (while (/= args nil) (set result `(((not ,args:car) #f) . ,result)) (set args args:cdr)) `(cond . ,result))) (#t #t)))) (defmacro let* (vars &rest body) (cond ((= vars nil) `(progn ,.body)) (#t `(let (,vars:car) (let* ,vars:cdr ,.body))))) (defmacro defvars (&rest args) (let ((arr [])) (while (/= args nil) (arr:push_back (list 'defvar args:car)) (set args args:cdr)) `(progn ,.arr))) (defmacro when (cnd &rest args) `(cond (,cnd (progn ,.args)))) (defmacro unless (cnd &rest args) `(cond (,cnd ()) (#t (progn ,.args)))) (defmacro if (cnd t &opt f) `(cond (,cnd ,t) (#t ,f))) (defmacro update (field updater) (cond ((not (instance? updater Cons)) (set updater (list updater)))) (set updater:cdr (cons field updater:cdr)) `(set ,field ,updater)) (defmacro yield* (arg) (let ((symbol (gensym "_yield"))) `(let ((,symbol ,arg)) (while (and (instance? ,symbol GDScriptFunctionState) ((unquote symbol):is-valid)) (yield) (set ,symbol ((unquote symbol):resume))) ,symbol))) (defmacro quit () ;; Just a small helper macro to make it easier to exit the game. ;; Mainly useful in the REPL. '((GDLisp:get-tree):quit)) (defmacro this-file () '(sys/special-ref this-file)) (defmacro this-filename () '(sys/special-ref this-filename)) (defmacro this-true-filename () '(sys/special-ref this-true-filename)) ;; TODO Document the semantics of this macro and what preconditions ;; are necessary for it to be safe to use. (defmacro contextual-load (arg) `(load (sys/context-filename ,arg))) (defmacro deflazy (name value &rest modifiers) (let ((fn-name (gensym "_lazy")) (this-file (gensym "_this_file")) (value-var (gensym "_value")) (meta-name ("__gdlisp_Lazy_{}":format [(gensym):__gdlisp_contents] "{}"))) ; TODO Find a better way to convert symbol to string than accessing a, theoretically, private field `(progn (defn ,fn-name () (let ((,this-file (this-file))) (if ((unquote this-file):has-meta ,meta-name) ((unquote this-file):get-meta ,meta-name) (let ((,value-var ,value)) ((unquote this-file):set-meta ,meta-name ,value-var) ,value-var)))) (define-symbol-macro ,name (list (list 'access-slot (list 'contextual-load (this-true-filename)) ',fn-name)) ,.modifiers)))) (defmacro defobject (name parent &opt visibility &rest body) (cond ((= visibility nil) (set visibility 'public)) ((not (instance? visibility Symbol)) (set body (cons visibility body)) ; It's not a modifier, so it's part of the body (set visibility 'public)) ((= visibility 'public) nil) ((= visibility 'private) nil) (#t (set body (cons visibility body)) ; It's not a modifier, so it's part of the body (set visibility 'public))) `(deflazy ,name (new ,parent ,.body) ,visibility)) (defmacro list/for (var list &rest body) (let ((iter (gensym))) `(let ((,iter ,list) (,var ())) (while (/= ,iter ()) (set ,var (access-slot ,iter car)) (set ,iter (access-slot ,iter cdr)) ,.body)))) (defmacro -> (arg &rest forms) (cond ((= forms nil) arg) (#t (let ((first (cond ((instance? forms:car Cons) forms:car) (#t (list forms:car)))) (rest forms:cdr)) (let ((new-arg (cons first:car (cons arg first:cdr)))) `(-> ,new-arg ,.rest)))))) (defmacro ->> (arg &rest forms) (cond ((= forms nil) arg) (#t (let ((first (cond ((instance? forms:car Cons) forms:car) (#t (list forms:car)))) (rest forms:cdr)) (let ((new-arg (snoc first arg))) `(->> ,new-arg ,.rest)))))) (defmacro as-> (arg var &rest forms) (cond ((= forms nil) arg) (#t (let ((first forms:car) (rest forms:cdr)) `(as-> (let ((,var ,arg)) ,first) ,var ,.rest))))) ================================================ FILE: MacroServer/TestLoadedFile.gd ================================================ # Copyright 2023 Silvio Mayolo # # This file is part of GDLisp. # # GDLisp is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # GDLisp is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GDLisp. If not, see . # This file is used exclusively for testing purposes and is not used # in compilation of actual GDScript code. static func example(): return "Test succeeded" ================================================ FILE: MacroServer/main.gd ================================================ # Copyright 2023 Silvio Mayolo # # This file is part of GDLisp. # # GDLisp is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # GDLisp is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GDLisp. If not, see . extends Node var peer = null var loaded_files = null func _ready(): loaded_files = [] var port_number = int(OS.get_environment("GDLISP_PORT_NUMBER")) peer = StreamPeerTCP.new() peer.big_endian = true peer.connect_to_host("127.0.0.1", port_number) func _process(_delta): if peer.get_available_bytes() > 0: var json_result = JSON.parse(peer.get_string()) if json_result.error == OK: var payload = json_result.result run_command(payload) else: push_error("Invalid JSON " + json_result.error_string) peer.put_string(failed_response(ERR_INVALID_DATA, "Invalid JSON " + json_result.error_string)) func failed_response(error_code, error_string = ""): var response = { "error_code": error_code, "error_string": error_string, "response_string": "" } return JSON.print(response) func successful_response(response_string): var response = { "error_code": OK, "error_string": "", "response_string": response_string } return JSON.print(response) func run_command(payload): var cmd = payload['command'] var args = payload['args'] match cmd: "quit": peer.put_string(successful_response("Acknowledged\nQuitting...")) get_tree().quit() "ping": peer.put_string(successful_response("pong")) "eval": var input = args[0] var result = eval(input) peer.put_string(successful_response(pretty(result))) "exec": var input = args[0] var result = exec(input) peer.put_string(successful_response(pretty(result))) "load": var input = args[0] var idx = len(loaded_files) loaded_files.push_back(load(input)) peer.put_string(successful_response(pretty(idx))) func eval(input): return exec(" return " + input) # Funny hack, thanks Godot Q&A! :) # # https://godotengine.org/qa/339/does-gdscript-have-method-to-execute-string-code-exec-python?show=362#a362 func exec(input): var script = GDScript.new() script.set_source_code("func exec(MAIN):\n" + input) script.reload() var obj = Reference.new() obj.set_script(script) return obj.exec(self) # I'll probably end up migrating this to GDLisp.gd proper at some # point, but for now, here it is. func pretty(value): if value == null: return "()" elif value is bool and value: return "#t" elif value is bool and not value: return "#f" elif value is int or value is float: return str(value) elif value is Vector2: return "V{{} {}}".format([pretty(value.x), pretty(value.y)], "{}") elif value is Vector3: return "V{{} {} {}}".format([pretty(value.x), pretty(value.y), pretty(value.z)], "{}") elif value is Array: var s = "[" var first = true for x in value: if not first: s += " " s += pretty(x) first = false return s + "]" elif value is Dictionary: var s = "{" var first = true for k in value: if not first: s += " " s += pretty(k) + " " + pretty(value[k]) first = false return s + "}" elif value is String: var s = "\"" for x in value: match x: "\n": s += "\\n" "\t": s += "\\t" "\r": s += "\\r" "\a": s += "\\a" "\b": s += "\\b" "\f": s += "\\f" "\v": s += "\\v" "\"": s += "\\\"" "\\": s += "\\\\" _: s += x return s + "\"" elif value.has_meta("__gdlisp_Primitive_Cons"): return "(" + _pretty_list(value) + ")" elif value.has_meta("__gdlisp_Primitive_Symbol"): return value.__gdlisp_contents else: return str(value) func _pretty_list(value): # The same logic as `crate::sxp::ast::fmt_list` to make list # output pretty. if value.cdr == null: return pretty(value.car) elif typeof(value.cdr) == TYPE_OBJECT and value.cdr.has_meta("__gdlisp_Primitive_Cons"): return pretty(value.car) + " " + _pretty_list(value.cdr) else: return pretty(value.car) + " . " + pretty(value.cdr) ================================================ FILE: MacroServer/main.tscn ================================================ [gd_scene load_steps=2 format=2] [ext_resource path="res://main.gd" type="Script" id=1] [node name="main" type="Node"] script = ExtResource( 1 ) ================================================ FILE: MacroServer/project.godot ================================================ config_version=4 [application] run/main_scene="res://main.tscn" [autoload] GDLisp="*res://GDLisp.gd" ================================================ FILE: README.md ================================================ [![GitHub Actions: Ubuntu](https://github.com/mercerenies/gdlisp/actions/workflows/test-Godotv3.5-Ubuntu.yml/badge.svg)](https://github.com/Mercerenies/gdlisp/actions/workflows/test-Godotv3.5-Ubuntu.yml) [![GitHub Actions: Ubuntu](https://github.com/mercerenies/gdlisp/actions/workflows/test-Godotv3.5-Mono-Ubuntu.yml/badge.svg)](https://github.com/Mercerenies/gdlisp/actions/workflows/test-Godotv3.5-Mono-Ubuntu.yml) [![GitHub Actions: Ubuntu](https://github.com/mercerenies/gdlisp/actions/workflows/test-Godotv3.4-Ubuntu.yml/badge.svg)](https://github.com/Mercerenies/gdlisp/actions/workflows/test-Godotv3.4-Ubuntu.yml) [![GitHub Actions: Ubuntu](https://github.com/mercerenies/gdlisp/actions/workflows/test-Godotv3.3.3-Ubuntu.yml/badge.svg)](https://github.com/Mercerenies/gdlisp/actions/workflows/test-Godotv3.3.3-Ubuntu.yml) # GDLisp Lisp for the [Godot](https://godotengine.org/) platform! This language aims to be a Lisp dialect which compiles to GDScript. This project is built using [Cargo](https://doc.rust-lang.org/cargo/) with [Rake](https://ruby.github.io/rake/) as a wrapper for custom build scripts. Use `rake test` to run the test suite, and use `rake run` to compile stdin input to GDScript. The current version of GDLisp is tested against Godot 3.5 and expects a command called `godot` to be on your system's path which points to the Godot executable. Check out the [official documentation](https://gdlisp.readthedocs.io/en/stable/) for a "Getting Started" guide! GDLisp is not affiliated with or endorsed by the Godot project in any official capacity. This is an independent project. ## Features * Support for standard GDScript functionality, including function declarations, class declarations, signals, etc. * The expression-based semantics we all love from Lisp * Lambdas / anonymous functions as well as anonymous classes * Support for compile-time macros that run in the GDLisp compiler itself ## FAQs ### Can I use GDLisp today? Absolutely! GDLisp is production-ready, and I encourage everyone to try it out and provide any feedback on the issue tracker. ### What Godot versions is GDLisp compatible with? GDLisp works with Godot 3.x. The next major release of GDLisp will be fully compatible with Godot 4, though there's no definite timeline on that as yet. ### Can I use GDLisp and GDScript in the same project? You certainly can! GDLisp constructs compile in a straightforward way to existing Godot concepts. Classes and functions written in GDLisp can be used from GDScript, and vice versa. ## License GDLisp is distributed under the terms of the GNU General Public License version 3 or later. For more details, see `COPYING`. As a special exception to the GNU General Public License, the GDLisp support file `GDLisp.lisp` can be used and redistributed without any restrictions. ================================================ FILE: Rakefile ================================================ require 'logger' require 'fileutils' if Gem.win_platform? # Needed to be able to create symlinks on Windows. require 'win32/file' end $logger = Logger.new($stdout) release_flag = if ENV['GDLISP_RELEASE'] ['--release'] else [] end task default: %w[run] task :clippy do |t, args| sh 'cargo', 'clippy', *args end task :doc do |t, args| ENV['RUSTDOCFLAGS'] ||= '' ENV['RUSTDOCFLAGS'] += ' -D warnings' sh 'cargo', 'doc', *args end task :build_rs do |t, args| sh 'cargo', 'build', *release_flag end task build: :build_rs do |t, args| sh 'cargo', 'run', *release_flag, '--', '--compile-stdlib' cp 'GDLisp.gd', 'MacroServer/GDLisp.gd' cp_r 'MacroServer', 'target/debug' cp 'GDLisp.msgpack', 'target/debug' cp_r 'MacroServer', 'target/debug/deps' cp 'GDLisp.msgpack', 'target/debug/deps' cp_r 'MacroServer', 'target/release' cp 'GDLisp.msgpack', 'target/release' cp_r 'MacroServer', 'target/release/deps' cp 'GDLisp.msgpack', 'target/release/deps' if release_flag.include? '--release' mkdir_p 'bin/' File.delete('bin/gdlisp') if File.exist?('bin/gdlisp') File.symlink('../target/release/gdlisp', 'bin/gdlisp') end end task run: :build do |t, args| sh 'cargo', 'run', *release_flag, *args end task test: :build do |t, args| sh 'cargo', 'test', *release_flag, *args end task :clean do |t, args| sh 'cargo', 'clean', *args end ================================================ FILE: build.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate lalrpop; fn main() { lalrpop::process_root().unwrap(); } ================================================ FILE: doc/README.md ================================================ # GDLisp Official Documentation This is the official documentation for the GDLisp language. * [`internal/`](internal/) - Information useful for contributors to this repository. Developers *using* GDLisp should not need the information here. ================================================ FILE: doc/internal/CompilationStages.md ================================================ # Compilation Stages ```mermaid flowchart LR subgraph AST direction TB Parse[Parse .lisp file into AST] end subgraph IR direction TB LoadIR[Load AST into IR data structures] Scopes[Check for Scope Conflicts] Loops["Validate Looping Constructs (Break/Continue)"] LoadIR-->Scopes-->Loops end subgraph GDScript direction TB Bind[Bind all top-level declarations in a global symbol table] Const[Check that any expressions in constant position are actually constant] Compile[Compile IR into GDScript syntax representation] Optimize[Run GDScript-Level Optimizations] Output[Output GDScript to .gd file] Bind-->Const-->Compile-->Optimize-->Output end AST-->IR-->GDScript ``` This document details the stages of the GDLisp Compiler. GDLisp source code begins in a `.lisp` source file. The parser loads that into an abstract syntax tree (AST). At this point, the code is *only* being viewed as a sequence of S-expressions that has no further semantic meaning. Next, the AST is loaded into a specially-designed intermediate representation (IR). During loading into IR, any macros that are encountered are evaluated and interpolated. Crucially, after the code is loaded into its IR representation, no more macro expansion will ever need to happen. The IR understands basic notions, such as the difference between an expression and a declaration, as well as control flow constructs like `cond` statements and assignment statements. It does *not* understand more detailed scoping rules regarding local variables. Once the IR is loaded, some validation is performed on the code in its IR form. * All scopes are checked for name conflicts. While name shadowing is allowed (i.e. declaring a local function with the same name as a global one, or a local function with the same name as a local function in a strictly outer scope), declaring the same variable or function twice in the same scope is forbidden. Additionally, the top-level scope can declare at most one `main` class. * Any loop control constructs are checked. That is, all uses of `break` and `continue` are audited to ensure that they make sense. `break` and `continue` can only be used inside of loops, and they cannot be used if there is a closure (such as a lambda) between the control construct and the loop declaration. Next, the IR is analyzed, and a global symbol table is created. Any top-level declarations have their names bound in the global symbol table. This ensures that, during compilation, functions can access functions declared anywhere in the file. With the symbol table, any expressions in constant position are checked to make sure they are actually constant. This includes `const` declarations, the right-hand-side of `enum` values, and the inside of `export` clauses. Then the IR is compiled into an internal representation of GDScript. This is basically GDScript source code, but represented as an abstract syntax tree manipulable within the GDLisp engine. This compilation takes into consideration the global symbol table and checks that any referenced names (local or global) make sense in that context. Once the GDScript source is built, some optimizations are performed on it to eliminate dead code, simplify convoluted constructs, and generally make the code better and faster. This step is specifically disabled during unit and integration testing, except for those specifically designed to test optimizations. Finally, the resulting GDScript source is stringified and written out to a `.gd` file. ================================================ FILE: doc/internal/README.md ================================================ # Internal GDLisp Documentation * [Compilation Stages](CompilationStages.md) ================================================ FILE: doc/readthedocs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: doc/readthedocs/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'GDLisp' copyright = '2022-2023, Mercerenies' author = 'Mercerenies' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = ['sphinxcontrib.mermaid'] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] highlight_language = 'scheme' # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output #html_theme = 'alabaster' html_theme = "sphinx_rtd_theme" html_static_path = ['_static'] ================================================ FILE: doc/readthedocs/index.rst ================================================ .. GDLisp documentation master file, created by sphinx-quickstart on Thu Dec 1 12:29:12 2022. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to GDLisp's documentation! ================================== .. toctree:: :maxdepth: 2 :caption: Contents: tutorial/index.rst reference/index.rst ================================================ FILE: doc/readthedocs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ================================================ FILE: doc/readthedocs/reference/classes.rst ================================================ .. _classes: Classes and Objected-Oriented Programming ========================================= GDLisp, like GDScript, is an object-oriented programming language. GDLisp implements a similar object inheritance mechanism to GDScript, allowing you to define classes which inherit from exactly one superclass and to create instances of those classes. Classes are declared at the top-level of a module as follows. Classes may *not* be nested within one another. :: (defclass ClassName (ParentClassName) body ...) (defclass ClassName (ParentClassName) public-or-private body ...) The class name is a symbol literal. By convention, class names in Godot are written in ``CamelCase``, though this is not a requirement. The parent class is a symbol literal whose value is a class known at compile-time. The parent class name may optionally be followed by a :ref:`visibility modifier `. Class names are placed in the value namespace, similar to constants and enums. The parent class name may be omitted by simply replacing it with the empty list ``()``. In this case, the parent class is assumed to be the built-in type ``Reference``. For example, the following class implicitly has parent class ``Reference``. :: (defclass MyNewReferenceType () body ...) After the class name, the class body consists of zero or more class declarations. These are somewhat similar to normal module-level declarations, but there are some forms that only make sense inside of classes, and there are some subtle differences between the two, so it's best to treat the two scopes as distinct. Macro expansion takes place inside of class declaration scopes, just like it does at the top level of a module. Inside of classes, there are four valid types of declarations: ``defsignal``, ``defconst``, ``defvar``, and ``defn``. Additionally, :ref:`progn ` forms can appear in class bodies and behave identically to the same forms at the top-level. Class Constants --------------- ``defconst`` inside of classes works identically to the ``defconst`` top-level form. It defines a constant inside the class scope, whose value is known at compile-time. See :ref:`constants` for more details on how this declaration type works. Class constants are considered "static". That is, it is not necessary to construct an instance in order to access a class constant. Class constants can be accessed on the class directly *or* on an instance, and the syntax to do so uses ``access-slot`` (or, more conveniently, the ``:`` colon syntax sugar). Given the following class, both ``Foo:A`` and ``(Foo:new):A`` will evaluate to ``42``. :: (defclass Foo () (defconst A 42)) Class Signals ------------- :: (defsignal signal-name) (defsignal signal-name (args ...)) Signals are the only values defined in the signal namespace. A signal is defined by ``defsignal``, followed by the name of the signal, and then followed by a :ref:`simple lambda list `. If the argument list is omitted, then it defaults to ``()``. Instance Variables ------------------ :: (defvar var-name) (defvar var-name initial-value) Instance variables are defined using ``defvar``. An instance variable is a name that exists on *instances* of the class, not on the class itself. GDLisp has no concept of "static" instance variables. Instance variables can be accessed on instances of a class via the ``access-slot`` syntax (more conveniently written using the infix ``:`` operator). Given an instance ``my-player`` of type ``Player``, the ``health`` field on this instance can be accessed via ``my-player:health`` (equivalently, ``(access-slot my-player health)``). An instance variable may optionally be followed by its initial value. If provided, the initial value will be set at the very beginning of the class' constructor, immediately after calling the superclass constructor. If not provided, the initial value of the variable shall be the null ``()`` object. .. Note:: GDLisp, like GDScript, has no notion of static instance variables. All instance variables are scoped to a particular instance. Initialization Time ^^^^^^^^^^^^^^^^^^^ By default, instance variables for which an initial value is given are initialized at object construction time, after the parent ``_init`` is called but before the current class' ``_init`` is executed. It is often useful to initialize instance variables when a node is first added to the scene tree. To this end, a ``defvar`` for which an initial value is provided can optionally be succeeded by the ``onready`` symbol. :: (defvar var-name initial-value onready) A variable indicated in this way will have its value set immediately before the ``_ready`` method of the class is invoked, when the node is added to the scene tree. Class and Instance Functions ---------------------------- :: (defn function-name (args ...) body ...) Class-level functions are declared similarly to module-level functions, using the ``defn`` keyword, followed by the function name, then a list of formal arguments, and finally the function body. Class-level functions take formal arguments as a :ref:`simple lambda list `, which means functions inside of a class do *not* support optional or variable arguments. A function defined inside of a class is called on instances of the class, using an ``access-slot`` form as the head of an S-expression in an expression context. That is, given an object ``foo``, the expression ``(foo:bar 1 2 3)`` (or, written out in full, ``((access-slot foo bar) 1 2 3)``) will invoke the instance function called ``bar`` on the object ``foo``, calling it with three arguments: ``1``, ``2``, and ``3``. Inside the body of an instance function, the argument names are bound within a local scope, similar to a module function. Additionally, the special variable name ``self`` is bound to the instance on which the function was invoked. The body expressions of the function are evaluated in order, and the final expression is returned. If the function has no body, then the null ``()`` object is returned. Like with module functions, instance functions can be exited early with the ``return`` special form. Static Functions ^^^^^^^^^^^^^^^^ :: (defn function-name (args ...) static body ...) A function may be marked as static by placing the keyword ``static`` keyword after the function's formal argument list. A static function can be invoked on *either* an instance or the class itself using the ``:`` (equivalently, ``access-slot``) forms to call the function. In either case, a static function behaves like an instance function except that ``self`` is never bound inside the function. .. _constructor-functions: Constructor Functions ^^^^^^^^^^^^^^^^^^^^^ :: (defn _init (args ...) body ...) The function called ``_init`` is special. This is the function which will be invoked when a new instance of the class should be constructed via ``new``. Any arguments passed to the class' ``new`` function will be forwarded onto ``_init``. ``_init`` takes a *constructor lambda list*, which permits a special form of syntax unique to constructors, rather than a simple lambda list. See :ref:`constructor-lambda-lists` for details. ``_init`` can never be static. Finally, ``_init`` never returns a value. The ``return`` special form may still be used to exit the constructor early, but its argument will be ignored. The first expression in the body of a constructor function can be of the form ``(super args ...)``, i.e. a proper list whose first element is the literal name ``super``. This will cause the constructor function to invoke the parent class' constructor with the arguments given. This must be the first expression in a constructor function. If an explicit ``super`` call is not supplied, then the parent class' constructor will be called implicitly with no arguments. .. _getter-and-setter: Getter and Setter Functions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: (defn (get field-name) () body ...) (defn (set field-name) (arg) body ...) Rather than a literal symbol, the function name of a ``defn`` can be one of the special forms ``(get field-name)`` or ``(set field-name)`` where ``field-name`` is an arbitrary literal symbol. These define, respectively, a getter and a setter function. Getters and setters can never be static. A getter function, defined with the name ``(get field-name)``, must take zero arguments. It will be invoked when the corresponding field (in the value namespace) is accessed on an instance of the class. That is, a getter method ``(get health)`` will be invoked on a class ``Player`` if we have an instance of the class ``my-player`` and attempt to access the field ``my-player:health``. A setter function, defined with the name ``(set field-name)``, must take exactly one argument. It will be invoked when the corresponding field is *assigned to* with the ``set`` special form. That is, a function ``(set health)`` will be invoked on a class ``Player`` if we have an instance of the class ``my-player`` and write ``(set my-player:health some-value)``. The sole argument to a setter function is the right-hand side of the ``set`` special form. A setter function never returns a value. Setters can be exited early with the ``return`` special form, but the value returned will be ignored. Setters and getters for the same field may be defined on the same class. It is an error to define an instance variable (via ``defvar``) and a setter or a getter for the same field name on the same class. Setters and getters are compatible with GDScript, in the sense that attempts to access or set the field from GDScript will also trigger the getter or setter, respectively. Superclass Calls ^^^^^^^^^^^^^^^^ Within a non-static instance function, a special form of syntax is available. :: (super:method-name args ...) Attempting to call a method on the literal symbol ``super`` will invoke the method of the given name on the current instance, but considering only functions defined in the parent class or above. This syntax only makes sense inside of instance functions in a class. The behavior is undefined if this ``super`` call syntax is used in a setter, getter, or constructor. ``super`` is *not* a value in the value namespace, and it is not permitted to assign ``super`` (on its own) to a variable or use it in some way other than the syntax shown. Main Classes ------------ In GDScript, a single source file maps to a defined class. Functions on a source file are, unless marked static, functions on *instances* of that class. GDLisp works differently. A GDLisp source file is a module, and it may *contain* one or more classes, but it is not itself a class. These classes contained in a GDLisp source file will compile to *inner classes* in the resulting GDScript source file. However, there are good reasons to have control over this "top-level" class in Godot. Packed scenes will always refer to a file's top-level class, not to inner classes. So GDLisp provides a mechanism to define a particular class that should be treated as the "main" class. :: (defclass ClassName (ParentClassName) main body ...) After the class' parent name and before the class' body, the symbol ``main`` can be written to indicate that this class is the module's "main" class. If your class has a visibility modifier, then the ``main`` modifier can be written before or after the visibility modifier (though a private ``main`` class makes very little sense). Designating a class as the "main" class does not change how you refer to this class in GDLisp. It is still a class name defined on the module in the value namespace, just like any other class, and it will still be instantiated, imported, and used in the exact same way. The ``main`` designator does affect how the class is compiled, though. Rather than compiling to an inner class, the main class compiles to the top-level class of the GDScript file. There are several limitations. * There can be at most one ``main`` class in a file. * There must be no conflicts between names defined inside the ``main`` class and names defined at the module level. That is, if a constant is defined at the module level, then there must be no constants or instance variables inside the class with the same name (up to normalization). Likewise, if a function or macro is defined at the top-level, then there must be no instance functions (static or otherwise) with the same name (again, up to normalization). Visibility Inside a Class ------------------------- A class name can, like most module declarations, be declared ``public`` or ``private``. However, the elements *inside* of a class have no visibility modifiers. There is no way to define private fields or instance functions in GDLisp. Everything defined inside of a class is presumed public. Name Normalization Within Classes --------------------------------- Name normalization works slightly differently inside of classes. The rules for *how* names are normalized within classes are the same as at the module level (see :ref:`name-normalization`). However, the difference is in how names are resolved. At the module level, a name must be referred to in the exact same way as it was defined. That is, if you define a function called ``foo-bar``, then you must call it as ``(foo-bar ...)``. Even though, at runtime, the resulting GDScript function will be called ``foo_bar`` (with an underscore in place of the dash), GDLisp will not allow you to call the function as ``(foo_bar ...)``. Inside of classes, the rules are much more lenient, owing to Godot's dynamic nature. You may access fields or call functions on classes and instances using *any* name that normalizes to the same name that was used to define the function or field. As a consequence, you can call built-in GDScript instance functions using the conventions of GDLisp, so you can get a child node from a node by writing ``(parent-node:get-node "ChildNodeName")``. This will normalize to a function call to ``get_node``, which is defined by Godot. ================================================ FILE: doc/readthedocs/reference/command-line.rst ================================================ The GDLisp Command Line Tool ============================ The core interface to the GDLisp compiler and programming language is the command line tool ``gdlisp``. This tool is written mostly in Rust, with a small portion written in GDScript for bootstrapping purposes. .. _building: Building -------- ``gdlisp`` is built using Rake, the Ruby task management tool. The following dependencies must be explicitly installed before building ``gdlisp``. * The ``rake`` command line tool must be installed, as it is necessary to run rakefiles. * The target version of Godot must be available on the target system and must be available on the system path under the name ``godot`` (case sensitive). * If building on Windows, you will also need the `win32-file gem `_. Once the dependencies are installed, to build the release version of GDLisp, it should suffice to run .. code-block:: bash $ GDLISP_RELEASE=1 rake build .. Note:: If you're building in a non-POSIX shell, you may have to initialize the environment variable ``GDLISP_RELEASE`` in some other way. After a successful build for release purposes, an executable will be placed at ``./bin/gdlisp``. At this point, if you plan to use GDLisp long-term, then you may wish to place that file on your system path. The Rakefile ^^^^^^^^^^^^ The Rakefile for this tool provides the following tasks. ``rake build`` Builds the entire project, including all dependencies. ``rake clean`` Cleans up any build artifacts. ``rake run`` Builds and then runs the ``gdlisp`` program. ``rake build_rs`` Builds only the Rust dependencies, not the Godot dependencies. ``rake test`` Runs the full GDLisp test suite. ``rake clippy`` Runs the Rust linter Clippy against the codebase. ``rake doc`` Generates the internal documentation of the codebase. That is *not* the page you're reading right now but consists of Rustdoc pages detailing the inner workings of the source code. When building the software, the build suite defaults to compiling in *debug mode*, which disables certain optimizations and produces better internal stack traces in GDLisp. To make a build for release purposes, define the ``GDLISP_RELEASE`` environment variable to any value. Running ------- Once you have the ``gdlisp`` command line tool, you may run it with ``--help`` to see the available options. Generally speaking, users of GDLisp will invoke the program in the following ways. * Invoking ``gdlisp`` with no arguments will open a REPL instance. * Invoking ``gdlisp`` with the name of one or more ``.lisp`` filenames will compile all of those files from GDLisp into GDScript. * Invoking ``gdlisp`` with the name of a directory will recursively search that directory, compiling all ``.lisp`` files into GDScript files. This is most useful if given the root directory of a Godot project (i.e. the folder containing your ``project.godot`` file). The REPL -------- Invoking ``gdlisp`` with no arguments drops you into a read-eval-print loop (or REPL, for short), where you can run arbitrary GDLisp declarations or expressions and see the output. .. _support-file: The GDLisp Support File ----------------------- GDLisp has a single-file support library that **must** be included in any project that uses GDLisp source code. This file defines all of the built-in functions and values available to GDLisp programmers, as well as providing some scaffolding necessary for the generated code as well. In a project that needs to utilize GDLisp source code, you must include the ``GDLisp.gd`` file. This file is bootstrapped from ``GDLisp.lisp`` and is produced into the root project directory as part of the build process above. 1. Copy and paste the ``GDLisp.gd`` file into the root of your project directory. 2. Create a Godot autoload (sometimes called a singleton) for this file in your project. The autoload must be called ``GDLisp`` (with that exact capitalization). All GDLisp source files will assume that this global variable exists. .. Warning:: Some versions of Godot try to choose the capitalization for your autoloads in a different way. Specifically, Godot may suggest the name ``GdLisp`` (with a lowercase ``d``) as a default name. The name of the autoload must be ``GDLisp``, with that *exact* capitalization, or GDLisp generated source files will not load. ================================================ FILE: doc/readthedocs/reference/datatypes.rst ================================================ Basic Datatypes =============== GDLisp compiles to GDScript and runs on the Godot engine, so the core datatypes for GDLisp are the same as those of GDScript. That is, GDLisp defines the following core types, identical to their representations in Godot. For more details about these basic types, see the `GDScript documentation `_. They are listed here for completeness. * A ``Null`` type with a single value. The single value of this type is written as ``()``, or ``nil``. (Note that ``()`` is the literal representation of the null value, whereas ``nil`` is merely a global constant which is *defined* to be ``()``) * A ``Bool`` type with two values, written as ``#f`` for false and ``#t`` for true. * An ``Int`` type for 64-bit signed integers. * A ``Float`` type representing double-precision floating point values. * A ``String`` type consisting of UTF-8 strings. * The ``Vector2`` and ``Vector3`` types, with literals written enclosed in ``V{`` and ``}``. Both types of vectors are written in this way; they are distinguished by the number of arguments to the syntactic form. * ``Rect2``, ``Transform2D``, ``Plane``, ``Quat``, ``AABB``, ``Basis``, ``Transform``, ``Color``, ``NodePath``, and ``RID`` all behave identically to GDScript. There is no literal syntax for any of these types. .. _cons-cell: Cons Cells ---------- GDLisp defines a ``Cons`` datatype. ``Cons`` values are constructed using the built-in function ``cons`` and store two fields: a ``car`` and a ``cdr``, both accessible by public field access on the cell. Cons cells are used to build up singly-linked lists to represent the abstract syntax tree of your code during macro expansion. Arrays ------ GDLisp supports all of the built-in Godot array types, including the base ``Array`` and all of its strongly-typed companions. Array literals are written in square brackets ``[`` and ``]``, or equivalently are constructed with the ``array`` function. Dictionaries ------------ GDLisp supports the built-in Godot ``Dictionary`` type, which maps keys to values. Dictionaries are constructed using the ``dict`` function, or equivalently enclosed in curly braces ``{`` and ``}``. The elements of a dictionary are *not* delimited by colons or commas; they are merely written side by side. For example, ``{key1 value1 key2 value2}``. Functions --------- ``Function`` is the type of first class functions in GDLisp. Functions are defined using top-level declaration forms such as ``defn`` or by the ``lambda`` or ``function`` special forms. Calling functions is done with the built-in ``funcall`` or its fully general companion ``apply``. .. _cell-type: Cells ----- A ``Cell`` is a simple type that has a single public field: ``contents``. A cell is constructed directly by the ``Cell`` constructor (``(Cell:new value)``). Cells are seldom used directly in GDLisp code but are frequently used behind the scenes to implement closures. Symbols ------- The ``Symbol`` data type represents atomic symbols in GDLisp. There is no equivalent to this in base Godot (though it is similar in principle to the Godot 4 type ``StringName``). A ``Symbol`` has a textual representation, similar to a string. Symbols are most commonly constructed by simply quoting a symbol literal, though they can also be constructed from strings using ``intern``, and unique, unused symbols can be constructed with ``gensym``. The Object Hierarchy -------------------- Like GDScript, GDLisp supports an object-oriented style of programming. GDLisp expands upon Godot's built-in inheritance hierarchy, introducing a partial ordering on all types. .. mermaid:: graph BT Nothing-->Symbol-->Reference-->Object-->AnyRef-->Any Nothing-->Node-->Object Nothing-->String-->AnyVal-->Any Nothing-->Int-->Number-->AnyVal Nothing-->Float-->Number Nothing-->Array-->BaseArray-->AnyVal Nothing-->Pool...Array-->BaseArray Nothing-->Prim["Primitive types"]-->AnyVal At the top of the hierarchy is the ``Any`` type, which contains all GDLisp objects, whether they inherit from the ``Object`` class or are primitives. Below that, the two types ``AnyRef`` and ``AnyVal`` partition the space of objects into two. ``AnyRef`` contains all of the object types and ``AnyVal`` contains all of the Godot primitives. On the ``AnyVal`` side, all of the primitive Godot types are provided as-is as subtypes of ``AnyVal``. Additionally, two new types are provided. ``Number`` is the lowest common supertype of ``Int`` and ``Float``, and ``BaseArray`` is a supertype of all of the array types in Godot (the generic ``Array`` as well as all of the strongly-typed "Pool" array types). Below ``AnyRef`` is ``Object``, the root of the Godot object hierarchy. In the current implementation of GDLisp, ``AnyRef`` and ``Object`` are equivalent as types, but this may change in the future if an alternative object hierarchy is added. Below ``Object``, all of the class types available in Godot fit into the hierarchy in the same way they do in GDScript. In particular, user-defined classes can subclass ``Object``, ``Node``, ``Reference``, or any of the other types freely. Finally, ``Nothing`` is the unique bottom type of the hierarchy. ``Nothing`` is a subtype of *every* type, and there is no value which is a member of the ``Nothing`` type. Note carefully: Not even the special null value ``()`` is a member of the ``Nothing`` type; ``Nothing`` is uninhabited *by design*. ================================================ FILE: doc/readthedocs/reference/expressions.rst ================================================ Expressions =========== Expressions are the forms that make up the body of functions and the initial value of variables and constants. Expressions are the basic unit of "work" in a GDLisp program. Note that GDLisp is an expression-oriented language, which means that there is no distinction between statements and expressions in GDLisp. Even though we may colloquially refer to the ``if`` special form as an "if statement", it is an expression like any other. This commitment to expressions allows for better composability, as any expression can be nested inside of any other. There are several different types of expressions, all of which are detailed below. Atoms ----- There are several S-expressions which compile natively to themselves. These include the null object ``()``, integer literals, floating-point literals, string literals, and the Boolean constants ``#t`` and ``#f``. Note that this explicitly does *not* include symbol literals, which are evaluated differently. See :ref:`expr-variable-names` below. .. _expr-variable-names: Variable Names -------------- A bare symbol appearing in expression context is treated as a variable name. All variables in GDLisp are lexically-scoped. That is, any variable is declared to be either scoped to the current module or to a narrower scope, such as a ``let`` block or a function body. It is an error to refer to a variable name that does not exist. A bare symbol in expression context always refers to a name in the value namespace. Variable names can be *shadowed*, which means that a bare symbol always refers to the name in the *narrowest scope* for which it is defined. Note that bare symbols appearing in the code are subject to macro expansion for :ref:`symbol macros `. Function Calls -------------- A proper, nonempty list will be interpreted in one of several ways, depending on the value of the first term (or head) of the list. The head of a function call can be one of three things. It can be a symbol literal, a ``literally`` form, or an ``access-slot`` form. Ordinary Calls ^^^^^^^^^^^^^^ An ordinary call is one whose head is a symbol literal. Ordinary calls can resolve to one of several forms, depending on the value of the head. First, if the value of the head is the start of one of GDLisp's :ref:`special forms `, then the call is treated as a special form, even if there is a function name in the current scope which would shadow that special form. These special forms are baked into the GDLisp compiler and are the primary bootstrapping tool on which we build up syntax. There is no way to define new special forms in GDLisp. If the head is not the start of one of the special forms, then it must be a valid name in the function namespace of the current lexical scope. Like values, names in the function namespace are always accessed starting from the innermost scope and falling back to outer scopes, which makes names in the function namespace subject to shadowing as well. If the name refers to a macro, then :ref:`macro expansion ` occurs. If the name refers to an ordinary function, then the call compiles to a runtime call to the function with the given name. Literal Calls ^^^^^^^^^^^^^ A literal call is a call whose head is the form ``(literally name)``, where ``name`` is an arbitrary symbol. ``literally`` calls are like :ref:`literal forms `, except that they are function calls rather than values. That is, a literal call always expands to a call to the function with the given name in the current scope in GDScript, without regard to whether or not that call is valid or makes sense. ``literally`` calls never undergo macro expansion and are never considered special forms. .. Warning:: All of the same caveats that apply to literal forms apply to literal calls, and they should be used with caution. Method Calls ^^^^^^^^^^^^ A method call is a call whose head is of the form ``(access-slot expr name)``, where ``expr`` is an arbitrary expression and ``name`` is a literal symbol. In cases where the operator precedence is not ambiguous, these ``access-slot`` forms are usually written using the ``:`` notation as ``expr:name``. That is, to call the method named ``bar`` on an object called ``foo``, with three arguments, we would write ``(foo:bar 1 2 3)``, which is syntax sugar for ``((access-slot foo bar) 1 2 3)``. The first argument to ``access-slot`` is an arbitrary expression, on which the method will be called, and ``name`` will be :ref:`normalized ` to the name of the method to call on the object. GDLisp does *not* validate the name of the method or that it exists on the object referred to by the expression. If the first argument to ``access-slot`` is the literal name ``super``, then the method call is a :ref:`super call `. .. _expr-super-calls: Super Calls """"""""""" :: (super:method-name args ...) A method call where the left-hand side of the ``access-slot`` call is the literal symbol ``super`` is a super call. A super call can only occur in a context where the ``self`` name exists (i.e. inside of a class body) and will call the method with the given name, using ``self`` as the target of the call, but only considering methods defined in the superclass of the current class body. Note that calls to a superclass' *constructor* are handled specially. Specifically, they are *not* written as ``super:_init``, and in fact they are not even expressions in the strictest sense of the word. A call to a superclass' constructor is a special part of the ``_init`` definition syntax. For more details, see :ref:`constructor-functions`. .. _expr-special-forms: Special Forms ------------- Special forms are elements of the GDLisp syntax that have special meanings baked into the compiler. Special forms can be thought of as similar to macros but more primitive, the building blocks on which GDLisp syntax is constructed. There are 25 special forms in GDLisp. ``access-slot`` Forms ^^^^^^^^^^^^^^^^^^^^^ :: expr:field (access-slot expr field) The ``access-slot`` form, usually written using the infix ``:`` notation, accesses a field on an object. That is, ``expr`` is evaluated, and then the slot with the name ``field`` (which must be a symbol literal) is returned from the object referenced by the expression. The field name is not validated. That is, GDLisp makes no effort to ensure that the name ``field`` is a field that exists on the type of ``expr``. There is one exception to this rule. If ``expr`` is the literal name of an enumeration (a la ``defenum``) whose definition is statically known, GDLisp will validate that the name ``field`` is actually a defined enumeration constant on that type. .. _expr-assert: ``assert`` Forms ^^^^^^^^^^^^^^^^ :: (assert condition) (assert condition message) The ``assert`` special form evaluates the condition and message as expressions. If ``condition`` is true, then the code proceeds as planned. If ``condition`` is false, then an error is generated, using ``message`` in the error message if provided. ``assert`` special forms are only used when the resulting Godot runtime is in debug mode. In release mode, these forms will be ignored, and their arguments will not even be evaluated. As such, arguments which have side effects should generally not be given to ``assert``. ``break`` Forms ^^^^^^^^^^^^^^^ :: (break) ``break`` is a special form that can only be used inside of loop contexts. ``break`` exits the current loop and continues immediately after the loop body. ``cond`` Forms ^^^^^^^^^^^^^^ :: (cond clauses ...) A ``cond`` form is the most basic form of conditional in GDLisp. ``cond`` takes zero or more clauses. Each clause's conditional portion is evaluated in turn. If the conditional is true, then the clause's body portion is evaluated and returned. Otherwise, the next clause is tried. If all clauses are exhausted, then the null object ``()`` is returned. Each clause must be a proper list containing one or more elements. If the list contains at least two elements, then the first element is the conditional term and the rest form the body of the clause. The body is treated as though it is inside a ``progn``, so the last expression will be returned. If the list contains only one element, then that element is *both* the condition and the body of the clause, and it will only be evaluated once. For example, :: (cond ((foo1) (bar1) (baz1)) ((foo2) (bar2))) This is a ``cond`` form consisting of two clauses. When this form is evaluated, first, we will call the function ``foo1`` with no arguments. If that function returns a truthy value, then we call ``bar1`` and then ``baz1``, using the latter as the result of the whole ``cond`` form. If ``foo1`` returns a falsy value, then we try ``foo2``. If ``foo2`` evaluates to a truthy value, then ``bar2`` is evaluated and its result is returned. Otherwise, ``()`` is returned as a default value. As an example of the one-argument clause form, consider :: (cond ((my-dict:get "x")) ((my-dict:get "y")) ((my-dict:get "z"))) Assuming ``my-dict`` is a dictionary object, this expression will attempt to get the keys ``x``, ``y``, and then ``z`` from the dictionary in order, returning the first one which exists and is truthy. If none satisfy the condition, then ``()`` is returned as a default value. Note that if all you want is a simple if statement with a "true" and a "false" case, then the :ref:`if macro ` may be more convenient for your use case. .. Tip:: A common idiom is to make the condition of the last clause be the literal ``#t`` true object. This acts as a sort of "else" clause, triggering unconditionally if all of the other branches fail. ``continue`` Forms ^^^^^^^^^^^^^^^^^^ :: (continue) ``continue`` is a special form that can only be used inside of loop contexts. ``continue`` exits the current loop iteration and continues the next iteration of the loop. .. _expr-flet: ``flet`` Forms ^^^^^^^^^^^^^^ :: (flet (clauses ...) body ...) An ``flet`` form is similar to a ``let`` form except that it binds functions in the function namespace, rather than arbitrary values in the value namespace. Specifically, an ``flet`` form creates a new local scope and defines zero or more functions in that local scope. Then the body is executed in that scope and its final value returned. Each function clause takes the following form. :: (name (args ...) body ...) ``name`` is a symbol literal indicating the name of the local function, ``args`` is an :ref:`ordinary lambda list `, and ``body`` is the body of the function. When the function called ``name`` is invoked inside of the ``flet`` form's body, the given arguments will be bound and the body will be executed in a new lexical scope, cloned from the scope in which ``flet`` itself was defined. Note that the bodies of the local functions are evaluated in a scope cloned from the one in which the ``flet`` form was defined, not the inner scope created by ``flet``. That is, the local function bodies defined by an ``flet`` do not have access to each other's names or to their own. For a version of ``flet`` that does have such access, see :ref:`expr-labels`. The bodies of ``flet`` local functions can create :ref:`closures `. ``flet`` creates a loop barrier between the enclosing scope and the clauses of the ``flet`` form. This means that a ``break`` or ``continue`` expression inside of a clause of the ``flet`` cannot be used to control a loop that began outside of the clauses. This constraint does not exist for the body of the ``flet``, only the clauses. ``for`` Forms ^^^^^^^^^^^^^ :: (for var iterable body ...) A ``for`` form is the second of the two most basic looping constructs in GDLisp. The first argument to ``for`` must be a literal symbol, then the other arguments are arbitrary expressions. First, ``iterable`` is evaluated once, and it must evaluate to an array (including pool arrays), string, or dictionary object. Then a new lexical scope is created. Then ``body`` is run in that lexical scope, once for each element of the iterable object. At each loop iteration, the variable ``var`` is bound to the current value. ``for`` forms always return the null object ``()``. For arrays, a ``for`` form iterates over each element of the array. For dictionaries, a ``for`` form iterates over each *key* of the dictionary, consistent with Python's semantics for the same. For strings, a ``for`` form iterates over each character (as a single-character string) of the string. A ``for`` loop defines a loop context for its body. This means that ``break`` and ``continue`` can be used in the body of a ``for`` loop. .. Warning:: Note that the behavior is undefined if the result of ``iterable`` is not an array, dictionary, or string. Currently, ``for`` loops in GDLisp compile to ``for`` loops in GDScript, which means some legacy GDScript behavior (such as iterating over numerical literals) may work, but this behavior may change in the future, so it is always recommended to explicitly call ``range`` if the intent is to iterate up to a number. .. _expr-function: ``function`` Forms ^^^^^^^^^^^^^^^^^^ :: #'name (function name) The ``function`` special form is used to take a function that exists in the function namespace and convert it into a first-class value. The ``function`` form is often abbreviated using the (equivalent) syntax ``#'name``. The ``name`` argument must be a symbol, and it must be a valid name in the function namespace of the current lexical scope. A function object is created (as a value) which, when called, invoked the function with the given name, forwarding all arguments. The name can refer to a function defined at module scope or to a local function defined in the current scope. In the latter case, the local function will be kept alive (by reference semantics) until the function object constructed by this ``function`` form is discarded. That is, it is permitted to have a reference to a local function which outlives the scope of that local function's binding. ``function`` *cannot* be used to create references to instance methods. Explicit ``lambda`` expressions must be used to do so. .. Warning:: The target name of a ``function`` form must be the name of a valid function. If the name refers to a macro, then the behavior is undefined. .. _expr-labels: ``labels`` Forms ^^^^^^^^^^^^^^^^ :: (labels (clauses ...) body ...) ``labels`` works nearly identically to ``flet`` and carries the exact same syntax. However, whereas an ``flet`` form evaluates its local function bodies in the enclosing scope of the ``flet`` block, a ``labels`` form evaluates its function bodies in the inner scope of the ``labels`` block itself. This means that the functions defined in a ``labels`` block have access to each other and to their own name, allowing them to be recursive or mutually recursive. The bodies of ``labels`` local functions can create :ref:`closures `. ``labels`` creates a loop barrier between the enclosing scope and the clauses of the ``labels`` form. This means that a ``break`` or ``continue`` expression inside of a clause of the ``labels`` cannot be used to control a loop that began outside of the clauses. This constraint does not exist for the body of the ``labels``, only the clauses. .. _expr-lambda: ``lambda`` Forms ^^^^^^^^^^^^^^^^ :: (lambda (args ...) body ...) A ``lambda`` form defines a local function without giving it a name. The argument list ``args`` is an :ref:`ordinary lambda list `. A new function object is created, which exists as a value (hence, can be assigned to variables in the value namespace or passed as an argument to a function). When the function created by this form is invoked, a new lexical scope is created, which is cloned from the lexical scope in which the ``lambda`` was first defined. Then the arguments are bound and the body is run, just like any other function. The ``lambda`` body can create :ref:`closures `. ``lambda`` creates a loop barrier between the enclosing scope and the body of the ``lambda`` form. This means that a ``break`` or ``continue`` expression inside of the body of the ``lambda`` cannot be used to control a loop that began outside of the body. ``let`` Forms ^^^^^^^^^^^^^ :: (let (clauses ...) body ...) ``let`` is the most basic form of local variable binding in GDLisp. A ``let`` form creates a new lexical scope in which zero or more local variables are bound, and then runs ``body`` in that local scope. The value of the final expression of ``body`` is returned, or ``()`` if ``body`` is empty. Each variable clause takes one of the following forms. :: var-name (var-name initial-value ...) In the second (and most general) form, a variable clause takes the form of a proper list whose first element is a literal symbol indicating the name of the variable to declare. The remaining elements are evaluated to determine the variable's initial value. Note carefully: the ``initial-value`` expressions are evaluated in the *outer* scope, not in the newly-created scope that the variable is being declared in. This means that, in a ``let`` statement which declares multiple variables, none of the variables have access to each other during initialization, even those declared later in the same block. The ``initial-value`` block is treated as a ``progn`` block, so if the block is empty then ``()`` is used as the variable's initial value. A variable name ``var-name`` that appears on its own (that is, a symbol literal *not* contained in a sublist) is treated as ``(var-name)`` and will initialize the variable to ``()``. ``let`` always binds in the value namespace. To bind in the function in the function namespace, see :ref:`expr-flet` and :ref:`expr-labels`. .. _expr-literal-forms: ``literally`` Forms ^^^^^^^^^^^^^^^^^^^ :: (literally variable-name) A ``literally`` form is a backdoor through the GDLisp scoping system. The sole argument to ``literally`` must be a symbol literal. ``(literally x)`` will be translated into the variable name ``x`` in the resulting GDScript code. This will be done **without any consideration** to whether or not ``x`` is a valid variable name. GDLisp will not check that the name is defined, or what scope it is defined in. GDLisp will merely assume that you know what you're doing and pass the name through. The name ``variable-name`` given to this form will undergo a partial form of :ref:`name normalization `. Specifically, ``variable-name`` will be escaped in the same way as an ordinary variable name, with the exception that GDScript reserved words will not be prefixed with an underscore. Care must be taken when using ``literally``. Since GDLisp does not perform any semantic analysis on the given name, it cannot guarantee that the name is valid, or even syntactically makes sense in GDScript in the case of keywords. Additionally, names referenced inside of ``literally`` will not have closures created for them if they occur inside of a ``lambda`` or other closure-producing construct. This can result in difficult-to-debug situations that GDLisp cannot handle. The primary intended use case for ``literally`` is to port future GDScript functions to GDLisp without having to wait on official support from the GDLisp compiler. If a future iteration of Godot adds a function called ``frobnicate`` to the global namespace, then you can call that function by using the name ``(literally frobnicate)``, even if the version of the GDLisp compiler you're using is not aware that such a function exists. ``macrolet`` Forms ^^^^^^^^^^^^^^^^^^ :: (macrolet (clauses ...) body ...) A ``macrolet`` form is syntactically identical to an ``flet``. However, whereas ``flet`` binds functions in the function namespace of the current scope, ``macrolet`` binds *macros* in the same namespace. The macros defined by a ``macrolet`` are only defined inside of the ``body`` scope. During that scope, those names are subject to macro expansion. ``macrolet`` creates a loop barrier between the enclosing scope and the clauses of the ``macrolet`` form. This means that a ``break`` or ``continue`` expression inside of a clause of the ``macrolet`` cannot be used to control a loop that began outside of the clause. This constraint does not exist for the body of the ``macrolet``. .. Warning:: The clauses of a ``macrolet`` form **cannot** create closures. If a locally-defined macro depends on a local variable or function defined in an enclosing scope, then the behavior is undefined. ``new`` Forms ^^^^^^^^^^^^^ :: (new Superclass body ...) (new (Superclass args ...) body ...) The ``new`` form constructs a new local *anonymous* class. That is, ``new`` is to ``defclass`` as ``lambda`` is to ``defn``. The newly-defined class is not given a name, and the only instances of that class are those created by this particular ``new`` form. When this form is evaluated, an instance of a subclass of ``Superclass`` is constructed. The body of this subclass shall be ``body``, which can consist of zero or more :ref:`class declarations `, with the exception that it is illegal to define static methods in an anonymous class. The constructor of this class shall be invoked with ``args``, or with zero arguments if the non-parameterized version of ``new`` is used. The body of a ``new`` statement is capable of creating :ref:`closures `. ``new`` creates a loop barrier between the enclosing scope and the body of the ``new`` form. This means that a ``break`` or ``continue`` expression inside of the body of the ``new`` cannot be used to control a loop that began outside of the body. .. Attention:: ``new`` is *not* a general-purpose constructor. Programmers used to Java or C# may be used to prefixing type names with ``new`` to construct ordinary instances of the type. That is not how object construction works in GDLisp. To construct ordinary instances of some class, call the method ``new`` on that class, such as ``(ClassName:new 1 2 3)``. The ``new`` special form is only intended to be used when behavior (such as instance variables or methods) is being added anonymously to the class for this instance alone. .. _expr-preload: ``preload`` Forms ^^^^^^^^^^^^^^^^^ :: (preload name) ``preload`` is a special form which takes a single string literal as argument. The string literal must be the name of a file which can be imported, using the Godot ``res://`` notation. ``preload`` works like the built-in function ``load`` but performs the act of loading at compile-time. It is an error if the pathname does not point to a file. .. Tip:: In GDLisp, most ``preload`` calls should be replaced with ``use`` directives. See :ref:`imports` for details. ``preload`` can be used in situations (such as macro expansion) where the name being loaded may not be known at definition time but will be known before compilation is complete. .. _expr-progn: ``progn`` Forms ^^^^^^^^^^^^^^^ :: (progn args ...) A ``progn`` form evaluates each of its arguments in order and returns the final argument. ``progn`` is a useful way to insert multiple expressions which have side effects in a context, such as the right-hand side of a ``defvar``, that only accepts one expression. An empty ``progn`` silently returns ``()``, the null object. .. Note:: ``progn`` can also be used in declaration (or class declaration) context. See :ref:`progn` for details. ``quasiquote`` Forms ^^^^^^^^^^^^^^^^^^^^ :: `s-expression (quasiquote s-expression) A ``quasiquote`` form refuses to evaluate its argument and returns the S-expression representing it, similar to ``quote``. However, ``unquote`` and ``unquote-spliced`` have special meaning inside of ``quasiquote`` forms. See :ref:`quoting` for more details. ``quote`` Forms ^^^^^^^^^^^^^^^ :: 's-expression (quote s-expression) A ``quote`` form refuses to evaluate its argument and returns the S-expression representing it verbatim. See :ref:`quoting` for more details. Note that a ``quote`` form is usually written abbreviated as ``'s-expression``. ``return`` Forms ^^^^^^^^^^^^^^^^ :: (return expr) Evaluates the expression and then returns that expression immediately from the enclosing function or instance method. .. _expr-set: ``set`` Forms ^^^^^^^^^^^^^ :: (set variable value) ``set`` is the basic form of variable and name assignment in GDLisp. It can do several different things, depending on the nature of the ``variable`` portion of the form. Variable Assignment """"""""""""""""""" If ``variable`` is a literal symbol, then it is interpreted as a variable name in the value namespace. The variable pointed to by that name is assigned a new value, namely the result of evaluating ``value``. The variable must be mutable, or a compile error will be issued. The value that was assigned is returned from the ``set`` form. Field Assignment """""""""""""""" If ``variable`` is of the form ``(access-slot object target)``, where ``object`` is an arbitrary expression and ``target`` is a literal symbol, then the assignment will modify an instance field on the instance to which ``object`` evaluates. The field's name shall be ``target``, after :ref:`name normalization `. This can trigger :ref:`setter functions `. The value that was assigned is returned from the ``set`` form. It is unspecified whether this will invoke a getter function on the class, if one exists. Delegated Assignment """""""""""""""""""" If ``variable`` is a proper list of the form ``(head args ...)`` where ``head`` is a literal symbol that is *not* ``access-slot``, then the assignment is a delegated assignment. The form :: (set (some-function args ...) value) will compile into the function call :: (set-some-function value args ...) That is, a ``set`` on a function call will compile to a call to the function whose name is the former function with ``set-`` prepended to it. The right-hand side of the assignment will be the first argument passed to the delegated function. The return value of the function is returned from the ``set`` form, so by convention a function intended to be used in this way should return the assigned value. ``symbol-macrolet`` Forms ^^^^^^^^^^^^^^^^^^^^^^^^^ :: (symbol-macrolet (clauses ...) body ...) A ``symbol-macrolet`` clause binds local macros, just like ``macrolet``, but the former binds *symbol* macros, which are subject to macro expansion when a literal symbol is used. Each clause is of the form :: (name value) Both parts are mandatory. ``name`` is the name of the symbol macro (which will be bound in the value namespace). ``value`` is the expression which should be run to evaluate the macro. ``symbol-macrolet`` creates a loop barrier between the enclosing scope and the clauses of the ``symbol-macrolet`` form. This means that a ``break`` or ``continue`` expression inside of a clause of the ``symbol-macrolet`` cannot be used to control a loop that began outside of the clause. This constraint does not exist for the body of the ``symbol-macrolet``. .. Warning:: Like ``macrolet``, the clauses of a ``symbol-macrolet`` **cannot** create closures. It is undefined behavior to write a local symbol macro that depends on a local variable or function defined in an enclosing scope. ``unquote`` Forms ^^^^^^^^^^^^^^^^^ :: ,expr (unquote expr) An ``unquote`` form can only be used inside of a ``quasiquote`` form. It is an error for this special form to appear in an expression context. ``unquote-spliced`` Forms ^^^^^^^^^^^^^^^^^^^^^^^^^ :: ,.expr (unquote-spliced expr) An ``unquote-spliced`` form can only be used inside of a ``quasiquote`` form. It is an error for this special form to appear in an expression context. ``while`` Forms ^^^^^^^^^^^^^^^ :: (while condition body ...) (while condition) A ``while`` form is one of the two most basic forms of looping in GDLisp. A ``while`` form takes a condition and then zero or more expressions forming a body as arguments. The ``while`` loop iterates zero or more times. At each iteration, the loop runs the condition first. If the condition is falsy, then the loop exits immediately. Otherwise, the body runs, and then the loop starts over. A ``while`` loop always returns the null object ``()``. It is possible to have a ``while`` loop where the body is empty, in which case, the condition is evaluated multiple times until it returns a falsy value. This can be used to emulate the "do ... while" construct seen in some programming languages, where the condition is evaluated at the end of the body, rather than the beginning. That is, to emulate such a construct in GDLisp, consider :: (while (progn body ... condition)) A ``while`` loop defines a loop context, for both its condition and its body. This means that ``break`` and ``continue`` can be used in either the condition or the body. .. _expr-yield: ``yield`` Forms ^^^^^^^^^^^^^^^ :: (yield) (yield object signal) The ``yield`` special form behaves similarly to the GDScript function of the same name. Called with zero arguments, ``yield`` halts the current function and returns a function state object from the function. That function state object has a ``:resume`` method which will return to the halted function at the same point it was yielded from. If ``yield`` is called with two arguments, then both arguments are evaluated. The first is treated as an object and the second shall evaluate to a string which is the name of a signal on the given object. The function still halts and returns a state object, just as if ``yield`` was called with no arguments. However, if the given object ``object`` ever fires the signal called ``signal``, then the function resumes automatically, without an explicit call to ``:resume``. It is an error to call ``yield`` with exactly one argument. Note that ``yield`` is a special form, not a function, despite it evaluating its arguments in applicative order. Functions in GDLisp must satisfy `η-reduction `_. That is, in order for ``yield`` to be a function, it would have to be the case that ``yield`` and ``(lambda (&rest args) (apply #'yield args))`` are equivalent functions. This is not true for ``yield``, since the former, when called, will halt the current function, whereas the latter will halt an inner function and return a (somewhat useless) function state object that resumes at the end of the inner function. .. _expr-capture: Closures and Name Capture ------------------------- Several special forms create *closures*. A closure is a nested scope that can outlive its containing scope. We call the variables which are placed inside a closure for such forms *captures*. This is perfectly acceptable. If a variable is defined in a local scope and then captured by a ``lambda`` or other special form, then that variable will remain in existence for as long as the ``lambda`` object exists. Function objects always have ``Reference`` semantics, which means that a function object (created with ``lambda`` or ``function``) will be freed when the last reference to it is freed. This ensures that closures created in this way are freed promptly. Custom objects created with ``new`` will follow the semantics of their superclass eventual (``Reference`` subclasses will have reference semantics, while ``Object`` and ``Node`` subclasses will have to be freed explicitly), so some care must be taken in those situations to prevent a memory leak. A closure is a read-write binding to a variable name. That means that the values in a closure are captured by *reference*, not by value. If the inside of a closure (such as a ``lambda``) modifies a captured variable, then the enclosing scope (and, by extension, any other closure that captured the same variable) will be able to see that change. For example, this lambda will return one number higher each time it's called. :: (let ((accum 0)) (lambda () (set accum (+ 1 accum)))) We can call this function, for example, as follows. :: (defn create-counter () (let ((accum 0)) (lambda () (set accum (+ 1 accum))))) (defn _ready () (let ((counter (create-counter))) (print (funcall counter)) ; Prints 1 (print (funcall counter)) ; Prints 2 (print (funcall counter)))) ; Prints 3 The variable ``accum`` is captured by the ``lambda`` *by reference*, so when we modify the variable, that modification is reflected in future calls to the ``lambda``, since there is truly only one copy of that variable. Forms that define local macros can never capture local variables or functions from an enclosing scope. ================================================ FILE: doc/readthedocs/reference/imports.rst ================================================ .. _imports: Import Directives ================= GDLisp uses the special ``use`` directive to import names and resources from other files. Although it is technically possible to import resources in the GDScript way (by defining constants whose values are ``preload`` calls), it is not recommended. There are three different forms to the ``use`` directive, all of which begin with the symbol ``use``, followed by the path name as a literal string. :: ;; Qualified import (use "res://filename.lisp" as alias-name) ;; Open import (use "res://filename.lisp" open) ;; Explicit import (use "res://filename.lisp" (...)) The path name is always a string and follows the same rules as Godot paths. See `File system `_ for details. Importing names involves adding names (in either of the value or function namespaces) to the module's lexical scope. Names added by a ``use`` directive are always imported as ``private``, and hence cannot be transitively imported into other modules through the current one. Qualified Imports ----------------- :: (use "res://filename.lisp") (use "res://filename.lisp" as alias-name) A qualified import takes the form of either the ``use`` symbol followed by the path name and nothing else, or the ``use`` symbol followed by ``as`` and then followed by the desired alias name. If the alias name is not provided, then the default alias shall be the name of the file, without the path or the file extension. So the import ``"res://foo/bar/baz.lisp`` would have a default alias name of ``foo/bar/baz``. Remember that forward slashes are valid in identifiers in GDLisp. Every public identifier in the target module will be imported into the current scope, in the appropriate namespace. The name in the current scope shall be the import's alias name, followed by a forward slash, followed by the identifier's name in the source module. As an example, suppose the module ``MyModule.lisp`` defines a public function called ``get-name``. Then the directive ``(use "res://MyModule.lisp" as MySpecialModule)`` would import that function into the current scope as ``MySpecialModule/get-name``. Non-GDLisp Imports ^^^^^^^^^^^^^^^^^^ The ``use`` directive is used for both importing other GDLisp files, as well as for importing GDScript files, scenes, textures, and essentially anything you would use ``preload`` for in GDScript. However, the GDLisp compiler can only parse other GDLisp files, so any non-GDLisp file must be imported using the qualified import, not an explicit or open import. This restriction may be loosened in the future. In the case of a non-GDLisp file, the alias name itself is introduced into the module's lexical scope in the value namespace. In the case of a GDScript file, the name refers to the top-level class defined in that file. In the case of other resources, the name refers to the resource itself. Open Imports ------------ :: (use "res://filename.lisp" open) An open import is similar to a qualified import except without the qualifier. An open import introduces all of the public names from the target module into the current scope, keeping their names identical to the original names from the source module. Explicit Imports ---------------- :: (use "res://filename.lisp" (...)) An explicit import does *not* import all of the public names into the current scope. Instead, it imports only the names specified. Each element of the list that follows the pathname identifies a single name to be imported. The most general form for these elements is :: (source-name namespace as destination-name) where ``source-name`` and ``destination-name`` are arbitrary identifiers, and ``namespace`` is one of ``value`` or ``function``. This imports the name ``source-name`` from the given namespace into the current module scope (in the same namespace), giving it the name ``destination-name``. The ``as destination-name`` portion may be omitted and, if left out, will be assumed to be the same as the ``source-name``. The ``namespace`` may also be omitted if unambiguous. If the namespace is omitted, then it will be inferred from the available names in the source module. If there is *both* a function and a value with that name in the source module, then an error will be issued. Finally, if both the alias name and the namespace are omitted, then the symbol for the name must be passed on its own, not inside of a sublist. In summary, the four possible forms of a named import are :: (source-name namespace as destination-name) (source-name namespace) ;; destination-name implied to be source-name (source-name as destination-name) ;; namespace is inferred from context source-name ;; namespace is inferred, and destination-name is implied to be source-name ================================================ FILE: doc/readthedocs/reference/index.rst ================================================ .. _comprehensive: Reference Documentation ======================= .. toctree:: :maxdepth: 2 :caption: Contents: parser.rst datatypes.rst source-file.rst lambda-lists.rst macros.rst classes.rst imports.rst expressions.rst quoting.rst command-line.rst standard-library/index.rst ================================================ FILE: doc/readthedocs/reference/lambda-lists.rst ================================================ Lambda Lists ============ Functions, macros, and constructors in GDLisp take arguments. As part of their declaration syntax, any of these declarations must declare the collection of *formal arguments* that they take. These formal arguments take the form of a *lambda list*, which is a special sub-language that communicates what arguments are permitted to the GDLisp compiler. Broadly speaking, a lambda list is a list of symbols indicating variable names to bind arguments to. As part of this list of symbols, special directives beginning with an ampersand ``&`` can change the behavior of the following arguments, such as making required arguments optional. Note that a name beginning with an ampersand in a lambda list is *always* treated as a special directive. It is an error to use a special directive that does not exist, or to use a directive in a context that disallows it. As a consequence, it is not possible to declare arguments in a lambda list whose names begin with an ampersand. It is generally not recommended to declare variable names beginning with an ampersand at all for this reason. It is an error to list the same name twice in a lambda list. Likewise, it is an error to list the same special directive twice, unless otherwise stated. A special directive generally only lasts until the next special directive. Unless otherwise stated, directives do not "stack". There are different types of lambda lists, depending on the context in which it appears. .. _simple-lambda-lists: Simple Lambda Lists ------------------- The simplest kind of lambda list is called, fittingly, a *simple lambda list*. A simple lambda list is a collection of zero or more required arguments, with no special directives or alternative forms allowed. That is, a simple lambda list is always a simple list of literal symbols. Simple lambda lists are used in the declaration of class functions and signals, which do not support more advanced argument parsing. Examples:: () (arg1 arg2) (foo bar baz) .. _ordinary-lambda-lists: Ordinary Lambda Lists --------------------- Ordinary lambda lists are the most common kind of lambda list in GDLisp, used for module-level functions and macros, as well as lambda expressions. An ordinary lambda list is a list of symbol names of arguments. An ordinary lambda list accepts several special directives. All special directives are optional. If multiple directives are provided, then ``&opt`` must be the first of them, and only one of ``&rest`` or ``&arr`` can be provided. * Any arguments that appear before any directives are treated as *required parameters*. It is an error to call a function or macro with fewer arguments than the number of required parameters in its lambda list. * Any arguments that appear after the ``&opt`` directive are optional parameters. If too few arguments are provided and the rest of the parameters are optional, then any optional parameters that have not been bound receive a default value of ``()``, the null object. * If the ``&rest`` directive is used, it must be followed by a single name, which indicates the name of the function's "rest" argument. After binding any required and optional arguments, all remaining arguments to the function are collected into a list and passed as the function's "rest" argument. If there are no extra arguments, then the empty list ``()`` is passed. Note that it is impossible to call a function which declares a ``&rest`` argument with "too many" arguments, as all extras will be collected into one variable. * The ``&arr`` directive works similarly to ``&rest``. It must be followed by a single name. All remaining arguments, after binding required and optional arguments, will be collected and passed at the "arr" argument, but in this case they will be collected into an array rather than a list. Examples:: () (arg1 arg2) (required-arg1 required-arg2 &opt optional-arg1 optional-arg2) (&rest all-args) (&arr all-args-as-array) (required-arg &opt optional-arg &rest all-remaining-args) .. _constructor-lambda-lists: Constructor Lambda Lists ------------------------ A constructor lambda list is a lambda list used to indicate the arguments to a class' ``_init`` constructor. A constructor lambda list is similar to a simple lambda list but allows for one additional feature. A constructor lambda list consists of a list of arguments. Each argument can be a symbol, similar to a simple lambda list. However, arguments can also take the special form ``@name`` (or, written literally, ``(access-slot self name)``), where ``name`` is any valid symbol identifier. An argument of this form will *not* be bound to a local variable when the constructor is called. Instead, the value passed for that argument will be bound to the instance variable with the given name on ``self``. If no such instance variable exists, then an error will be issued at runtime. This allows a very compact representation of classes whose constructors merely initialize fields. .. code-block:: (defclass Enemy (Node) (defvars name attack-points defense-points) (defn _init (@name @attack-points @defense-points))) Examples:: (foo bar) (x y z) (@x @y @z) (some-ordinary-arg @some-instance-var) ================================================ FILE: doc/readthedocs/reference/macros.rst ================================================ .. _macros: Macros ====== Macros are the key feature that separates a Lisp dialect from many conventional programming languages. A macro is a mechanism to dynamically generate code during compilation. Macros are subdivided into two categories in GDLisp: functional macros and symbol macros. Functional macros are far more common, so this documentation will often refer to them simply as "macros" with the "functional" part understood. A (functional) macro declaration looks similar to a function declaration, except that the former uses ``defmacro`` rather than ``defn``. .. code-block:: (defmacro macro-name (args ...) body ...) (defmacro macro-name (args ...) public-or-private body ...) A macro's argument list takes the form of a :ref:`ordinary lambda list `, which means that macros can take optional and variable arguments. When a macro declaration is encountered, all names that it references, either directly or indirectly, must always be fully defined. That is, while two functions can mutually depend on each other, regardless of the order in which they're defined in a file, a macro cannot depend on a function or constant defined *later* in the current file. Macros are defined immediately, and are available to the compiler. The name of the macro is bound, in the function namespace, to the given macro object for the current module. A macro is a function-like object that is invoked during compilation. Specifically, whenever the compiler is expecting a declaration or an expression and encounters a proper, nonempty list ``(foo args ...)``, it will first check whether it is aware of a macro defined in the current scope with the name ``foo``. This process is referred to as *macro expansion*. If there is a macro with that name, then rather than interpreting the code as an expression or declaration, the macro will be invoked with the given arguments. Note carefully that the macro's arguments are passed in *unevaluated*. That is, if there is a macro called ``example`` and it is called as ``(example arg)``, then the literal symbol ``arg`` will be passed in, *not* the value of a variable called ``arg``. Likewise, if it is called as ``(example (+ 1 1))``, then the literal three-element list ``(+ 1 1)`` will be passed in, not the number ``2``. A macro must return an S-expression, which will replace the macro call in the current position of the code. If a macro is called in declaration context, then the result of the macro call will be treated as a declaration. If a macro is called in expression context, then the result of the macro call will be treated as an expression. The result of a macro call is itself subject to macro expansion. A macro call can return an S-expression which itself calls another macro, or even the same macro recursively. The behavior is undefined if a macro exhibits infinitely recursive behavior. That is, the following macro will exhibit undefined behavior if it is ever invoked. :: (defmacro recursive () '(recursive)) .. _symbol-macros: Symbol Macros ------------- .. code-block:: (define-symbol-macro macro-name value) (define-symbol-macro macro-name value public-or-private) The second kind of macro is a symbol macro. Symbol macros work like functional macros except that they occupy the value namespace, not the function namespace. When a symbol macro is declared, it binds a name in the value namespace of the current module. Note that symbol macros cannot take arguments. The same caveats with regards to definedness apply for symbol macros: all names referenced (directly or indirectly) by a symbol macro must be fully available at compile-time, when the symbol macro is first defined. Macro expansion for symbol macros occurs when a symbol literal ``foo`` is evaluated in declaration or expression context. In this case, before compiling the symbol literally, GDLisp checks whether a symbol macro with the given name exists in the current scope. If it does, then that symbol macro's body is evaluated in a new lexical scope, and the return value of the symbol macro is used in place of the symbol literal. Like functional macros, symbol macros can expand recursively into other macro calls. Note that the "body" of a symbol macro is a single expression, not a collection of zero or more expressions. Symbol macros are designed to have the same syntax as ``defconst``. To write a symbol macro whose body consists of multiple statements, wrap the body in the ``progn`` special form. Technical Limitations --------------------- Macros can reference other GDLisp source files freely. However, due to technical limitations, macros cannot currently interface directly with GDScript source files or other resource types (such as packed scenes or textures). This limitation may be lifted in the future. Additionally, care must be taken if files are dynamically loaded via the ``load`` function. GDLisp performs name mangling during macro expansion in order to consistently load macros into the runtime. The GDLisp compiler understands ``use`` directives and ``preload`` calls (both of which must refer to a statically-known filename) and will translate these names accordingly, but GDLisp will not attempt to translate the argument to a ``load`` function. The built-in macro ``contextual-load`` can be helpful to perform such dynamic loading inside of macros. ================================================ FILE: doc/readthedocs/reference/parser.rst ================================================ .. _parser: The GDLisp Parser ================= The GDLisp language is a Lisp dialect, with a few extensions to make object-oriented programming using Godot types more straightforward. Listed below is a full description of the GDLisp grammar using `Extended Backup-Naur form `_. GDLisp source code is always written in UTF-8. GDLisp supports the full breadth of Unicode code points, so unless otherwise stated, "character" refers to a valid Unicode character. .. code-block:: ebnf prefixed-expr = [ prefix ], expr ; prefix = "'" | "#'" | "`" | "," | ",." ; expr = literal | list-expr | array-expr | dict-expr | vector-expr | nested-name | nested-node-path | self-name | self-node-path ; literal = "#t" | "#f" | integer | float | string | symbol ; list-expr = "(", ")" | "(", prefixed-expr, {prefixed-expr}, ["." prefixed-expr], ")" ; array-expr = "[", {prefixed-expr}, "]" ; dict-expr = "{", {prefixed-expr, prefixed-expr}, "}" ; vector-expr = "V{", prefixed-expr, prefixed-expr, [prefixed-expr], "}" ; nested-name = expr, ":", symbol ; nested-node-path = expr, ":", node-path ; self-name = "@", symbol ; self-node-path = node-path ; integer = ? integer literal ? ; float = ? floating point literal ? ; string = ? string literal ? ; symbol = ? symbol literal ? ; node-path = ? node path literal ? ; GDLisp code, like any other Lisp dialect, is built up of zero or more `S-expressions `_, where an S-expression is defined to be an (optionally prefixed) form of any of the following. * A literal expression * A list * An array * A dictionary * A (2D or 3D) vector * A nested name or node path * A self name or node path Unless otherwise stated, an arbitrary amount of whitespace is allowed between adjacent tokens in the above EBNF grammar. "Whitespace" is, here, defined as any Unicode character with the ``White_Space=yes`` property. Literals -------- .. code-block:: ebnf literal = "#t" | "#f" | integer | float | string | symbol ; integer = ? integer literal ? ; float = ? floating point literal ? ; string = ? string literal ? ; symbol = ? symbol literal ? ; node-path = ? node path literal ? ; Literals in GDLisp are integers, floating point values, strings, symbols, or node path literals. Integer Literals ^^^^^^^^^^^^^^^^ An integer literal consists of an optional sign (``+`` or ``-``) followed by one or more ASCII digits (``0`` to ``9``). The following are valid integer literals: ``0``, ``+56``, ``-9``, ``10000``, ``00900``. Leading zeroes in an integer literal are ignored. Unlike in C, the presence of a leading zero does *not* cause the subsequent number to be interpreted as ASCII. Floating Point Literals ^^^^^^^^^^^^^^^^^^^^^^^ A floating point literal is an expression which matches the following regular expression .. code-block:: text [+-]?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)? and is *not* a valid integer literal. String Literals ^^^^^^^^^^^^^^^ A string literal is a sequence of zero or more characters enclosed in quotation marks ``"``. Inside the quotation marks is a sequence of individual string characters, where each individual character is one of * Any Unicode character other than a backslash ``\`` or a quotation mark ``"``. * A valid escape sequence beginning in a backslash ``\``. Escape Sequences """""""""""""""" An escape sequence in GDLisp begins with a backslash and consists of one or more characters indicating what character the sequence should be translated to in the resulting code. GDLisp supports most of the commonly-used escape sequences found in C-style languages. * ``\n`` translates to a newline (``0x0A``) * ``\t`` translates to a horizontal tab (``0x09``) * ``\r`` translates to a carriage return (``0x0D``) * ``\a`` translates to the 'alert' character (``0x07``) * ``\b`` translates to the backspace character (``0x08``) * ``\f`` translates to a form feed (``0x0C``) * ``\v`` translates to a vertical tab (``0x0B``) * ``\"`` translates to a literal quotation mark (``0x22``) * ``\'`` translates to a literal apostrophe (``0x27``) * ``\\`` translates to a literal backslash (``0x5C``) Finally, a backslash followed by a ``u`` is a Unicode literal. Unicode literals can be represented in two forms: basic and extended. A basic Unicode literal consists of ``\u`` followed by exactly four valid hexadecimal characters. The four characters are interpreted as a number in base 16 and must point to a valid Unicode code point. Note that there are characters outside of the basic multilingual plane (such as emoji) that cannot be represented in this way. For such characters, the extended form is provided. An extended Unicode literal consists of ``\u`` followed by a curly-brace-enclosed list of at least one hexadecimal character. The characters in the list are interpreted as a number in base 16 and must point to a valid Unicode code point. A backslash followed by any other character in a string literal is an error. Symbol Literals ^^^^^^^^^^^^^^^ Symbols are the cornerstone of a Lisp program and are used as variable and function names. These are, generally speaking, the valid identifiers in a Lisp program. .. code-block:: ebnf symbol = starting-char, { following-char }, { qualifier } ; qualifier = ".", following-char, { following-char } ; A symbol consists of a starting character, followed by zero or more following characters, then subsequently followed by zero or more qualifiers. A qualifier consists of a dot followed by one or more following characters. The starting character of a symbol literal can be any of the following. * An ASCII letter * Any of the following: ``_~+=-\/!%^&*<>?`` * Any non-ASCII character which falls into the Unicode categories L, Mn, Nl, No, S, Pc, Pd, or Po. A "following" character can be any starting character, a valid ASCII number, or any non-ASCII character in the Unicode category N. The following are examples of valid identifiers in GDLisp: ``foo``, ``bar``, ``satisfies?``, ``set-element``, ``list/map``, ``com.mercerenies.gdlisp``. .. Note:: Unlike in some Lisp dialects, symbols in GDLisp are **case sensitive**. That is, ``foo`` and ``Foo`` are distinct symbols that refer to distinct variables or functions. Node Path Literals ^^^^^^^^^^^^^^^^^^ A node path literal is the primary means of accessing nodes in the scene tree whose names are known at compile-time. A node path literal consists of a dollar sign ``$`` followed by either a quoted string literal or a sequence of one or more of the following: * An ASCII letter or number * Any of the following: ``_~+=-\/!$%^&*<>?`` Note that only ASCII characters are allowed in the non-quoted node path form. To include Unicode characters in a node path, it is necessary to quote the path. Lists ----- .. code-block:: ebnf list-expr = "(", ")" | "(", prefixed-expr, {prefixed-expr}, ["." prefixed-expr], ")" ; In GDLisp, the fundamental unit of composition is a *cons cell*, sometimes called a *pair*. A cons cell consists of two elements, conventionally referred to as the *car* and the *cdr*, separated by a dot and enclosed in parentheses. .. code-block:: text (a . b) By convention, lists are built up as a singly-linked list, using cons cells as the links. The car of each is the first element, or "head", of the list, and the cdr of each is the rest of the list, or "tail". The end of the list is denoted with the special "null" atom, indicating by a pair of parentheses with nothing in between. This convention is so widely used in Lisp programs that the syntax supports it directly. That is, a sequence of of one or more S-expressions, followed by the dotted end of the list, is interpreted as a list whose final cdr is the rightmost term. Concretely, the following are equivalent. .. code-block:: text (a b c . d) (a . (b . (c . d))) Similarly, if the dotted terminator is left off, it is assumed to be the special ``()`` null object, so the following are equivalent. .. code-block:: text (a b c d) (a . (b . (c . d . ()))) An S-expression can be viewed as a *dotted list*, consisting of a leading list of values (in the ``car`` portion of cons cells) terminated by an arbitrary non-cons value as the final ``cdr``. For instance, the S-expression ``(a b c . d)`` (or, equivalently, ``(a . (b . (c . d)))`` would be viewed as a dotted list consisting of the elements ``a``, ``b``, and ``c``, terminated by ``d``. We call a dotted list which is terminated by the ``()`` null object a *proper list*. All lists appearing unquoted in GDLisp source code must be proper lists. Arrays ------ .. code-block:: ebnf array-expr = "[", {prefixed-expr}, "]" ; Like in GDScript, a GDLisp array is a general-purpose random-access data structure. Array literals are written as a proper list whose first element is the symbol ``array``. For convenience, zero or more expressions wrapped in square brackets will desugar to this syntax. Note that, unlike in GDScript, elements in an array literal are *not* separated by commas. Examples: .. code-block:: text [] ==> (array) [1 2 3 4] ==> (array 1 2 3 4) Dictionaries ------------ .. code-block:: ebnf dict-expr = "{", {prefixed-expr, prefixed-expr}, "}" ; A dictionary expression is a collection of an even number of expressions, enclosed in curly braces. Like array literals, dictionary literals are not a new form of syntax but are instead mere sugar for something that can be expressed with only basic S-expressions. The first element of each pair of expressions is a key and the second is a value in the resulting dictionary. It is an error to have a brace-enclosed collection of an *odd* number of expressions. A dictionary literal desugars to a proper list whose first element is the symbol ``dict``. Examples: .. code-block:: text {} ==> (dict) {1 2 3 4} ==> (dict 1 2 3 4) Vectors ------- .. code-block:: ebnf vector-expr = "V{", prefixed-expr, prefixed-expr, [prefixed-expr], "}" ; Vectors are used in Godot to represent objects in 2D or 3D space. A vector in GDLisp is constructed using the ``vector`` built-in function. Since vectors are so ubiquitous, the syntax ``V{ ... }`` is provided, which desugars to a call to the ``vector`` function with the given arguments. Nested Names ------------ .. code-block:: ebnf nested-name = expr, ":", symbol ; nested-node-path = expr, ":", node-path ; self-name = "@", symbol ; self-node-path = node-path ; Godot is built on an single-inheritance, messaging-passing object-oriented paradigm. This means that it's very common to call a function *on* an object, not just an independent function that exists in the abstract. In GDLisp, the equivalent to the GDScript "dot" operator is the ``access-slot`` built-in special form. That is, ``foo.bar`` in GDScript would be translated to the GDLisp ``(access-slot foo bar)``. This is relatively awkward and verbose to write all the time, so several shortcuts are provided. These are purely syntax sugar and are *not* new data structures in the abstract syntax tree of the language. The following translations are made. 1. ``foo:bar``, with a literal colon in the middle, is translated to ``(access-slot foo bar)``. The left-hand side can be any (non-prefixed) expression, and the right-hand side must be a symbol literal. Note that a prefix in front of the left-hand side will be parsed at a lower precedence than the ``:``, so ``'a:b`` will be parsed as ``(quote (access-slot a b))``, not ``(access-slot (quote a) b)``. If the precedence is not to your liking, then you can always write out the S-expressions by hand rather than using the syntax sugar, and sometimes this is necessary in complex macro expansions. 2. ``foo:$bar`` translates to a function call equivalent to ``(foo:get-node "bar")``. Written in full generality, this is ``((access-slot foo get-node) "bar")``. Internally, the *actual* expression calls a GDLisp built-in function that allows for better optimization potential, but the effect is the same as long as your custom nodes do not override ``get-node``. The right-hand side must be a node path literal, either quoted or unquoted. 3. ``@bar`` translates to ``(access-slot self bar)``, or equivalently ``self:bar``. 4. A node path literal on its own ``$bar`` translates to a function call equivalent to ``((access-slot self get-node) "bar")``. As with the nested form, ``@$bar`` does not *literally* translate to the latter, instead factoring through a GDLisp middleman for optimization purposes. Prefixes -------- .. code-block:: ebnf prefixed-expr = [ prefix ], expr ; prefix = "'" | "#'" | "`" | "," | ",." ; Expressions, in general, can have a single prefix applied to them. Each prefix is mere syntax sugar for some slightly more complicated S-expression form. The semantics of these forms are explained in future sections, but the translations are defined here. * ``'foo`` translates to ``(quote foo)`` * ``#'foo`` translates to ``(function foo)`` * ```foo`` translates to ``(quasiquote foo)`` * ``,foo`` translates to ``(unquote foo)`` * ``,.foo`` translates to ``(unquote-spliced foo)`` Comments -------- There are two types of comments in GDLisp. Both are ignored entirely by the implementation and will *not* be present in the compiled GDScript code. Line comments begin with a semicolon ``;`` and continue until the next carriage return or linefeed character, or until the end of the file. Conventionally, line comments which occupy the *entire* line will be written with two semicolons, so it's common to see a pair of semicolons denote a line comment. But this is merely a convention and is, as far as the GDLisp parser is concerned, an irrelevant distinction. Block comments begin with ``#|`` and continue until the next ``|#``. Note that block comments cannot be nested, so additional leading sequences of ``#|`` inside a block comment will be ignored, not paired off against matching delimiters. ================================================ FILE: doc/readthedocs/reference/quoting.rst ================================================ .. _quoting: Quoting and Quasiquoting ======================== Quoting an S-expression delays its evaluation. That is, normally in GDLisp, a symbol or proper list that appears in expression context will have special semantics, whether that involves expanding macros, applying functions, accessing local variables, or any number of other rules. However, if you actually intend to explicitly construct a list or symbol as-is and have access to that data structure as a runtime value, you must quote the S-expression. This is done by calling the special form ``quote`` with one argument, as ``(quote some-s-expression)``. This can be abbreviated to an apostrophe followed by the expression: ``'some-s-expression``. All literals evaluate to themselves when quoted. This includes Booleans (``#t`` and ``#f``), integers, floats, strings, symbols, and the null object. Aside from symbols (when are treated specially when quoted), the quote operator is a no-op on literals. That is, the following are all equivalent when evaluated. :: '1 == 1 '1.0 == 1.0 '#t == #t '#f == #f '() == () '"abc" == "abc" A list evaluates, likewise, to the list itself when quoted. So while ``(foo bar)`` is a function call in expression context, ``'(foo bar)`` (or equivalently ``(quote (foo bar))``) is a proper list at runtime, whose first element is the symbol ``foo`` and whose second is the symbol ``bar``. Note that this also highlights another property of quoting, namely that quoting an expression is contagious on the inner expressions. Since the outer list in ``'(foo bar)`` is quoted, the names ``foo`` and ``bar`` are never evaluated as variable names either. You may also use dotted list notation to construct lists with non-``()`` endings. For instance, ``'(1 2 . 3)`` will return a runtime cons object whose car is ``1``. The cdr of this object is another cons object whose car is ``2`` and whose cdr is ``3``. Quasiquoting ------------ Often, fully quoting with ``quote`` is sufficient. However, there are often situations, especially during macro expansion, where an S-expression should be interpreted mostly literally but with a few values interjected at runtime. This is where quasiquoting comes in. An S-expression is quasiquoted with the ``quasiquote`` special form, abbreviated with a single prefix backtick `````. ``quasiquote`` behaves like ``quote`` except that it interprets the forms ``unquote`` and ``unquote-spliced`` in a special way. An ``unquote`` form (or, equivalently, a prefix ``,``) inside of a ``quasiquote`` effectively reverses the ``quasiquote``. The expression inside of the ``unquote`` is evaluated and *interpolated* into the list or expression being constructed by ``quasiquote``. An ``unquote-spliced`` form (equivalently, a prefix ``,.``) works like an ``unquote``, except that it can only be used inside a *list* within a ``quasiquote`` and will flatten itself inside the enclosing list. Consider the following examples. :: `(1 2 ,(list 3 4) 5) This evaluates to ``(1 2 (3 4) 5)``. The ``unquote`` (written with a comma) causes the inner expression to be evaluated using the usual GDLisp rules, which produces a list that is then inserted, as a single element, into the surrounding list. On the other hand, :: `(1 2 ,.(list 3 4) 5) This evaluates to ``(1 2 3 4 5)``, because ``unquote-spliced`` (written as ``,.``) flattens the result of evaluating the inner expression into the outer list. The expression within an ``unquote-spliced`` can evaluate to a list or a Godot array. If it evaluates to any other datatype, then a runtime error will be issued when the offending expression is interpolated. Nested Quasiquotes ^^^^^^^^^^^^^^^^^^ .. Warning:: Nested quasiquotes are an experimental feature, whose behavior may change in a future version of GDLisp. Use with caution. Quasiquotes can be nested. If a ``quasiquote`` appears inside of another ``quasiquote``, then it effectively cancels off with one ``unquote`` or ``unquote-spliced`` on the inner list. That is, :: ``,a This expression will not evaluate ``a`` as an expression. It will return the constant list ``(quasiquote (unquote a))``. On the other hand, :: ``,,a This *will* evaluate the inner ``unquote`` but not the outer one, so if ``a`` has value ``1``, then this will return ``(quasiquote (unquote 1))``. ================================================ FILE: doc/readthedocs/reference/source-file.rst ================================================ Structure of a GDLisp Source File ================================= A GDLisp source file is, by convention, written with a ``.lisp`` file extension. GDLisp source files will be compiled into GDScript source files (``.gd``) in a one-to-one fashion, with the resulting compiled GDScript file taking the same filename as the input file, merely with the extension changed. So, for example, ``Player.lisp`` would be compiled to ``Player.gd``. GDLisp source files are *always* encoded in UTF-8. A GDLisp source file defines a module. Syntactically, a GDLisp source file consists of zero or more S-expressions. With the exception of quoted and quasiquoted terms, all lists appearing in GDLisp source code must be *proper lists*. These top-level S-expressions will be interpreted as declarations and will be compiled together to form a top-level GDScript class body. There are six types of declarations: ``defn``, ``defmacro``, ``defconst``, ``defclass``, ``defenum``, and ``define-symbol-macro``. Additionally, there is one pseudo-declaration, called ``progn``, which is treated specially in this context. Finally, ``use`` directives appear in declaration context, despite not being declarations themselves. Macros are discussed in :ref:`macros`. Classes are discussed in :ref:`classes`. ``use`` directives are discussed in :ref:`imports`. The other declaration forms are discussed below. Namespaces ---------- All GDLisp declarations fall into one of three *namespaces*. The namespaces are intentionally kept distinct, and it is possible to define the same name in all three namespaces in a given scope. * The value namespace consists of variables and constants and their values. The value of a variable or constant can be a first-class function object (reified as a value), as well as any other type of object in GDLisp. Names in the value namespace are declared at the top-level by ``defconst``, ``defclass``, ``defenum``, and ``define-symbol-macro``. Names can be *locally* introduced to the value namespace through several special forms, most prominently ``let``. * The function namespace consists of functions that have been explicitly bound to a function name. Functions are called by simply indicating the name of the function as the head of an S-expression in the source code. A name in the function namespace is *always* bound to a value of function type or to a macro. At the top-level, names in the function namespace are declared by ``defn`` and ``defmacro``. Names can be *locally* introduced to the function namespace through several special forms, such as ``flet`` and ``labels``. * The final namespace is the signal namespace. This namespace only exists inside the scope of a class declaration, never at the top-level of a module. The signal namespace is the namespace which contains signals *within* a class defined by ``defsignal``. Classes are discussed later. Functions --------- .. code-block:: (defn function-name (args ...) body ...) (defn function-name (args ...) public-or-private body ...) A function is the fundamental unit of code execution in GDLisp. A function defined at the top-level of a module compiles to a static function in the resulting GDScript file. A function declaration introduces a name into the function namespace for the current module. When the function is called, the arguments will be bound to the formal arguments of the function in a new lexical scope, and the body of the function will be executed, in order, in that lexical scope. The final expression of the function body is always returned from the function, but it is also possible to return early using the ``return`` special form. If a function body is empty, then the function is a no-op which silently returns the null object ``()``. The list of formal arguments is an :ref:`ordinary lambda list `. When a function is called, the number of arguments passed to the function is validated at compile-time, and an error is issued if the count is incompatible with the function's lambda list. Compilation of Functions ^^^^^^^^^^^^^^^^^^^^^^^^ A GDLisp function translates to a GDScript function with its name :ref:`normalized `. No guarantees are currently made about the GDScript signature of the resulting function if optional or variable arguments are used. However, if the function only accepts *required* arguments, then the resulting function is guaranteed to compile to a function that accepts exactly the same number of required arguments. Therefore, if you intend to call a GDLisp function from GDScript, then the GDLisp function should take only required arguments, in order to maximize compatibility. .. _constants: Constants --------- .. code-block:: (defconst constant-name constant-value) (defconst constant-name constant-value public-or-private) A constant is a top-level immutable name which binds to a value. The value of a constant must be an expression whose value is known at compile time. The notion of a *constant expression* is recursively defined as * The name of another constant. * A literal value. (Due to Godot limitations, it is not currently possible to assign *symbol* literals to constants. This limitation will hopefully be lifted in a future version of GDLisp.) * A ``progn`` block with zero terms, or a ``progn`` block with exactly one term which is a constant expression. * A built-in function call that performs basic arithmetic, such as ``+`` or ``abs``, which is called with only constant expressions as arguments. * An array or dictionary literal consisting of only constant expressions as elements. * A field access ``foo:bar``, where ``foo`` is the name of an enum and ``bar`` is a valid option for that enum. * A call to the ``preload`` special form. A constant declaration defines a name in the value namespace for the current module. Enumerations ------------ .. code-block:: (defenum enum-name entries ...) (defenum enum-name public-or-private entries ...) An enumeration is a scoped namespace containing a finite number of values, useful for representing a collection of choices. Each entry in the enumeration can be specified either as a symbol literal or as a two-element list, where the first element is the symbol literal and the second is the constant expression indicating the value of the enum entry. Examples:: (defenum PlayerChoice ATTACK DEFEND HEAL PASS) (defenum Color (RED 0) (GREEN 1) (BLUE 2)) An enumeration defines a name in the value namespace. This name behaves like a GDLisp object and has fields defined corresponding to the names indicated in the entries. In the first example above, ``PlayerChoice`` is a value for which ``PlayerChoice:ATTACK``, ``PlayerChoice:DEFEND``, ``PlayerChoice:HEAL``, and ``PlayerChoice:PASS`` are all distinct integer values. In the second example above, ``Color`` is a value, and ``Color:RED`` is the value 0, ``Color:GREEN`` is the value 1, and ``Color:BLUE`` is the value 2. If provided, the value of an enumeration constant must be an integer. .. _progn: The ``progn`` Directive ----------------------- .. code-block:: (progn body ...) ``progn`` is a special sort of directive, in that it can be used as a declaration *or* an expression. In declaration context, it takes zero or more declarations and evaluates them in order in the *current* scope, as though the ``progn`` wasn't even there. A ``progn`` directive is never useful directly in a file in declaration context, since it would be easier and more readable to simply place the declarations at the top-level. However, it is useful in macro expansions, when a macro wishes to define multiple declarations but must evaluate to a *single* declaration S-expression. .. _visibility-modifiers: Visibility Modifiers -------------------- Several module-level declarations take an optional visibility modifier. A visibility modifier, if provided, must either be the symbol ``private`` or the symbol ``public``. If a visibility modifier is not provided, it is always assumed to be ``public``. A name with public visibility can be accessed from anywhere. Any other module is free to import the name and reference, call, or instantiate it at their liberty. A name with private visibility is only directly usable within the current module. The current module can freely use the name, but it is an error to attempt to import the name in *another* module. .. _name-normalization: Name Normalization ------------------ GDLisp is far more lenient than GDScript when it comes to identifiers. In particular, GDLisp allows several non-standard characters such as ``-`` and ``?`` in identifiers, as well as Unicode characters. Additionally, GDLisp does not have a notion of "keywords", and it's perfectly kosher to define a variable called ``if`` or ``while`` (though it may confuse the readers of your code). When the GDLisp compiler translates your code into GDScript, it must convert these identifiers into valid GDScript identifiers. The exact translation rules are an implementation detail that may change in future releases of GDLisp, but some guarantees are made in order to maximize compatibility. * Any name which is a valid GDScript identifier and *not* a GDScript keyword will be left unchanged. So ``foo``, ``foobar``, ``player_health1``, and ``i`` will all be left untouched by the GDLisp compiler. * An ASCII arrow ``->`` in a name will be translated to ``_to_``. This allows a name like ``array->list`` to translate into GDScript as ``array_to_list``. * A dash ``-`` that is *not* part of an ASCII arrow is translated to an underscore ``_``. So a conventional Lisp function like ``create-player`` will have its name translated into the GDScript function ``create_player``. * A question mark ``?`` at the *end* of a name will be translated to ``is_`` at the beginning. For instance, a Lisp predicate called ``positive-number?`` will be translated to the GDScript function ``is_positive_number``. * If the name is a reserved word in GDScript, then an underscore will be prefixed, so ``if`` translates to ``_if`` when used as a variable name. * Any other characters, or a ``?`` that is not at the end of a name, will translate in an implementation-defined way. .. Warning:: It is undefined behavior to define two names in the same scope and namespace that will normalize to the same name under these rules. So, for example, it is undefined behavior to define functions called ``foo-bar`` and ``foo_bar`` in the same scope, since these names will both translate to ``foo_bar``. Reserved Names -------------- While GDLisp allows the programmer to define variables and functions with a very broad set of identifier names, some names are specifically reserved for use by the GDLisp compiler. GDLisp programs should *never* define any names, in any namespace, that conflict with a GDLisp reserved name. The compiler may or may not flag such behavior. * The names ``self``, ``super``, and ``GDLisp`` are reserved. * All names beginning with ``sys/`` (including the forward slash) or ``__gdlisp`` are reserved. Further, names defined in the ``sys/`` namespace are strictly reserved for *internal* use and are an implementation detail of the compiler. GDLisp programmers should never directly invoke such functions or reference such values, and the behavior of those names may change at any time, even in a maintenance release. Order of Definition ------------------- Generally speaking, classes and functions defined in a GDLisp module can reference each other freely and can be defined in any order. However, there is an important exception to this rule, and that is macros. When a macro ``foo`` is defined, whether by ``defmacro``, ``define-symbol-macro``, ``macrolet``, or ``symbol-macrolet``, that macro and all of its dependencies must be *fully* defined. That is, every function that ``foo`` calls and every constant, enum, or class name that ``foo`` references must already be defined in a preloaded file or *earlier* in the current file, and the same must be recursively true of all of the names referenced by the macro ``foo``. This is a very strong constraint which is necessary to allow the macro to be loaded during compilation. ================================================ FILE: doc/readthedocs/reference/standard-library/functions.rst ================================================ Built-in Functions ================== All of the GDLisp global functions are documented here. These functions are available (in the function namespace) at the top-level of every GDLisp file. Note that most of the `GDScript built-in functions `_ are available unmodified in GDLisp as well. All names in GDScript which contain an underscore have their underscores replaced with dashes in GDLisp, so ``get_stack`` translates to ``get-stack`` in GDLisp for instance. The exceptions to this are: * ``assert``, ``preload``, and ``yield`` are not functions and are instead special forms in GDLisp. See :ref:`expr-assert`, :ref:`expr-preload`, and :ref:`expr-yield`, respectively. * ``typeof`` in GDLisp behaves differently. It is documented at :ref:`function-typeof`. * ``convert`` has been wrapped to support GDLisp synthetic types. See :ref:`function-convert`. * ``len`` has been wrapped in GDLisp to support GDLisp lists in addition to strings, arrays, and dictionaries. See :ref:`function-len`. * ``bool`` has been extended to support all types. See :ref:`function-bool`. ``*`` ----- :: (defn * (&rest args) ...) Multiplies all of the arguments (which must be numerical or vectors) together. If given no arguments, returns the integer 1. ``+`` ----- :: (defn + (&rest args) ...) Adds all of the arguments (which must be numerical or vectors) together. If given no arguments, returns the integer 0. To concatenate strings in GDLisp, use the built-in Godot function ``str``, which accepts multiple strings and concatenates them together. ``-`` ----- :: (defn - (x &rest args) ...) Subtracts all of the arguments from the first in order. If given only one argument, returns its negation. That is, ``(- x)`` is equivalent to ``(- 0 x)``. ``/`` ----- :: (defn / (x &rest args) ...) Divides all of the arguments out of the first in order. If given only one argument, returns its reciprocal. That is, ``(/ x)`` is equivalent to ``(/ 0 x)``. ``/=`` ------ :: (defn /= (x &rest args) ...) Returns true if *none* of the arguments are equal to each other. Note that since ``/=`` is non-transitive, this is different than just checking whether *adjacent* arguments are equal. That is, ``(/= 1 2 1)`` is *false* even though no adjacent elements are equal. ``<`` ----- :: (defn < (x &rest args) ...) Returns true if each argument (in order) is less than the following element, according to their natural ordering. ``<=`` ------ :: (defn <= (x &rest args) ...) Returns true if each argument (in order) is less than or equal to the following element, according to their natural ordering. ``=`` ----- :: (defn = (x &rest args) ...) Checks whether each argument is equal to the argument following it. ``>`` ----- :: (defn > (x &rest args) ...) Returns true if each argument (in order) is greater than the following element, according to their natural ordering. ``>=`` ------ :: (defn >= (x &rest args) ...) Returns true if each argument (in order) is greater than or equal to the following element, according to their natural ordering. ``append`` ---------- :: (defn append (&rest args) ...) Appends all of the lists together, returning a list which contains all of the elements of each argument, in the same order they appeared. The resulting list will share structure with the final argument to ``append``. That is, the cons cells leading up to the final argument to ``append`` will *not* be rebuilt. Additionally, the final argument to ``append`` can be an improper list, while all of the other arguments must be proper lists. .. _function-apply: ``apply`` ----------- :: (defn apply (f &rest args) ...) Calls the function object ``f`` with the given arguments, returning the result of the function call. At least one argument must be supplied, and the final argument is treated as a list of arguments, not an individual one. For a version of ``apply`` without the "variable argument" behavior, see :ref:`function-funcall`. The difference between this function and ``funcall`` is that the former treats its final argument as a list and prepends the others onto it. Concretely, all of the following will sum the numbers from one to four, returning ten:: (+ 1 2 3 4) (funcall #'+ 1 2 3 4) (apply #'+ 1 2 3 4 ()) (apply #'+ '(1 2 3 4)) (apply #'+ 1 2 '(3 4)) ``array`` ---------- :: (defn array (&rest xs) ...) Constructs a array of the arguments given. The ``[ ... ]`` syntax desugars to a call to this function. ``array->list`` --------------- :: (defn array->list (arr) ...) Converts a Godot array or pooled array into a proper list. ``array/concat`` ---------------- :: (defn array/concat (&rest arrays) ...) Concatenates all of the arrays in order, producing a new array. The existing arrays are not modified. If given an empty argument list, returns an empty array. .. _function-array-filter: ``array/filter`` ---------------- :: (defn array/filter (p xs) ...) Applies the unary predicate ``p`` to each element of the array ``xs`` and returns a filter of all elements for which the predicate returned truthy. Does not modify ``xs``. .. _function-array-find: ``array/find`` ---------------- :: (defn array/find (p arr &opt default) ...) Applies the unary predicate ``p`` to each element of the array ``arr``. Returns the first element of the array for which the predicate returned true. If no element returns true, then ``default`` is returned. This function will short-circuit and stop calling ``p`` as soon as a match is found. .. _function-array-fold: ``array/fold`` -------------- :: (defn array/fold (f xs &opt x) ...) A left-fold over an array. ``f`` shall be a function of two arguments, ``xs`` shall be a Godot array, and ``x`` (if supplied) shall be a non-null starting value. The array is traversed from the beginning to the end. At each element, the call ``(funcall f acc element)`` is made, where ``acc`` is the value we've accumulated so far and ``element`` is the current element. The return value of that function call is used as the new value of ``acc``. At the end, ``acc`` is returned. The initial value of ``acc`` is ``x`` if supplied. If ``x`` is not supplied, then the initial value is the first element of the array, and iteration begins at the second. If ``x`` is not supplied and the array is empty, then an error is produced. .. _function-array-map: ``array/map`` ------------- :: (defn array/map (f xs) ...) Applies the unary function ``f`` to each element of the array ``xs`` and returns a new array of the returned values. Does not modify ``xs``. ``array/reverse`` ----------------- :: (defn array/reverse (arg) ...) Returns a new array containing all of the same elements as the input array ``arg`` but in reverse order. Does not mutate ``arg``. .. _function-bool: ``bool`` -------- :: (defn bool (x) ...) Normalizes the argument to Boolean. Returns ``#t`` if ``x`` is truthy, or ``#f`` otherwise. ``cons`` -------- :: (defn cons (a b) ...) Constructs a fresh cons cell, with the first argument as car and the second as cdr. ``connect>>`` ------------- :: (defn connect>> (obj signal-name function) ...) Connects the signal on ``obj`` whose name is the string ``signal-name`` to the given function. This function is similar to the built-in method ``connect`` on Godot objects, but the former is designed to work on GDLisp first-class functions, such as those constructed with :ref:`expr-lambda` or :ref:`expr-function`. This function returns a unique identifier which can be passed to :ref:`function-disconnect` to disconnect the signal. .. Warning:: Due to current limitations in the implementation, signals connected in this way can only accept up to six arguments. If you need more than six arguments, use the built-in Godot method ``Object.connect`` instead. ``connect1>>`` -------------- :: (defn connect1>> (obj signal-name function) ...) Connects the signal on ``obj`` whose name is the string ``signal-name`` to the given function. The connection will be dropped as soon as it's been called once. Returns an identifier that can be passed to :ref:`function-disconnect` to disconnect the signal. .. Warning:: Like ``connect>>``, functions connected with this function can only accept up to six arguments. .. _function-convert: ``convert`` ----------- :: (defn convert (what type) ...) Converts the value ``what`` into the type ``type``, according to the Godot rules for the GDScript function of the same name. ``type`` can be a primitive type object (such as ``Int``) or a type enumeration constant (such as ``Type:INT``). ``dict`` ---------- :: (defn dict (&rest xs) ...) Constructs a dictionary, where each even-indexed argument is a key paired with the value immediately after it. of the arguments given. The ``{ ... }`` syntax desugars to a call to this function. The behavior is undefined if this function is called with an odd number of arguments. ``dict/elt`` ------------ :: (defn dict/elt (dict k) ...) Gets the value corresponding to key ``k`` of the dictionary ``dict``. ``dict/find`` ------------- :: (defn dict/find (p dict &opt default) ...) Iterates over the dictionary ``dict`` in element insertion order. For each key-value pair of the dictionary, the predicate ``p`` is called with two arguments: the key and the value. Returns the first *key* for which the predicate returned true, or ``default`` if no match is found. This function will short-circuit and stop calling ``p`` as soon as a match is found. .. _function-disconnect: ``disconnect>>`` ---------------- :: (defn disconnect>> (obj signal-name index) ...) Disconnects a signal that was connected using ``connect>>`` or ``connect1>>``. The index shall be the return value of the function that was used to make the connection. Note that signals connected with Godot's built-in ``connect`` method should be disconnected, correspondingly, with the ``Object.disconnect`` method. This function is only designed to disconnect signals made using the GDLisp functions ``connect>>`` and ``connect1>>``. ``equal?`` ---------- :: (defn equal? (x &rest args) Returns whether each value is equal to the one after it, following data structures recursively. This function is similar to the built-in Godot function ``deep_equal``, but ``equal?`` works on more data types. Specifically, ``equal?`` will recursively follow arrays (of any type), dictionaries, and GDLisp lists. ``elt`` ------- :: (defn elt (x y) ...) Gets the element from the data structure. ``(elt x y)`` is guaranteed to compile to the Godot ``x[y]``, hence can be used on arrays, dictionaries, and any other data structure that the subscript operator is compatible with. .. _function-funcall: ``funcall`` ----------- :: (defn funcall (f &rest args) ...) Calls the function object ``f`` with the given arguments, returning the result of the function call. Note that, since function objects do not know their own function signature at compile-time, the number of arguments cannot be validated at compile time when using ``funcall``. An error will be issued at runtime, if the argument count does not match up. Example:: (let ((my-function (lambda (x y) (+ x y)))) (funcall my-function 10 15)) ; Returns 25 For a version of ``funcall`` that takes a list of arguments rather than individual arguments, see :ref:`function-apply`. ``gcd`` ------- :: (defn gcd (&rest args) ...) Returns the greatest common divisor of the arguments, or 0 if no arguments are given. ``gensym`` ---------- :: (defn gensym (&opt prefix) ...) Returns a new symbol object whose name has not appeared in the GDLisp source code that the compiler has encountered up to this point and that has not been interned dynamically with ``intern``. If ``prefix`` is provided, then it must be a string and the returned symbol will have a name that begins with ``prefix``. If not, then a default prefix will be chosen for you. ``init`` -------- :: (defn init (a) ...) Returns a new list which consists of all of the elements of the original list ``a`` *except* the final one. ``a`` can be a proper or improper list, and in either case the terminator is ignored (it is not considered the final element, even if it is nontrivial), and the resulting list shall be proper. ``a`` must be nonempty. ``instance?`` ------------- :: (defn instance? (value type) ...) Given a value and a type object, returns whether or not that value is an instance of that type. The type object can be any of the following. * A GDScript or GDLisp class. * The name of a built-in Godot object type, such as ``Node`` or ``Spatial``. * A Godot primitive type, such as ``Int``. * A GDLisp abstract type, such as ``BaseArray`` or ``AnyVal``. ``intern`` ---------- :: (defn intern (s) ...) Returns a symbol object whose name is ``s``. If the given symbol already exists in the global symbol table, then the *same* object (up to ``=`` comparison) is returned. Otherwise, a new symbol object is created and returned. ``last`` -------- :: (defn last (a) ...) Returns the final element of ``a``, which must be a nonempty list (proper or improper). The terminator of an improper list is ignored (it is not considered the final element, even if it is nontrivial). ``lcm`` ------- :: (defn lcm (&rest args) ...) Returns the least common multiple of the arguments, or 1 if no arguments are given. .. _function-len: ``len`` ------- :: (defn len (x) ...) Returns the length of the list, array, dictionary, or string object. ``list`` -------- :: (defn list (&rest args) ...) Returns a proper list containing all of the arguments, in the same order. ``list->array`` --------------- :: (defn list->array (list) ...) Converts a proper list into a Godot array. ``list/elt`` ------------ :: (defn list/elt (list n) ...) Given a list and an index, returns the element of the list at that 0-indexed position. Produces an error if the index is out of bounds. ``list/copy`` ------------- :: (defn list/copy (list) ...) Returns a new list containing the same elements as ``list`` in the same order. The two lists will not share any structure. ``list/filter`` --------------- :: (defn list/filter (p xs) ...) Applies the unary predicate ``p`` to each element of ``xs`` and returns a list of all elements for which the predicate returned truthy. The returned list shares no structure with the argument list. ``list/find`` ---------------- :: (defn list/find (p xs &opt default) ...) Applies the unary predicate ``p`` to each element of the proper list ``xs``. Returns the first element of the list for which the predicate returned true. If no element returns true, then ``default`` is returned. This function will short-circuit and stop calling ``p`` as soon as a match is found. ``list/fold`` ------------- :: (defn list/fold (f xs &opt x) ...) A left-fold over a list. ``f`` shall be a function of two arguments, ``xs`` shall be a proper list, and ``x`` (if supplied) shall be a non-null starting value. The list is traversed from the beginning to the end. At each list element, the call ``(funcall f acc element)`` is made, where ``acc`` is the value we've accumulated so far and ``element`` is the current element. The return value of that function call is used as the new value of ``acc``. At the end, ``acc`` is returned. The initial value of ``acc`` is ``x`` if supplied. If ``x`` is not supplied, then the initial value is the first element of the list, and iteration begins at the second. If ``x`` is not supplied and the list is empty, then an error is produced. ``list/map`` ------------ :: (defn list/map (f xs) ...) Applies the unary function ``f`` to each element of the list ``xs`` and returns a new list of the result values. ``list/reverse`` ---------------- :: (defn list/reverse (arg) ...) Returns a list containing all of the same elements as ``arg`` but in reverse order. Does not mutate ``arg``. ``list/tail`` ------------- :: (defn list/tail (list k) ...) Returns the ``k``\ th cdr of ``list``. That is, returns a tail of the list with the first ``k`` elements removed. The resulting list shares structure with ``list``. ``max`` ------- :: (defn max (&rest args) ...) Returns the maximum of all of the arguments given, or ``- INF`` if given no arguments. ``member`` ---------- :: (defn member? (value structure) ...) Returns whether or not the value is a member of the structure. This is guaranteed to compile to the Godot ``in`` infix operator and has the same semantics. ``min`` ------- :: (defn min (&rest args) ...) Returns the minimum of all of the arguments given, or ``INF`` if given no arguments. ``mod`` ------- :: (defn mod (x y) ...) Returns the (integer) modulo of ``x`` by ``y``. As in Godot, to take a floating-point modulo, use the built-in GDScript function ``fmod``. ``NodePath`` ------------ :: (defn NodePath (string) ...) Constructs a ``NodePath`` object containing the given string. ``not`` ------- :: (defn not (x) ...) Returns a proper Boolean (i.e. either ``#t`` or ``#f``) of the *opposite* truthiness to ``x``. So if ``x`` is truthy then this function returns ``#f``, and if ``x`` is falsy then this function returns ``#t``. ``set-dict/elt`` ---------------- :: (defn set-dict/elt (x dict k) ...) Sets the value corresponding to key ``k`` of the dictionary ``dict`` to ``x``. If the key does not exist in the dictionary, then it is inserted. ``set-elt`` ----------- :: (defn set-elt (x y z) ...) Sets the element from the data structure. ``(set-elt x y z)`` is guaranteed to compile to the Godot ``y[z] = x``, hence can be used on arrays, dictionaries, and any other data structure that the subscript operator is compatible with. ``snoc`` -------- :: (defn snoc (a b) ...) Appends an element ``b`` to the end of a proper list ``a``. The list ``a`` is not modified, and the returned list is a newly-constructed one. .. _function-typeof: ``typeof`` ---------- :: (defn typeof (value) ...) Given a value, return a type object representing its type. Note that this is **not** the same as the GDScript ``typeof`` function, which returns an integer constant. The object returned by the GDLisp ``typeof`` function is an object representing the value's most specific type, which can be passed to ``instance?`` for instance checks. * In the case of a primitive value (i.e. an instance of a subtype of ``AnyVal``), a special type object is returned. This type object can only be passed to ``instance?`` and does not have any public fields or methods. * In the case of an object whose type is defined in GDLisp or GDScript, the script resource or inner class resource is returned. * In the case of a direct instance of a Godot native type like ``Node``, the relevant native type object is returned. ``vector`` ---------- :: (defn vector (x y &opt z) ...) Constructs a vector of two or three dimensions with the given arguments. The ``V{ ... }`` syntax desugars to calls to this function. ``vector/map`` -------------- :: (defn vector/map (f arg &rest args) ...) Applies a function to each component of the 2D or 3D vectors. In its simplest form (with two arguments), ``vector/map`` produces a new vector of the same dimension as the input, where ``f`` has been applied to the X and Y (and Z, if 3D) coordinates of the input vector. If given more than two arguments, the arguments must all be of the dimension. ``f`` will be applied to the X components of *all* input vectors at once, then to the Y components, then to the Z components (if needed), producing a single new vector. Example:: (vector/map #'+ V{1 2} V{100 200}) ; Returns V{101 202} ================================================ FILE: doc/readthedocs/reference/standard-library/index.rst ================================================ The GDLisp Standard Library =========================== The GDLisp standard library is provided at runtime by the ``GDLisp.lisp`` support file. See :ref:`support-file` for details on how to ensure that your project can access this file at runtime. All names defined in the GDLisp standard library are available at the global scope in every GDLisp source file. .. toctree:: :maxdepth: 1 :caption: Contents: values.rst functions.rst macros.rst types.rst ================================================ FILE: doc/readthedocs/reference/standard-library/macros.rst ================================================ Built-in Macros =============== All of the global macros available in GDLisp are documented here. ``->`` ------ :: (defmacro -> (arg &rest forms) ...) First-argument threading. Evaluates ``arg``. Then, for each form in ``forms``, passes the accumulated argument as the first argument to the given list (shifting the other arguments over). Any argument which is a symbol is treated as a one-element list. Examples:: (-> a b c) ; Expands to (c (b a)) (-> a (b 1) c (d 2 3)) ; Expands to (d (c (b a 1)) 2 3) ``->>`` ------- :: (defmacro ->> (arg &rest forms) ...) Last-argument threading. Evaluates ``arg``. Then, for each form in ``forms``, passes the accumulated argument as the last argument to the given list. Any argument which is a symbol is treated as a one-element list. Examples:: (->> a b c) ; Expands to (c (b a)) (->> a (b 1) c (d 2 3)) ; Expands to (d 2 3 (c (b 1 a))) ``and`` ------- :: (defmacro and (&rest args) ...) Short-circuiting conjunction operator. Returns the first falsy value out of its arguments, evaluating as few arguments as necessary to do so. Returns ``#t`` if given no arguments. ``as->`` -------- :: (defmacro as-> (arg var &rest forms) ...) Arbitrary threading macro. Evaluates ``arg``. Then evaluates the first of ``forms`` with ``var`` bound to ``arg``. The next form in ``forms`` is evaluated with the result of the prior bound to ``var``, and so on. The result of the final form is returned. Example:: ;; Equivalent: (as-> (foo) v (bar 2 v 3) (baz 1 v 4 5)) (baz 1 (bar 2 (foo) 3) 4 5) ``contextual-load`` ------------------- :: (defmacro contextual-load (filename) ...) Expands to a ``load`` call to the file with the name ``filename``. However, unlike a plain ``load`` call, ``contextual-load`` will be understood during macro expansion and translated to an appropriate virtual filename. Thus, ``load`` should not be used directly in macro bodies, as it will fail when run during macro expansion. .. Caution:: While the GDLisp macro engine understands how to translate names into virtual filenames inside a ``contextual-load``, it will NOT attempt to trace dependencies into the corresponding file. So ``contextual-load`` is *only* safe to use on a filename which has already been loaded in the current scope. For instance, ``(contextual-load (this-true-filename))`` is always safe, since the current file is always (trivially) loaded. ``deflazy`` ----------- :: (defmacro deflazy (name value &rest modifiers) ...) ``deflazy`` defines the name ``name`` in the value namespace of the current scope. That name, when it is evaluated for the *first* time, will evaluate ``value`` and return it. The value will then be cached and returned when ``name`` is evaluated in the future. The only valid modifiers are ``public`` and ``private``, which set the visibility of the defined name. New modifiers may be added in the future. ``defobject`` ------------- :: (defmacro defobject (name parent &opt visibility &rest body) ...) Defines a new subclass of ``parent``, whose visibility is ``visibility`` (or public, if not provided) and whose class body is ``body``. This subclass only has one instance, which is lazily initialized to the name ``name``. The first time ``name`` is evaluated, the instance is constructed and returned. On future evaluations of ``name``, the same instance will be returned. .. _macro-defvars: ``defvars`` ----------- :: (defmacro defvars (&rest args) ...) ``defvars`` expands into multiple ``defvar`` blocks with no initializers. That is, ``(defvars a b c)`` is equivalent to :: (defvar a) (defvar b) (defvar c) There is no way to include initializers or ``onready`` modifiers with this macro. For such use cases, call ``defvar`` directly. .. _macro-if: ``if`` ------ :: (defmacro if (cond true-case &opt false-case) ...) Evaluates ``cond``. If it's true, then evalautes and returns ``true-case``. If ``cond`` is false, then evaluates and returns ``false-case``. If not provided, ``false-case`` defaults to ``()``. .. _macro-let-star: ``let*`` -------- :: (defmacro let* (vars &rest body) ...) ``let*`` is equivalent to ``let`` except that each variable clause in a ``let*`` is evaluated in sequence and has access to the variables declared before it in the same ``let*`` block. That is, :: (let* ((a 1) (b (+ a 1))) b) is equivalent to :: (let ((a 1)) (let ((b (+ a 1))) b)) and will return ``2``. Attempting to do the same with a single ``let`` block will result in the ``a`` variable not being in scope during initialization of ``b``. ``list/for`` ------------ :: (defmacro list/for (var list &rest body) ...) Equivalent to the ``for`` special form, but works on lists rather than arrays. ``var`` is a symbol name and ``list`` is an expression that evaluates to a list. ``list`` is evaluated, and then ``body`` is run once per list element in a local scope where ``var`` is defined to be the current list element. Always returns ``()``. ``or`` ------ :: (defmacro or (&rest args) ...) Short-circuiting disjunction operator. Returns the first truthy value out of its arguments, evaluating as few arguments as necessary to do so. Returns ``#f`` if given no arguments. ``quit`` -------- :: (defmacro quit () ...) Expands to a call to the ``quit`` method on the scene tree. This is most commonly used in the REPL, where ``(quit)`` will quickly and easily exit the REPL. ``this-file`` ------------- :: (defmacro this-file () ...) ``(this-file)`` is an expression which will load the current file. ``this-file`` can be used in macro contexts to dynamically load the current file. Equivalent to ``(load (this-filename))``. ``this-filename`` ----------------- :: (defmacro this-filename () ...) ``(this-filename)`` evaluates to a string consisting of the path to the current file being compiled (as a ``.gd`` file). In macro expansion, ``(this-filename)`` will expand to the virtual filename of the file, suitable to be used *during* macro expansion. ``this-true-filename`` ---------------------- :: (defmacro this-true-filename () ...) ``(this-true-filename)`` evaluates to a string consisting of the path to the current file being compiled (as a ``.gd`` file). In macro expansion, ``(this-true-filename)`` will expand to the *real* runtime filename of the file. This filename is *not* suitable to load during macro expansion but it should be used in situations where a macro is attempting to expand *into* a ``load`` call which will happen at runtime. .. _macro-unless: ``unless`` ---------- :: (defmacro unless (cond &rest body) ...) Shorthand syntax for an ``if`` block which *only* has an ``else`` part. If ``cond`` is false, evaluates and returns the body. If ``cond`` is true, returns ``()``. ``update`` ---------- :: (defmacro update (field updater) ...) Convenient shorthand for updating a field. ``(update x (foo y z))`` expands to ``(set x (foo x y z))``. That is, the value ``field`` is taken and passed as the first argument to the ``updater`` (with the other arguments, if any, shifted one to the right), and then the result is put back into the place ``x``. ``x`` can be anything that is valid on the left-hand side of a ``set``. See :ref:`expr-set` for more details on the valid argument forms. If ``updater`` is a symbol rather than a proper list, then it is wrapped in a one-element list before expanding. Examples:: (update a (+ 1)) ; Adds 1 to the variable a (update b -) ; Sets b equal to its negative (update player:position (* 2)) ; Multiplies the position field on player by 2 (update (elt x 0) (/ 2)) ; Divides the first element of the array x by 2 .. _macro-when: ``when`` -------- :: (defmacro when (cond &rest body) ...) Shorthand syntax for an ``if`` block which has no ``else`` part. If ``cond`` is true, evaluates and returns the body. If ``cond`` is false, returns ``()``. ``yield*`` ---------- :: (defmacro yield* (arg) ...) If ``arg`` is a function call which yields and produces a ``GDScriptFunctionState`` object, then this macro yields the *current* function as well. When the current function is resumed, then so too shall the inner function. When the inner function terminates and returns normally, the result of the inner function is returned from ``yield*``. Effectively, ``yield*`` propagates a ``yield`` from ``arg`` to the caller. Example:: (defn foo () (print 2) (yield) (print 4) (yield) (print 6)) (defn bar () (print 1) (yield* (foo)) (print 7)) (let ((x (bar))) (print 3) (set x (x:resume)) (print 5) (x:resume) (print 8)) This code will print the numbers from 1 to 8 in order. Note that the actual "yielding" is done in ``foo``, but when we resume from our ``let`` block, we resume *through* the ``bar`` function. .. Warning:: ``yield*`` should only be used to yield from functions that used the 0-argument form of ``yield``. If a function uses the 2-argument ``yield`` and then is resumed by a signal, the intermediate ``yield*`` object will be left in an un-resumable state. ================================================ FILE: doc/readthedocs/reference/standard-library/types.rst ================================================ Types and Classes ================= These are all of the class and enumeration constants defined at the top-level in GDLisp. All of `the classes defined by Godot `_ are available in GDLisp. Any such classes not listed below are available without modification. Godot classes that are singletons use the following convention: The singleton object is available in GDLisp as-is, and the type that represents it is available preceded with an underscore. So, for example, ``Engine`` is a global constant in GDLisp that refers to the singleton object whose type is ``_Engine``. Godot Enumerations ------------------ The global enumerations defined in `Enumerations `_ have been included with modifications. The common prefix for each enumeration type has been removed from the name of the constant and replaced with an appropriate typename. So, for example, the name ``KEY_SHIFT``, which refers to the shift key, is referred to in GDLisp as ``Key:SHIFT``. The following name translations take place * ``BUTTON_`` constants belong in the ``Mouse`` enum. * ``CORNER_`` constants belong in the ``Corner`` enum. * ``ERR_`` constants (and the ``OK`` and ``FAILED`` constants) belong in the ``Err`` enum. * ``HALIGN_`` constants belong in the ``HAlign`` enum. * ``JOY_`` constants belong in the ``Joy`` enum. * ``KEY_`` constants belong in the ``Key`` enum. * ``KEY_MASK_`` constants belong in the ``KeyMask`` enum. * ``MARGIN_`` constants belong in the ``Margin`` enum. * ``METHOD_FLAG_`` constants (and the ``METHOD_FLAGS_DEFAULT`` constant) belong in the ``MethodFlag`` enum. * ``MIDI_MESSAGE_`` constants belong in the ``MidiMessage`` enum. * ``OP_`` constants belong in the ``Op`` enum. * ``PROPERTY_HINT_`` constants belong in the ``PropertyHint`` enum. * ``PROPERTY_USAGE_`` constants belong in the ``PropertyUsage`` enum. * ``TYPE_`` constants belong in the ``Type`` enum. * ``VALIGN_`` constants belong in the ``VAlign`` enum. * ``HORIZONTAL`` and ``VERTICAL`` go in their own enum called ``Orientation``. Godot Primitive Types --------------------- The 27 Godot primitive types are also available in GDLisp. In GDLisp, these types are real type objects and can be used as the right-hand side of an ``instance?`` call. Since they are real objects in the GDLisp ecosystem, they can also be assigned to variables and passed as function arguments. Additionally, the names of the GDLisp primitive types are normalized to be consistent with other class names. Specifically, * ``Null`` refers to the Godot type whose constant is ``TYPE_NIL`` and whose only value is the null object ``()``. * ``Bool`` refers to the Godot type ``bool`` whose constant is ``TYPE_BOOL``. * ``Int`` refers to the Godot type ``int`` whose constant is ``TYPE_INT``. * ``Float`` refers to the Godot type ``float`` whose constant is ``TYPE_REAL``. * ``String``, ``Vector2``, ``Rect2``, ``Vector3``, ``Transform2D``, ``Plane``, ``Quat``, ``AABB``, ``Basis``, ``Transform``, ``Color``, ``NodePath``, ``RID``, ``Object``, ``Dictionary``, ``Array``, ``PoolByteArray``, ``PoolIntArray``, ``PoolStringArray``, ``PoolVector2Array``, ``PoolVector3Array``, and ``PoolColorArray`` are available in GDLisp and refer to the Godot primitive types of the same name. ``_GDLisp`` ----------- :: (defclass _GDLisp (Node) ...) The class of the singleton GDLisp support object. The behavior of the program is undefined if an attempt is made to construct any additional instances of this type. ``Any`` ------- :: (defclass Any () ...) The type of all values in GDLisp. Every single value is an value of this type. ``AnyRef`` ---------- :: (defclass AnyRef (Any) ...) The type of all object values in GDLisp. ``AnyVal`` ---------- :: (defclass AnyVal () ...) The type of all primitive values in GDLisp. All values which are not instances of ``AnyRef`` are instances of ``AnyVal``. ``BaseArray`` ------------- :: (defclass BaseArray (AnyVal) ...) The common supertype of all array types in GDLisp, including ``Array`` itself and the seven strictly-typed pool array types. ``Cell`` -------- :: (defclass Cell (Reference) (defvar contents) (defn _init (contents) ...)) The type of simple cells containing one single value. ``Cell`` has a single public mutable field, ``contents``, and a constructor of one argument. See also :ref:`cell-type`. ``ConnectFlags`` ---------------- :: (defenum ConnectFlags (DEFERRED ...) (PERSIST ...) (ONESHOT ...) (REFERENCE_COUNTED ...)) This enumeration makes global the `ConnectFlags enumeration `_ in Godot. ``Cons`` -------- :: (defclass Cons (Reference) (defvar car) (defvar cdr) (defn _init (car cdr) ...) (defn (get caar) () ...) (defn (get cadr) () ...) (defn (get cdar) () ...) (defn (get cddr) () ...) (defn (get caaar) () ...) (defn (get caadr) () ...) (defn (get cadar) () ...) (defn (get caddr) () ...) (defn (get cdaar) () ...) (defn (get cdadr) () ...) (defn (get cddar) () ...) (defn (get cdddr) () ...)) The type of pairs, or cons cells, in GDLisp. Cons cells have two public mutable fields, called ``car`` and ``cdr``. Additionally, getters are defined for various nestings of ``car`` and ``cdr`` up to three layers deep. For instance, ``cons-cell:caddr`` is equivalent to ``cons-cell:car:cdr:cdr``. See also :ref:`cons-cell`. ``Function`` ------------ :: (defclass Function (Reference) ...) The type of GDLisp function objects which have been reified into the value namespace. These are valid arguments to ``funcall`` and ``apply``. ``Nothing`` ----------- :: (defclass Nothing () ...) The bottom of the type hierarchy. There are not, and never will be, values of this type in GDLisp. ``Notification`` ---------------- :: (defenum Notification (POSTINITIALIZE ...) (PREDELETE ...)) An enumeration representing the Godot notification constants `defined on Object `_. ``Number`` ---------- :: (defclass Number (AnyVal) ...) The type of numbers in GDLisp. Integers and floating-point numbers are both instances of this type. ``Symbol`` ---------- :: (defclass Symbol (Reference) ...) The type of symbols at runtime in GDLisp. ================================================ FILE: doc/readthedocs/reference/standard-library/values.rst ================================================ Global Values ============= Constant values defined in the GDLisp value namespace at global scope are documented here. Note that all constants defined in the global scope for use in GDScript are also available (unmodified) in GDLisp. That includes all of the following: * `@GDScript Constants `_ * `@GlobalScope Constants `_ * `@GlobalScope Enumerations `_ Additionally, the constants defined on the root ``Object`` type are all exposed globally in GDLisp. These include: ``CONNECT_DEFERRED``, ``CONNECT_PERSIST``, ``CONNECT_ONESHOT``, ``CONNECT_REFERENCE_COUNTED``, ``NOTIFICATION_POSTINITIALIZE``, and ``NOTIFICATION_PREDELETE``. ``GDLisp`` ---------- :: (defconst GDLisp ...) The top-level constant called ``GDLisp`` refers to the GDLisp node itself. That is, the top-level name ``GDLisp`` can be used to reference the singleton object that represents the ``GDLisp.lisp`` support library. It is generally not necessary to refer to this object directly, since GDLisp does the name resolution for you, but the name is available if needed. ``GODOT-VERSION`` ----------------- :: (defconst GODOT-VERSION ...) This constant is defined to be an integer value representing the Godot version that ``GDLisp.lisp`` was compiled with, as follows .. code-block:: text GODOT-VERSION = 1,000,000 * major-version + 10,000 * minor-version + 100 * patch-version So, for instance, Godot 3.4.1 would be represented by the ``GODOT-VERSION`` constant ``3040100``. These version integers are constructed in such a way that two version integers can be compared using the standard (numerical) comparison operators, to check if the version of Godot is newer or older than some set value. Note that this checks the version of Godot that ``GDLisp.lisp`` was compiled with, not the one being used at runtime. To get the version of Godot that is currently *running*, use the built-in Godot function `get_version_info `_. ``nil`` ------- :: (defconst nil ()) The value ``nil`` is defined to be the special null object ``()``. ================================================ FILE: doc/readthedocs/requirements.txt ================================================ sphinx==5.2.2 sphinx_rtd_theme==1.1.1 sphinxcontrib-mermaid==0.7.1 ================================================ FILE: doc/readthedocs/tutorial/classes.rst ================================================ Classes in GDLisp ================= GDLisp prides itself on being a functional language, capable of manipulating functions as first-class objects and providing powerful abstraction techniques to support common functional paradigms. But GDLisp targets the Godot platform, which is designed with an object-oriented paradigm in mind. GDLisp fully supports this paradigm and provides mechanisms for working with classes, both those built-in to Godot and those from GDScript source files, as well as for declaring new classes in GDLisp proper. Using Classes ------------- Every built-in GDScript class is available in GDLisp as a global name. This includes classes deriving from ``Object``, like ``Node``, ``Spatial``, and ``Control``, as well as typenames that are considered primitive in Godot, such as ``Int`` and ``Vector2``. To access a field on a class or an object, put a colon ``:`` after the class or object name and follow it by the name of the constant or variable. For example:: Vector2:ZERO Transform:FLIP_X Quat:IDENTITY some-timer-node:paused some-material:render_priority Likewise, to call a method on an object or a class, use the colon syntax as the first term of an S-expression. :: (Reference:new) (Engine:get_singleton "MySingleton") (some-node:get_path) (my-object:free) Declaring Classes ----------------- Classes can be declared inside a module using the following syntax. :: (defclass ClassName (ParentClassName) body ...) Note that ``ParentClassName`` must always be a (possibly qualified) class *name*. Unlike GDScript, GDLisp does not allow string paths to be used as the name of a parent class. As with functions and other declarations, ``defclass`` can be suffixed with the symbol ``private`` to make the class invisible outside of the current module. :: (defclass ClassName (ParentClassName) private body ...) Inside the class, several types of declarations are supported. Note that declarations inside a class are always public, as GDLisp cannot enforce access restrictions on class members. Signal Declarations ^^^^^^^^^^^^^^^^^^^ :: (defsignal signal_name (args ...)) The ``defsignal`` form declares the existence of a signal with the given name and argument list. Note that signals are conventionally named in ``snake_case``, not ``train-case``, for maximum compatibility with Godot signals. If the argument list is empty, it can be omitted. The argument list, if provided, must be a :ref:`simple lambda list `. That means that the ``&opt``, ``&rest``, and ``&arr`` directives are not supported. Constant Declarations ^^^^^^^^^^^^^^^^^^^^^ :: (defconst ConstantName value) Constants inside a class work identically to those at the module-level, with the exception that a class-scoped ``defconst`` cannot be marked ``private``. Note that ``defconst`` is evaluated at class scope, so ``self`` does *not* exist in the right-hand side of a ``defconst``. Variable Declarations ^^^^^^^^^^^^^^^^^^^^^ :: (defvar var-name initial-value) Variables declared inside a class function as instance variables. When an instance of the class is initialized, it will receive an instance variable with the given name. ``initial-value`` is optional, but if provided it will be used as the initial value of the instance variable at initialization time. ``initial-value`` can contain arbitrary expressions and can use ``self``. :: (defvar var-name initial-value onready) If the ``defvar`` form is followed by the ``onready`` keyword, then the initial value will be applied during the ``_ready`` method (when the node is added to the tree) instead of the ``_init`` method (when the object is constructed). The ``onready`` modifier makes no sense on objects that are not nodes. .. Note:: If you're wondering where the GDScript ``setget`` keyword is, GDLisp doesn't have ``setget``. GDLisp implements proxy properties in a different way. See :ref:`getters-and-setters` below. Export Modifiers """""""""""""""" A variable declaration in a class can be followed by an ``export`` form, which will be compiled into a GDScript ``export`` clause. This provides certain information to the Godot editor indicating what values are acceptable for the instance variable. Examples:: (defvar player-hp 10 (export int)) (defvar character-name "Alice" (export String "Alice" "Bob" "Charlie")) (defvar background-color Color:red (export Color)) (defvar player-node (export NodePath)) The ``defvars`` Macro """"""""""""""""""""" If you're simply declaring several instance variables in a row and do not need to provide initial values for any of them, you may use the :ref:`macro-defvars` macro. ``defvars`` takes any number of instance variable names and declares those variables, without initial values. ``defvars`` does not support ``export`` or ``onready``. :: (defvars player-hp character-name background-color) Instance Functions ^^^^^^^^^^^^^^^^^^ The main lifeblood of a class is its instance methods, which are declared using a similar ``defn`` syntax to module functions. :: (defn method-name (args ...) body ...) Like signal declarations, instance function declarations take a simple lambda list, which means modifiers such as ``&opt``, ``&arr``, and ``&rest`` are not allowed in this context. Inside the instance method body, the variable ``self`` is available and refers to the current instance of the class. Note that GDLisp does *not* implicitly insert ``self`` in any context. That is, a bare name like ``example`` will *always* refer to a statically-scoped local variable or module constant with the name ``example``, even inside a class. To refer to the instance variable with that name, you must explicitly write ``self:example``. The syntax sugar ``@example`` is provided, which desugars to ``self:example``. Methods may be marked ``static``, to indicate that they should be called on the class itself rather than an instance. :: (defn method-name (args ...) static body ...) Inside a static method, the name ``self`` is *not* bound. .. Tip:: You probably shouldn't be using the ``static`` modifier very often. Usually, when you find yourself writing a ``static`` class method, that method would be better written as a top-level module function instead. ``static`` is provided mainly for compatibility with GDScript. Super Calls """"""""""" Inside an instance method, you may use the special syntax ``(super:method-name ...)`` to invoke the method called ``method-name`` on the *superclass* of the current class. ``method-name`` need not be the name of the currently-executing method (though it usually will be, in practice). Note that ``super`` on its own is *not* a variable, so attempting to assign ``super`` to a local variable or pass it as a function argument will fail. Constructors """""""""""" Class constructors in GDLisp are a bit special and have some additional syntax to accommodate that. A constructor is called ``_init`` and is declared using ``defn`` like any other instance function. Constructors do not return values, though ``(return nil)`` can still be used to exit the constructor early. :: (defn _init (args ...) body ...) A class constructor cannot be made ``static``. Inside the body of the constructor, if you wish to call the superclass' constructor, you may do so by calling the function ``super`` as the *first* expression in the constructor. :: (defn _init (args ...) (super args ...) body ...) The ``super`` call, if present, *must* be the first expression in the constructor body. The argument list to a constructor supports a special syntax unique to constructors. An ``@`` sign can be placed before the name of an argument. :: (defn _init (@foo @bar)) In this case, ``foo`` and ``bar`` are *not* local variables to the constructor. Instead, the values given to those parameters are assigned directly to instance variables on the class itself. Essentially, the above is equivalent to :: (defn _init (foo bar) (set @foo foo) (set @bar bar)) Note that the automatic assignment happens after any ``super`` constructor call. .. _getters-and-setters: Getters and Setters """"""""""""""""""" Getters and setters are special instance methods that look like ordinary instance variables, from a caller's perspective. Getters are declared using ``defn`` with a special method name of the form ``(get ...)``. A getter method must be non-static and cannot take any arguments. :: (defn (get variable-name) () body ...) When a user of the class attempts to get the instance variable ``variable-name`` from the class, the method ``(get variable-name)`` will be called instead, and its result will be used as the value of the expression. Likewise, setters are declared using a ``(set ...)`` name. A setter method must be non-static and must take exactly one argument. Like a constructor, a setter does not return any values, though it can exit early with ``(return nil)``. :: (defn (set variable-name) (value) body ...) The setter will be invoked when a caller attempts to ``set`` the given instance variable. :: (set my-instance:variable-name value) The same variable name can be used for a setter and a getter. The name of a setter/getter cannot coincide with the name of an actual, concrete ``defvar`` instance variable on the class. Setters and getters are fully compatible with GDScript, in that a caller from GDScript who attempts to get or set the given variable will correctly invoke the getter or setter function, respectively. If your goal is to wrap a ``defvar`` with some validation or some actions, a common idiom is to precede the ``defvar`` with an underscore and then use the property outside the class. Example:: (defclass Player (Node2D) (defsignal hp_changed) (defvar _hp 10) (defvar max_hp 10) (defn (get hp) () ;; Simply return the instance variable @_hp) (defn (set hp) (x) ;; Make sure the HP value is in bounds (set @_hp (clamp x 0 @max_hp)) ;; Let everyone know the value has changed (@emit_signal "hp_changed"))) Main Classes ------------ Godot is based around the idea that every file is also a class, in some form or another. In GDLisp, this is not the case. However, it is often useful to designate one class in a GDLisp module as the "primary" class of that module, so that Godot can link scenes and other resources up to it in the editor. For this reason, GDLisp classes can be marked with the ``main`` modifier. :: (defclass ClassName (ParentClassName) main body ...) Every file can have at most one ``main`` class, and the ``main`` class cannot be private. From the perspective of GDLisp modules, the ``main`` modifier changes nothing. A GDLisp module will still import the class name with ``use`` just like any other name. However, in the resulting GDScript file, the ``main`` class will be compiled to the top-level class of the file, rather than a nested class. This allows the editor to bind the ``main`` class to a packed scene or other resource. Signals ------- Godot communicates between nodes and other objects using signals. For the most part, GDLisp supports signals and connections just like GDScript:: (my-object:connect "signal" target-object "_target_method_name") (my-object:disconnect "signal" target-object "_target_method_name") However, GDLisp also offers some convenience functions to deal with common use cases. :: (connect>> my-object "signal" (lambda () ...)) The built-in function ``connect>>`` connects a signal to a lambda function. When the signal is fired, the lambda function will be called and can accept the arguments of the signal. As a first-class function, the lambda is free to close around any local variables (including ``self``, if applicable) in the current scope, so you should never have to explicitly bind variables of a signal connection when using ``connect>>``. GDLisp also provides a function ``connect1>>``, which takes the same arguments but connects a signal as if using the one-shot flag. That is, the connection removes itself after firing once. Both ``connect>>`` and ``connect1>>`` return a unique value to identify the connection. To disconnect a signal that was connected in this way, use ``disconnect>>``, which takes the return value of ``connect>>`` or ``connect1>>`` and disconnects it. .. Note:: You may freely intermix GDLisp-style ``connect>>`` calls and Godot ``connect`` method calls in the same program. However, ``disconnect>>`` should only be used on connections established with ``connect>>`` or ``connect1>>``, while the built-in Godot method ``disconnect`` should be used for those established with ``connect`` or through the Godot UI. ================================================ FILE: doc/readthedocs/tutorial/control-flow.rst ================================================ Basic Control Flow ================== Let's jump right in and take a look at some GDLisp code. Once you've :ref:`built GDLisp `, you should have an executable called ``gdlisp``. If you invoke this executable with no arguments, you'll be placed in a read-eval-print loop, or REPL for short. In this interactive interpreter, you can type GDLisp expressions or declarations and see the results live. This interpreter is backed by a running Godot instance, so you also have access to the full breadth of built-in Godot types and functions, like ``Node`` and ``sqrt``. Hello, world! ------------- Let's start with everybody's favorite program. :: (print "Hello, world!") If you type this into the REPL and hit enter, you'll see "Hello, world!" printed to the screen. This calls the built-in function ``print`` with one string argument, namely the string "Hello, world!". Congratulations! You've just written your first line of GDLisp. If you just ran this, you'll notice that, in addition to the string, the interpreter also printed out a stray set of parentheses ``()``. This is normal and is for good reasons. In GDLisp, every expression returns a value, even those like ``print`` that are usually only called for their side effects. Since ``print`` has no meaningful return value, it returns the nil value as a default, which is written as ``()``. So when you see ``()``, you can read that as "nil". .. Note:: You might wonder why the null value prints out as ``()``, rather than as the word "nil". This is because ``()`` is used as a placeholder for the empty list and is used as the final cdr cell in a proper list. You can read more about how lists are actually stored in GDLisp at :ref:`parser`. Let's do some math. :: (+ 1 1) This will, naturally, add one and one together and return the result: two. Operators like ``+`` and ``-`` are written in prefix notation in GDLisp, in contrast to GDScript and most non-Lisp languages, which use infix notation. In fact, ``+`` isn't an operator at all; it's just a function like ``print``. GDLisp is very liberal in what it considers a valid function name, and ``+`` is a perfectly valid identifier. The ``+`` function also supports variable arguments. The following will add all four numbers together and print the total:: (+ 1 2 3 4) Since everything is written as prefix function calls, there's never any ambiguity about operator precedence. :: (* 3 (+ 1 1) (+ 1 10 (* 2 4))) All of the common mathematical operators work as functions in exactly the way you'd expect. ``+`` is for addition, ``*`` is for multiplication, ``-`` is for subtraction, and ``/`` is for division. All of the operators support variable arguments, with associativity to the left when it matters (so, for instance ``(/ a b c)`` is equivalent to ``(/ (/ a b) c)``, not ``(/ a (/ b c))``). Comments -------- GDLisp line comments start with a semicolon. :: ; This is a line comment By convention, a single semicolon is used to annotate a line of code, and a pair of semicolons is used when a line comment stands on its own line. :: ;; This is a standalone line comment. (+ 1 1) ; This comment annotates other code. Block comments begin with ``#|`` and are terminated by ``|#``. Block comments cannot be nested. Data Types ---------- GDLisp supports the same basic data types as GDScript, albeit with different syntax. The GDLisp equivalent of Godot's "null" value is called "nil" and is written in GDLisp as ``()``. GDLisp also defines a global constant called ``nil``, which evaluates to ``()``. The Boolean values "true" and "false" are written, respectively, as ``#t`` and ``#f``. Integers and floats are written in GDLisp in the same way as in GDScript. Strings are written in GDLisp using double quotes ``"..."``. Strings *cannot* be written using single quotes, as the single quote character is used for something different in GDLisp. Aside from that, all of the usual escape sequences work. Additionally, GDLisp supports two methods of escaping Unicode values in strings. The first consists of ``\u`` followed by exactly four hexadecimal digits, exactly like GDScript's syntax for the same. The second consists of ``\u{...}``, with any number of hexadecimal digits enclosed in the curly braces. This syntax can be used to represent *any* Unicode code point, including those outside the basic multilingual plane. Conditionals ------------ If Expressions ^^^^^^^^^^^^^^ The most basic conditional in GDLisp is the :ref:`macro-if` macro. :: (if some-conditional true-case false-case) ``if`` is a macro that takes three arguments. The first argument is the conditional, which is evaluated according to Godot's rules of truthiness. That is, zero, false, the empty string, and other intuitively "empty" objects are considered false. :: (if (> x 0) (print "x is positive!") (print "x is not positive!")) This expression tests whether the variable ``x`` is positive and, in either case, prints out an appropriate message. Note that ``>``, like ``+``, is a perfectly valid identifier in GDLisp and is in fact a simple, built-in function just like ``+``. Up to this point, we've been calling these forms "expressions" somewhat informally, but it's important to note that even control forms like ``if`` are expressions in GDLisp, whereas in a language like GDScript or Python ``if`` would be considered a statement. Put more colloquially, ``if`` is really no different than any other expression and can also return values. So we could've written our ``print`` like so. :: (print (if (> x 0) "x is positive!" "x is not positive!")) Or we could have returned the resulting string from a function, or assigned it to a variable, or any number of other things. GDLisp does not distinguish between statements and expressions. Anything that can be used in expression context returns a value. The true and false branches of the ``if`` statement are each individual arguments, which means each branch expects a single expression. If you need to perform multiple actions inside one of the branches, you can use the :ref:`expr-progn` special form. :: (if (> x 0) (progn (print "x is positive!") (print "Have a lovely day :)")) (print "x is not positive!")) ``progn`` is a special form that takes zero or more arguments, evaluates each of them in order, and then returns the value of the last one. Finally, the "false" branch is optional and will default to the nil expression ``()``. :: (if (> x 0) (progn (print "x is positive!") (print "Have a lovely day :)"))) Though if your intention is to execute some code conditionally for its side effects, you might find the macros :ref:`macro-when` and :ref:`macro-unless` more useful. Cond Expressions ^^^^^^^^^^^^^^^^ Extending our ``if`` expression from above, we might consider adding another case to distinguish between negative numbers and zero. :: (if (> x 0) (print "x is positive!") (if (< x 0) (print "x is negative!") (print "x is zero!"))) Nesting additional ``if`` branches in an "else" block is a very common pattern. Languages like Python and GDScript accommodate this with an ``elif`` keyword. GDLisp accommodates this pattern with the ``cond`` expression, the general-purpose conditional dispatch form. ``cond`` takes the following form. :: (cond (conditional1 branch1) (conditional2 branch2) ...) That is, the word ``cond`` is followed by several lists. Each list consists of a conditional expression, similar to the conditional portion of ``if``, followed by one or more expressions. When evaluated, the ``cond`` form evaluates each conditional. When it encounters one that's true, it stops, evaluates that branch, and returns the result. Our ``if`` above can be translated as follows:: (cond ((> x 0) (print "x is positive!")) ((< x 0) (print "x is negative!")) (#t (print "x is zero!"))) If none of the branches match, then ``cond`` silently returns ``()``, though in many cases (including our example above), it's common to include a final "catch-all" clause whose conditional is the literal true ``#t`` value. Like ``if``, ``cond`` is an expression and returns values, so the entire ``cond`` expression can be assigned to a variable or passed as a function argument. .. Note:: Incidentally, ``cond`` is the only primitive conditional expression in GDLisp. ``if``, ``when``, ``unless``, ``and``, and ``or`` are all macros built on top of ``cond``, while ``cond`` is baked into the compiler. Comparisons ^^^^^^^^^^^ We've already seen the ``<`` and ``>`` functions, which compare values for, respectively, the less-than and greater-than relations. The ``<=`` and ``>=`` functions also exist for non-strict comparisons. Finally, ``=`` is used for equality and ``/=`` is used for inequality. Comparison, ordering, and equality semantics are equivalent to the corresponding Godot operators. All of these functions accept variable arguments and are applied transitively to their arguments. Concretely, that means that ``(< a b c d)`` is true if and only if ``a`` is less than ``b``, ``b`` is less than ``c``, and ``c`` is less than ``d``. The other comparison operators work similarly. Of particular note is ``/=``, which is unique among the comparison operators in that it is not transitive. ``(/= a b c d)`` is true if and only if *none* of the arguments are equal. It compares every possible pairing of arguments, not just the adjacent ones. More concretely, ``(/= 1 2 1 3)`` is false, since one is equal to itself. Since the ``=`` function obeys Godot's built-in equality semantics, it compares dictionaries by reference. The GDLisp function ``equal?`` works like ``=`` but for deep equality. ``equal?`` is similar to the GDScript function ``deep_equal``, except that the former accepts variable arguments and applies the equality relationship transitively. Looping Expressions ------------------- Like GDScript, GDLisp provides constructs for repeating code. While Loops ^^^^^^^^^^^ The simplest looping construct is a ``while`` loop. To print all of the numbers from 10 down to 0, we can write:: (let ((x 10)) (while (> x 0) (print x) (set x (- x 1)))) The ``while`` form takes a conditional expression as its first arguments, and the loop body, which can consist of zero or more expressions, follows. When a ``while`` loop is evaluated, the conditional is evaluated. If the conditional is true, then the body is evaluated and the process repeats. If the conditional is false, then the body is skipped. ``while`` loops always return ``()``. GDLisp has no direct equivalent of a "do-while" loop from other languages. However, the construct is easy enough to mimic. Since the conditional part of a ``while`` loop can be *any* expression (even a ``progn`` form), we can simply place the entire loop body inside the conditional part and leave the body empty. :: (let ((x 10)) (while (progn (print x) (set x (- x 1)) (> x 0)))) For Loops ^^^^^^^^^ For loops in GDLisp work exactly as they do in GDScript. :: (for i (range 10) (print x)) ``for`` takes a variable name, an iterable object (such as an array or a string), and a body. It evaluates the body once for each element of the iterable, with the variable bound to that element at each iteration. Like ``while``, a ``for`` loop always returns ``()``. Breaking and Continuing ^^^^^^^^^^^^^^^^^^^^^^^ The built-in special forms ``(break)`` and ``(continue)`` work like the equivalent GDScript keywords. ``(break)`` can be used inside of a loop and, when evaluated, will immediately exit the loop. ``(continue)`` can be used inside a loop and jumps back to the beginning of the loop, beginning the *next* iteration of the loop (or exiting the loop, if we were on the final iteration). How NOT to Loop ^^^^^^^^^^^^^^^ Now that we've seen the two basic looping constructs in GDLisp, it's important to emphasize that there are often alternatives. In an imperative language like GDScript or Java, most iteration is done, as a matter of course, with ``for`` or ``while``. However, GDLisp is a functional programming language, and as such it provides several higher-order functions designed to capture common looping and iteration patterns. This section aims to provide an incomplete summary of some of the useful functions that can be used to replace common looping patterns. Transforming each Element of an Array """"""""""""""""""""""""""""""""""""" Consider this GDScript code. .. code-block:: gdscript var new_array = [] for element in old_array: new_array.push_back(element * 2) return new_array This block of code takes an array ``old_array``, doubles each element, and returns a new array containing the doubles. This pattern of applying an operation to each element of an array is captured by the :ref:`function-array-map` function. The idiomatic way to write the above code in GDLisp is :: (array/map (lambda (x) (* x 2)) old-array) Summing or Combining all Elements of an Array """"""""""""""""""""""""""""""""""""""""""""" This GDScript code sums the elements of an array. .. code-block:: gdscript var total = 0 for element in old_array: total += element return total The pattern of accumulating all of the elements of an array using some binary function is called a *fold*, and it can be done in GDLisp with :ref:`function-array-fold`. :: (array/fold #'+ old-array 0) Discarding some Elements of an Array """""""""""""""""""""""""""""""""""" The following GDScript code takes an array and keeps only the elements which are positive. .. code-block:: gdscript var new_array = [] for element in old_array: if element > 0: new_array.push_back(element) return new_array This pattern is captured by the :ref:`function-array-filter` function. :: (array/filter (lambda (x) (> x 0)) old-array) Searching an Array for some Matching Element """""""""""""""""""""""""""""""""""""""""""" This GDScript code searches an array, in order, for an element satisfying a particular condition, returning the first match. .. code-block:: gdscript for element in old_array: if element % 10 == 0: return element return null This can be done in GDLisp with :ref:`function-array-find`. :: (array/find (lambda (x) (= (mod x 10) 0)) old-array) ``array/find`` also optionally accepts a third argument, which defaults to ``()`` and is returned if no match is found. Locals ------ Local Variables ^^^^^^^^^^^^^^^ As we start to write larger blocks of code, it will become convenient to store the results of intermediate expressions in local variables. We've already seen examples that use the most basic primitive: ``let``. :: (let ((variable-name1 initial-value1) (variable-name2 initial-value2) ...) ...) The ``let`` form takes a list of variable clauses and then a body. The given variables are declared and bound to their initial values. Then the body is evaluated in a scope where the local variables exist. You can declare any number of variables in a ``let`` clause. Crucially, the variables' scope is bound strictly to the ``let`` block, so as soon as the body of the ``let`` finishes evaluating, the variables are no longer accessible. This is in contrast to GDScript, where a ``var`` declaration implicitly lasts until the end of the *enclosing* scope. In GDLisp, a local variable always defines its own scope when it's declared. Example:: (let ((a 1) (b 2)) (+ a b)) ; 3 Note that when a ``let`` block has multiple variable clauses, the clauses are evaluated in parallel. :: (let ((x 1) (y (+ x 1))) y) In this example, the ``x`` from the first clause is *not* in scope when the ``y`` clause is evaluated. If there's an ``x`` from an *enclosing* scope available, then ``y`` will be set to that variable plus one. If not, this code will produce an error at compile-time. It's common to want several variable bindings to be evaluated in order, such as when initializing several pieces of data to perform some calculations. For this use case, GDLisp provides the :ref:`macro-let-star` macro. :: (let* ((x 1) (y (+ x 1))) y) ; 2 ``let*`` works exactly like ``let`` and shares all of the same syntax, except that the variables in a ``let*`` are bound sequentially, with each subsequent variable having access to all of the prior ones. In effect, a ``let*`` form is equivalent to several nested ``let`` forms, each having only one variable clause. To change the value of an existing local variable, use ``set``. :: (let ((x 1)) (print x) ; 1 (set x (+ x 1)) (print x)) ; 2 Note that local variables, even mutable local variables, fully support closures. That is, if a ``lambda`` or other local function construct closes around a local variable, then both the closure function and the enclosing scope can modify the variable, and both scopes will reflect the change. Local Functions ^^^^^^^^^^^^^^^ Similar to ``let``, GDLisp provides the ``flet`` primitive for declaring one or more local functions. This is most useful when you have a private function that is only needed inside one function. :: (flet ((normalize-name (name) ((name:capitalize):substr 0 10))) (print "Hello, " (normalize-name first-name) (normalize-name last-name)) (print "You have an unread message from " (normalize-name caller-name))) In this hypothetical example, we need to print some messages involving people's names. For consistency in our hypothetical UI, we want all names to be capitalized in a particular way and to truncate their length to ten characters. We can define the function to normalize these names as a local function with ``flet`` and then call it as many times as we want inside the ``flet`` block. Outside the block, the function doesn't exist. ``labels`` is a variant of ``flet`` with the same syntax but more powerful binding semantics. ``labels`` evaluates its function bodies in a scope where all of the function clauses already exist. This allows your local helper functions to be recursive or to depend on each other in any order. .. Tip:: If you're declaring a local function with the intent of passing it (as an argument) to a higher-order function like ``array/map`` or ``array/filter``, you'll have better luck using an ordinary ``let`` and a ``lambda``, since you need to reify the function as a value anyway. ``flet`` and ``labels`` are intended for situations where you want to locally *call* the function inside the scope. ================================================ FILE: doc/readthedocs/tutorial/index.rst ================================================ Getting Started =============== Welcome to GDLisp, the Lisp dialect designed to compile to GDScript! If you're interested in learning how to write GDLisp code, you're in the right place. This tutorial is targeted at game developers who use Godot and are interested in learning about GDLisp. No prior knowledge of Lisp is assumed. However, I do assume familiarity with the Godot toolchain and with the scene tree model. If you're a first-time user of Godot, I recommend starting out with GDScript. The official Godot documentation has `an excellent tutorial `_ for folks new to the engine. .. toctree:: :maxdepth: 2 :caption: Contents: lisping.rst control-flow.rst modules.rst classes.rst macros.rst what-now.rst ================================================ FILE: doc/readthedocs/tutorial/lisping.rst ================================================ Introduction to Lisp ==================== The term "Lisp" encompasses a broad collection of programming languages dating back to the 1960s. This section aims to describe the Lisp ecosystem in a broad sense and to talk about the parts of GDLisp that are like other Lisp dialects, without going into too much detail on the Godot-specific parts. If you've used another Lisp dialect in the past, you can likely skim this section or skip it entirely. What is Lisp? ------------- Lisp dialects are homoiconic, functional programming languages with a distinctive syntax. If you've seen Lisp code in the past, you'll probably recognize it offhand. :: (let ((x 1) (y 1)) (if (> x 0) (+ x y) y)) This is a GDLisp expression. It declares two local variables: ``x`` and ``y``. Both variables get the initial value of 1. Then we check whether or not ``x`` is greater than zero. If so, we add ``x`` and ``y`` together, and if not, we simply return ``y``. Aside from some minor syntax sugar, every piece of GDLisp syntax is shown in the above snippet of code. Specifically, a GDLisp source file is made up of zero or more *S-expressions*. An S-expression, or symbolic expression, is defined recursively to be either a *literal* (such as a number, a symbol, or a string) or a list of zero or more S-expressions wrapped in parentheses. .. Note:: This is a slight oversimplification. Strictly speaking, an S-expression is either a literal or a *cons cell*, which may or may not form a proper list, but in practice most cons cells written in a GDLisp program are proper lists. For a full description of the syntax of GDLisp, see :ref:`parser`. For example, ``3`` is an S-expression representing, naturally, the number 3. ``(10 20)`` is an S-expression which is a list. That list contains two literals: 10 and 20. Lists can be nested, such as ``((9) 8)``. This S-expression is a list of two elements. The first element is *itself* a list of one element, while the second is a literal. This is the entirety of the GDLisp syntax. Every construct in GDLisp can be built up from these basic rules. There is some syntax sugar on top of these constructs to make common idioms (such as calling methods on objects, or getting Godot nodes by name) easier. This raises the natural question: What do we *do* with this syntax? Every GDLisp expression is read in the same way. Strings and numbers evaluate to themselves, so ``3``, when evaluated, returns the numerical value 3. Symbols, which are unquoted barewords, evaluate to the value of the variable with the given name. In the ``let`` snippet above, ``x`` and ``y`` are symbols, and inside the ``let`` block, they evaluate to the current value of the variables ``x`` and ``y``, respectively. Finally, lists can act as function calls, macro calls, or special forms. We'll dive more into the specifics of those later on, but the basic idea is that an S-expression which is a list takes the form :: (name arg1 arg2 ...) where ``name`` is usually a symbol. The function or other form with the given name will be *called* with the given arguments. Why Lisp? --------- So why should we care? Our syntax has been reduced to two simple rules, in contrast to languages like Python which require complex parsers to even understand the language. This gives us some powerful introspection capabilities, and none more so than *macros*. If you've been programming in GDScript or another programming language for very long, you've likely encountered *functions*. When we write ``example(a, b, c)`` in GDScript, what happens is this: Godot invokes a function called ``example`` with the arguments ``a``, ``b``, and ``c``. But that's not all that happens. First, ``a``, ``b``, and ``c`` have to be evaluated. They might be variables, or function calls of their own, or something more complicated. The same is true in GDLisp, with some caveats. If there exists a function called ``example`` in the current scope, then the above GDScript call is equivalent to ``(example a b c)``. Remember that this is an S-expression of length 4. The first element indicates the function's name and the other three are its arguments. However, GDLisp also supports *macros*. A macro is like a function, but whereas a function takes runtime values and returns a value, macros operate on code. That is, macros take input in the form of source code and produce new code. Calling a macro is done in the same way as calling a function, so if ``example`` happened to be a *macro* in the current scope, then ``(example a b c)`` would invoke the macro with three arguments. However, those three arguments would *not* be evaluated. Instead, they would literally be passed, as S-expressions representing pieces of syntax, to ``example``. ``example`` then returns a new S-expression that will be evaluated in place of the original macro call. Let's see a more concrete example. GDLisp already provides a macro called ``and``, but let's pretend for the moment that it does not. Let's write a function called ``and`` that takes two Boolean arguments and returns true if and only if both arguments are true. :: (defn and (a b) (if a b #f)) Let's break this down for a moment. ``defn`` is used to declare functions, so this defines a *function* (not a macro) called ``and``. The function takes two arguments ``a`` and ``b``. Inside the function, if ``a`` is true, then we return ``b``, and if ``a`` is false, then we return false (represented by the literal ``#f``). Note that we never actually use the word ``return`` here. In GDLisp, the last expression of a function is automatically returned. This function has a problem, though. In most programming languages, the Boolean operators ``and`` and ``or`` exhibit `Short-circuit evaluation `_. That is, if ``a`` happens to be false, then the whole expression is false, regardless of the value of ``b``, so there's no sense in even evaluating the latter. But our current function doesn't do that. By definition, functions evaluate all of their arguments before even trying to execute. What we want is a special sort of function-like object that *doesn't* evaluate its arguments and instead produces some code that does what we want. And that's exactly what a macro is. Let's rewrite this function to be a macro instead. :: (defmacro and (a b) `(if ,a ,b #f)) That's it. That's the macro. We've changed ``defn`` to ``defmacro`` to indicate our intent to write a macro, and we've sprinkled some funny commas and backticks in the code. Now our macro takes two unevaluated arguments ``a`` and ``b``, and it produces an ``if`` statement. The backtick starts a :ref:`quasiquote `, which is a sort of fancy template. The quasiquote is interpreted literally, without evaluating anything inside of it, until we encounter an *unquote* expression, indicated by the comma, which interpolates a value into the resulting expression. This is sort of like string interpolation in Python or Javascript, but it works for arbitrary S-expressions, not just strings. .. Note:: We're not introducing new fundamental syntax here. We're just introducing some syntax sugar. The backtick and comma expand to, respectively, the words ``quasiquote`` and ``unquote``. So, written without any syntax sugar, the macro declaration above is equivalent to:: (defmacro and (a b) (quasiquote (if (unquote a) (unquote b) #f))) but the former is quite a bit more readable. So the true benefit of Lisp is this. Since our code is merely data, we can write functions which operate on code and produce more code, and we write those functions in the same language that we would write ordinary value-based functions as well. These "code functions" are called macros. As we delve further into the GDLisp programming language, we'll encounter several built-in macros. In fact, ``and`` is a macro built-in to the standard library (though the implementation is a bit more complex than our example here, as the standard library macro accounts for arbitrary numbers of arguments). Functional Programming ---------------------- Functional programming is a paradigm that puts emphasis on treating functions as first-class citizens of the language and being able to pass around functions and construct new functions from existing ones using combinators. GDLisp is designed to support a functional style of programming. GDScript, on its own, has some basic facilities for functional programming, but they can be obtuse. For example, `FuncRef `_ can be used to wrap an object and a method name into a callable object. However, FuncRef cannot create closures around local variables. Likewise, there's no way to declare a local function that's only needed for a few lines. In GDLisp, all of these restrictions are lifted. Functions are first-class values in the language. To convert a function into a value that can be assigned to a variable, the ``function`` primitive can be used. :: (defn addone (a) (+ a 1)) (function addone) Here, ``addone`` is a function that can be called. However, being a function, it can't very well be passed to another function as an argument. But by wrapping the name in the ``(function ...)`` special form, we convert it into a callable object. This is a common enough operation that it, like quasiquoting above, has a shortcut syntax. Namely, ``#'addone`` is equivalent to ``(function addone)``. To call a function that has been wrapped in this way, we use :ref:`function-funcall`. :: (funcall (function addone) 10) ; Returns 11 But that's not all. GDLisp also fully supports *closures*. The ``lambda`` primitive constructs a local function which has full access to its enclosing scope, including any local variables, as well as the current ``self`` object. Suppose, as a random example, that we have an array of numbers and we want to add some constant to each element of that array, producing a new array of values. In GDScript .. code-block:: gdscript var old_array = [1, 2, 3, 4, 5] var my_constant = 10 var new_array = [] for x in old_array: new_array.push_back(x + my_constant) return new_array If we find ourselves doing this sort of transformation a lot, we might wish to capture the behavior in a "for-each" sort of function that applies a FuncRef to each element of a list. .. code-block:: gdscript func map(old_array, f): var new_array = [] for x in old_array: new_array.push_back(f.call_func(x)) return new_array But we can't really easily apply this to our "add a constant" situation, since FuncRef can't easily capture additional values like ``my_constant``. We can create a sort of FuncRef-like object that *does* capture these values and use that instead. .. code-block:: gdscript class AddConstant: var constant func _init(c): constant = c func call_func(arg): return arg + constant var old_array = [1, 2, 3, 4, 5] var my_constant = 10 return map(old_array, AddConstant.new(10)) But this is a lot of extra code to capture the intuitive notion of "add a constant to a number". In GDLisp, this transformation is already built-in and is called :ref:`function-array-map`. And if we want to use a local variable, we can simply create a ``lambda`` that automatically captures that local variable. :: (let ((old-array [1 2 3 4 5]) (my-constant 10)) (array/map (lambda (x) (+ x my-constant)) old-array)) ``lambda`` constructs a local function that, when called, adds ``my-constant`` to the argument. Then ``array/map`` applies that to each element of our array. Functional programming is all about breaking a problem down into manageable pieces. We've taken the complex problem of "add some number to each element of an array" and broken it down into two easy pieces: "do something to each element of an array" and "add a constant to a number". The first piece was built-in under the name ``array/map``, and the second was a simple ``lambda`` expression. ================================================ FILE: doc/readthedocs/tutorial/macros.rst ================================================ Macros ====== Macros are the centerpiece of any Lisp. Macros provide a mechanism for modifying the code of your program using other program code. A macro declaration looks like a function declaration but is declared using ``defmacro`` instead of ``defn``. :: (defmacro macro-name (args ...) ...) Macros can only be declared at module scope, not inside of a class. When a macro is *called*, its arguments are not evaluated. Instead, it is given the syntax of the arguments directly and should return the syntax of a new GDLisp expression to use in its place. This is best demonstrated with some examples. ``when`` is a macro built into GDLisp that evaluates a conditional and then runs the code inside if (and only if) the conditional is true. Essentially, ``when`` is an ``if`` block that does not have an "else" branch and for which the "true" case can be longer than one expression. If GDLisp did not provide this macro, we could write it as follows:: (defmacro when (condition &rest body) `(if ,condition (progn ,.body))) Let's break this down, as it uses several new features. When the macro is called, it must be called with at least one argument. The first argument, as an abstract syntax tree, will be assigned to ``condition``, and the rest will be accumulated into a list and passed as ``body``. Inside the macro, we use a new form of syntax called *quasiquoting*. Any GDLisp expression can be *quoted*. By calling the ``quote`` special form, or equivalently putting a single quotation mark ``'`` before it, evaluation of the expression is delayed. For literals like strings or numbers, this has no effect, as strings and numbers are self-evaluating forms. For lists and symbols, quoting the value returns a reified representation of the value. That is, ``(if c t f)`` is an ``if`` expression that will evaluate ``c`` and then branch, but ``'(if c t f)`` evaluates to a literal list of four elements, all of which are symbols in this example. Quoting is useful when we have a constant expression that we wish to reify at runtime. But often, especially with macros, we wish to have a *mostly* constant expression with some unknown values interpolated in. For this, we use *quasiquoting*. You can think of quasiquoting as being sort of like string interpolation in Python or Ruby, but for arbitrary expressions, not just strings. A quasiquote begins with the backtick `````, which expands to the ``(quasiquote ...)`` special form. Inside the quasiquote, everything is interpreted literally with the following two exceptions. * ``(unquote ...)`` blocks (equivalently, a prefix comma ``,``) will be evaluated and spliced as an element into the result. * ``(unquote-spliced ...)`` blocks (equivalently, a prefix ``,.``) will be evaluated and concatenated into the result. The enclosing context must be either a list or an array. To see the difference, consider the following:: ;; Assume the variable x has the value (2 3 4) `(1 x 5) ; No unquote, evaluates to (1 x 5) `(1 ,x 5) ; Regular unquote, evaluates to (1 (2 3 4) 5) `(1 ,.x 5) ; Spliced unquote, evaluates to (1 2 3 4 5) Looking back at our ``when`` example:: (defmacro when (condition &rest body) `(if ,condition (progn ,.body))) The ``when`` macro, when called, returns an S-expression whose head is ``if``. The first argument to ``if`` is our condition. Then the second argument is a ``progn`` which runs the entire body (as a list of expressions) if the condition is true. We could write its opposite, ``unless``, in a similar way:: (defmacro unless (condition &rest body) `(if (not ,condition) (progn ,.body))) Macros also work in declaration context, both at module scope and inside of a class. We've already seen one such macro: ``defvars``. :: (defmacro defvars (&rest args) (let ((var-decls (list/map (lambda (name) `(defvar ,name)) args))) `(progn ,.var-decls))) Note that the ``let`` block is *not* part of the expanded code. The ``let`` block is code that's run when the macro is called and is used to *compute* the macro expansion, which is a ``progn`` consisting of several ``defvar`` special forms in a row. .. Note:: You may be wondering how ``progn`` works here, since it's an *expression* that evaluates expressions in order, while ``defvar`` is clearly a declaration that only makes sense at class or module scope. The answer is that ``progn`` is actually deep magic. It's even more special than other "special" forms, in that ``progn`` is the one thing in GDLisp that is valid fully-expanded in both expression or declaration context (or both, incidentally, but that can only occur in the command line REPL). GDLisp provides several macros built-in, which you'll grow accustomed to using as a matter of course (as mentioned before, ``if`` itself is a macro, written in terms of ``cond``). For end-user games, you might never write ``defmacro`` at all, but it's an invaluable tool for library authors who wish to do advanced code generation. And even in a runnable game, you might find yourself automating the boring bits of code generation from time to time as well. ================================================ FILE: doc/readthedocs/tutorial/modules.rst ================================================ Functions and Modules ===================== Now that we have some basic tools to do calculations and store variables in GDLisp, it's time to talk about how to write independent GDLisp source files. Modules ------- This is one of the key places that GDLisp differs from GDScript, and it's important to understand the distinction. In GDScript, every source file you write represents a GDScript resource, effectively a class in Python terminology. A GDLisp source file does *not* automatically produce an instantiable class. There are ways to declare a class, and we will discuss those in the next section. But a GDLisp source file has a one-to-one correspondence with a *module*. A module is a collection of named classes, functions, and constants that can be imported and used in other modules. Functions --------- We've already seen how to declare local functions inside of a block of code, either as a reified value with ``lambda`` or as a scoped local name with ``flet`` or ``labels``. At the top-level of a module, named functions are declared with the ``defn`` declaration form. :: (defn function-name (args ...) body ...) GDLisp functions are, by default, public to the module, which means other modules are free to import and use the function. Private module-level functions can be declared with the ``private`` modifier:: (defn function-name (args ...) private body ...) A function declared in this way can be used freely inside the current module but cannot be imported or used in another module. To call a function, simply use its name as the first symbol of an S-expression:: (function-name arg1 arg2 arg3 ...) The formal argument list of a function supports optional and variadic arguments. To declare a function with optional arguments, use the ``&opt`` directive:: (defn compare-strings (a b &opt case-sensitive) (if case-sensitive (a:casecmp_to b) (a:nocasecmp_to b))) This declares a function ``compare-strings`` that accepts two or three arguments. If the third argument ``case-sensitive`` is not supplied, it defaults to ``()`` (which is falsy). Variadic arguments can be declared with ``&rest`` or ``&arr``. The former groups all extra arguments into a GDLisp list, and the latter groups all extra arguments into a Godot array. :: (defn make-array (&arr args) args) (make-array 1 2 3 4 5) ; Produces the array [1 2 3 4 5] GDLisp standalone function calls always have their argument count validated at compile-time. If a function is called with the wrong number of arguments, the GDLisp compiler will emit an error. Note that the same argument directives ``&opt``, ``&rest``, and ``&arr`` can be applied to local functions declared with ``flet``, ``labels``, or ``lambda``. The only difference is that in the case of ``lambda``, the function is reified into an object, so the argument validation happens at runtime rather than compile-time. Constants --------- Constants are declared with ``defconst``. Like functions, constants are public by default but can be made private to the module. Examples:: (defconst DAYS_IN_YEAR 365) (defconst DEFAULT_PLAYER_NAME "Steve") (defconst EPOCH 1970 private) ; Private constant .. Important:: Unlike GDScript, GDLisp does **not** use constants to load resources and other scripts. GDLisp has a dedicated import syntax for loading other files, whether those files are resources, source files, or packed scenes. Enumerations ------------ Enumerations are declared with ``defenum``. :: (defenum Color RED YELLOW BLUE) This defines an enumeration constant called ``Color`` with three elements: ``Color:RED``, ``Color:GREEN``, and ``Color:BLUE``. Note that we refer to the elements of an enumeration with a colon ``:``, as opposed to a dot ``.`` like we would in GDScript. We'll see this syntax again when we discuss classes. Enum elements can optionally be given specific numerical values. For example:: (defenum NUMBERS (ONE 1) (TEN 10) (ONE_HUNDRED 100)) As with constants and functions, an enumeration can be made private to the enclosing module. :: (defenum NUMBERS private (ONE 1) (TEN 10) (ONE_HUNDRED 100)) Importing Modules ----------------- Modules are of little use if they can't be imported and reused in other modules. In GDScript, we load other resources, including other GDScript source files, with the ``preload`` function, as follows. .. code-block:: gdscript const MyScript = preload("res://MyScript.gd") const MySprite = preload("res://MySprite.png") const MyScene = preload("res://MyScene.tscn") This is still technically possible to do in GDLisp, but it's not idiomatic, and it complicates macro expansion. GDLisp has a special, and very versatile, ``use`` directive that's designed for importing data from other files. Importing Resources ^^^^^^^^^^^^^^^^^^^ Importing non-GDLisp resources is simple. :: (use "res://MySprite.png" as MySprite) (use "res://MyScene.png" as MyScene) After the keyword ``use``, we write the path of the resource, using the same ``res://`` syntax as a GDScript ``preload``. Then we specify how we'd like to name the resource in the current scope. The above snippet defines two constants in the current scope: ``MySprite`` and ``MyScene``. Note that names imported into a module are *not* transitively re-exported, so while our hypothetical module above has access to the names ``MySprite`` and ``MyScene``, other modules that *import* our module cannot import those names from it. If you don't specify an alias for the import, one will be chosen for you based on the name of the resource. The following two lines are equivalent:: (use "res://Example/MySprite.png" as MySprite) (use "res://Example/MySprite.png") This is also how source files written in GDScript are imported into GDLisp. The entire GDScript resource is imported as a single name which the GDLisp module can access. Importing Other GDLisp Modules ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Importing GDLisp modules works a bit differently. A GDLisp module is a collection of functions and other names, not just a single runtime resource. If we use the same import syntax as above:: (use "res://MyModule.lisp" as MyModule) Then every public name from the module ``MyModule`` will be imported into the current module, with the prefix ``MyModule`` added. Concretely, suppose ``MyModule.lisp`` contained these names:: (defn a-function () ...) (defn another-function () ...) (defconst MY_FIRST_CONSTANT 1) (defconst MY_SECOND_CONSTANT 2 private) Then the ``use`` directive given above would have the following effects: * The function ``a-function`` is imported into the current scope, under the name ``MyModule/a-function``. * The function ``another-function`` is imported into the current scope, under the name ``MyModule/another-function``. * The constant ``MY_FIRST_CONSTANT`` is imported into the current scope, under the name ``MyModule/MY_FIRST_CONSTANT``. * The constant ``MY_SECOND_CONSTANT`` is *not* imported, as the name is private and not visible to external modules. As with non-GDLisp resources, if an alias is not specified, then one will be chosen for you based on the full path of the module. Explicit Imports """""""""""""""" Instead of providing a prefix to insert before all names from a module, you may instead specify explicitly which names you want to import (without a prefix) from the module. :: (use "res://MyModule.lisp" (a-function MY_FIRST_CONSTANT)) In this example, the function ``a-function`` and the constant ``MY_FIRST_CONSTANT`` will be imported as-is into the current scope (with no prefix), and ``another-function`` will not be imported at all. Aliases can be provided, for any explicit imports. :: (use "res://MyModule.lisp" ((a-function as an-external-function) (MY_FIRST_CONSTANT as CONSTANT))) Finally, if you want to import all of the (public) names from a module, without any prefix, you may replace the explicit import list with the word ``open``. :: (use "res://MyModule.lisp" open) GDLisp.lisp ----------- If you're compiling your own GDLisp modules, there's one more dependency you need to know about. All of the functions and constants that are defined in GDLisp are provided by a support library, called ``GDLisp.lisp``. This support library is included in the GDLisp package you downloaded and is automatically compiled into ``GDLisp.gd`` when you build the GDLisp compiler. ``GDLisp.gd`` must be included as an autoloaded singleton (with the name ``GDLisp``) in any project that uses GDLisp code. To include this in your project, simply copy the ``GDLisp.gd`` file into your project's root directory and add it in the autoloads list under your project settings, ensuring that the name of the autoload is ``GDLisp`` (this is case-sensitive). **All** GDLisp modules implicitly assume that this autoload is available, so you **must** include it in any project that includes GDLisp code. ================================================ FILE: doc/readthedocs/tutorial/what-now.rst ================================================ What's Next =========== This has been a whirlwind tour of the basic features of GDLisp. More comprehensive documentation is also available at :ref:`comprehensive`. GDLisp is available for use today. I encourage you to try it out in your next Godot project. You can report any issues you encounter at our `official Github repository `_. Good luck, and happy coding! ================================================ FILE: minimal.txt ================================================ Command to build Godot with minimal modules, for testing: > scons platform=server tools=yes target=release_debug disable_3d=no disable_advanced_gui=no module_bmp_enabled=no module_bullet_enabled=no module_camera_enabled=no module_csg_enabled=no module_cvtt_enabled=no module_dds_enabled=no module_denoise_enabled=no module_enet_enabled=no module_etc_enabled=no module_fbx_enabled=no module_freetype_enabled=yes module_gltf_enabled=no module_gridmap_enabled=no module_hdr_enabled=no module_jpg_enabled=no module_jsonrpc_enabled=no module_lightmapper_cpu_enabled=no module_mbedtls_enabled=no module_minimp3_enabled=no module_mobile_vr_enabled=no module_mono_enabled=no module_ogg_enabled=no module_opensimplex_enabled=no module_opus_enabled=no module_pvr_enabled=no module_raycast_enabled=no module_recast_enabled=no module_regex_enabled=no module_squish_enabled=no module_stb_vorbis_enabled=no module_svg_enabled=no module_tga_enabled=no module_theora_enabled=no module_tinyexr_enabled=no module_upnp_enabled=no module_vhacd_enabled=no module_visual_script_enabled=no module_vorbis_enabled=no module_webm_enabled=no module_webp_enabled=no module_webrtc_enabled=no module_websocket_enabled=no module_webxr_enabled=no module_xatlas_unwrap_enabled=no GDScript and GDNative modules are required. Tools are required to be able to query the API. Freetype, 3D, and advanced GUI are required for tools. Everything else is disabled. ================================================ FILE: src/command_line.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! This module provides functionality to parse command line //! arguments. //! //! Generally speaking, the entrypoint to this module will be //! [`parse_args`]. use getopts::{Options, ParsingStyle}; const PROGRAM_DESCRIPTION: &str = r#"The GDLisp compiler. If invoked with filenames, compiles the GDLisp sources at the given path. If given a directory, compiles all GDLisp source files in the Godot project at that directory. If given *no* file or directory arguments, invokes the GDLisp REPL."#; /// This structure contains information about the command line /// arguments passed to the compiler. It is usually constructed via /// [`parse_args`]. #[derive(Debug, Clone, Default)] pub struct CommandLineArgs { /// The input file provided, or `None` if none was provided. pub input_file: Option, /// Whether `--help` was provided. pub help_message: bool, /// Whether the internal `--compile-stdlib` was provided. pub compile_stdlib_flag: bool, /// Whether the `--legacy-repl` command was provided. pub legacy_repl_flag: bool, } impl CommandLineArgs { /// Construct a new defaulted [`CommandLineArgs`]. pub fn new() -> CommandLineArgs { CommandLineArgs::default() } /// Construct a [`CommandLineArgs`] which indicates that the user /// would like to see the help message. pub fn help() -> CommandLineArgs { let mut inst = CommandLineArgs::new(); inst.help_message = true; inst } } /// The [`Options`] which are used for parsing GDLisp command line /// arguments. pub fn options() -> Options { let mut opts = Options::new(); opts .parsing_style(ParsingStyle::FloatingFrees) .long_only(false) .optflag("", "help", "Display usage information") .optflag("", "compile-stdlib", "Compile the GDLisp standard library") .optflag("", "legacy-repl", "Run the old-style GDLisp REPL which compiles rather than executing (legacy)"); opts } /// Parse the arguments and return an appropriate [`CommandLineArgs`] /// instance. /// /// If any parsing error occurs, then [`CommandLineArgs::help()`] is /// returned instead. If you would like to do your own error-handling, /// consider calling [`options`] directly. pub fn parse_args(args: &[String]) -> CommandLineArgs { match options().parse(args) { Err(_) => { CommandLineArgs::help() } Ok(parsed) => { let mut result = CommandLineArgs::new(); result.help_message = parsed.opt_present("help"); result.compile_stdlib_flag = parsed.opt_present("compile-stdlib"); result.legacy_repl_flag = parsed.opt_present("legacy-repl"); result.input_file = parsed.free.first().cloned(); result } } } /// Helper function to show the GDLisp compiler help message. pub fn show_help_message(program: &str) { let opts = options(); let brief = format!("Usage: {} [FILE] [options]\n\n{}", program, PROGRAM_DESCRIPTION); print!("{}", opts.usage(&brief)); } ================================================ FILE: src/compile/args.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! This module provides convenience functions for checking the length //! of [`Vec`] vectors of values. //! //! This module is *not* a handler for the concept of an "argument //! list" on a GDLisp function; for that, use [`crate::ir::arglist`] //! or [`crate::gdscript::arglist`], depending on whether you're //! concerned about the GDLisp or GDScript representation of an //! argument list. This module, instead, is for situations where the //! *compiler* is expecting some predetermined number of arguments, //! and we'd like a convenient way to check that the argument count is //! correct and return an error otherwise. use super::error::{GDError, GDErrorF}; use crate::pipeline::source::SourceOffset; use crate::sxp::ast::{AST, ASTF}; use crate::sxp::literal::Literal; use crate::ir::special_form::access_slot::ACCESS_SLOT_FORM_NAME; use std::fmt; /// `Expecting` specifies how many arguments are expected. The two /// bounds are always inclusive. If a function has no upper bound, /// then [`usize::MAX`] can be used. /// /// An `Expecting` instance where `maximum < minimum` will accept no /// argument lists and always produce an error. No effort is made in /// this module to prevent the construction of such instances. /// /// See also [`ExpectedShape`], which deals with the expected shape of /// a single argument, rather than the number of arguments. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Expecting { pub minimum: usize, pub maximum: usize, } /// `ExpectedShape` specifies the type of argument that is expected. /// /// Whereas [`Expecting`] concerns itself with the number of /// arguments, this enum concerns itself with the shape of an /// individual argument. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ExpectedShape { /// A variable declaration clause for a let expression. VarDecl, /// A macro declaration clause in a macrolet expression. MacroDecl, /// A function declaration clause in an flet expression or similar. FnDecl, /// A valid "extends" clause for a class declaration. SuperclassDecl, /// A special reference name. See /// [`crate::ir::special_form::special_ref_form`]. SpecialRefValue, /// A symbol literal. Symbol, /// A string literal. String, /// An integer literal. Integer, /// The empty list, as a literal. EmptyList, /// Equivalent to `EmptyList` but issues an error message of the /// form "expected end of list". This shape should be used in cases /// where the full list is not syntactically empty but the tail is /// expected to be, for clarity. EndOfList, /// A nonempty list literal. NonemptyList, /// A list literal containing a single element. SingletonList, /// A second argument to `yield`. The GDScript `yield` function is /// unique in that it has two optional arguments, but if one is /// supplied then the other becomes required. This special value /// provides an error message specific to that unique case. YieldArg, /// A valid instance function name, either a symbol or an expression /// of the form `(set name)` or `(get name)`, where `name` is an /// arbitrary symbol. InstanceFnName, /// A symbol, or a pair of symbols as a 2-element list. SymbolOrPairOfSymbols, /// The `access-slot` literal name. AccessSlotName, } impl Expecting { /// `Expecting` instance which expects no arguments at all. pub const NONE: Expecting = Expecting { minimum: 0, maximum: 0 }; /// Convenience function for constructing general `Expecting` /// values. pub fn new(minimum: usize, maximum: usize) -> Expecting { Expecting { minimum, maximum } } /// Synonym for [`Expecting::new`]. pub fn between(minimum: usize, maximum: usize) -> Expecting { Expecting::new(minimum, maximum) } /// An `Expecting` which demands a specific number of arguments. pub fn exactly(value: usize) -> Expecting { Expecting::new(value, value) } /// An `Expecting` with no upper bound. pub fn at_least(minimum: usize) -> Expecting { Expecting::new(minimum, usize::MAX) } /// An `Expecting` with no lower bound. pub fn at_most(maximum: usize) -> Expecting { Expecting::new(0, maximum) } /// Check that the number of arguments is within the bounds /// specified by `self`. pub fn contains(&self, args_count: usize) -> bool { args_count >= self.minimum && args_count <= self.maximum } /// Check that `self.contains(args_count)`, and if not, raise an /// appropriate [`GDErrorF::WrongNumberArgs`] with the given `name` /// and `pos`. pub fn validate_amount(&self, name: &str, pos: SourceOffset, args_count: usize) -> Result<(), GDError> { if self.contains(args_count) { Ok(()) } else { Err(GDError::new(GDErrorF::WrongNumberArgs(String::from(name), *self, args_count), pos)) } } /// Validate against the length of a slice. /// /// Equivalent to `self.validate_amount(name, pos, slice.len())`. pub fn validate(&self, name: &str, pos: SourceOffset, slice: &[T]) -> Result<(), GDError> { self.validate_amount(name, pos, slice.len()) } } impl ExpectedShape { /// Extracts a [`Literal::Symbol`], or reports an error if the /// [`AST`] is not a symbol. pub fn extract_symbol(form_name: &str, ast: AST) -> Result { let pos = ast.pos; match ast.value { ASTF::Atom(Literal::Symbol(s)) => Ok(s), _ => Err(GDError::new(GDErrorF::InvalidArg(form_name.to_owned(), ast, ExpectedShape::Symbol), pos)), } } /// Extracts a [`Literal::String`], or reports an error if the /// [`AST`] is not a string literal. pub fn extract_string(form_name: &str, ast: AST) -> Result { let pos = ast.pos; match ast.value { ASTF::Atom(Literal::String(s)) => Ok(s), _ => Err(GDError::new(GDErrorF::InvalidArg(form_name.to_owned(), ast, ExpectedShape::String), pos)), } } /// Extracts a [`Literal::Int`], or reports an error if the [`AST`] /// is not an integer literal. pub fn extract_i32(form_name: &str, ast: AST) -> Result { let pos = ast.pos; match ast.value { ASTF::Atom(Literal::Int(n)) => Ok(n), _ => Err(GDError::new(GDErrorF::InvalidArg(form_name.to_owned(), ast, ExpectedShape::Integer), pos)), } } /// Validates that the vector is in fact empty. If it is, this /// function returns `()` harmlessly. If it is nonempty, then this /// function produces an appropriate error about the expected shape /// with [`ExpectedShape::EmptyList`]. /// /// This function takes a `&[&AST]` to be compatible with the output /// of [`DottedExpr`](crate::sxp::dotted::DottedExpr). pub fn validate_empty(form_name: &str, lst: &[&AST], pos: SourceOffset) -> Result<(), GDError> { if lst.is_empty() { Ok(()) } else { // Note: Report error as `lst[0].pos`, since that's where the proof of nonempty-ness started. let err_pos = lst[0].pos; let lst: Vec<_> = lst.iter().map(|x| (*x).to_owned()).collect(); Err(GDError::new(GDErrorF::InvalidArg(form_name.to_owned(), AST::list(lst, pos), ExpectedShape::EmptyList), err_pos)) } } /// Equivalent to [`validate_empty`](ExpectedShape::validate_empty) /// but produces [`ExpectedShape::EndOfList`] as error instead. /// /// This function takes a `&[&AST]` to be compatible with the output /// of [`DottedExpr`](crate::sxp::dotted::DottedExpr). pub fn validate_end_of_list(form_name: &str, lst: &[&AST], pos: SourceOffset) -> Result<(), GDError> { if lst.is_empty() { Ok(()) } else { // Note: Report error as `lst[0].pos`, since that's where the proof of nonempty-ness started. let err_pos = lst[0].pos; let lst: Vec<_> = lst.iter().map(|x| (*x).to_owned()).collect(); Err(GDError::new(GDErrorF::InvalidArg(form_name.to_owned(), AST::list(lst, pos), ExpectedShape::EndOfList), err_pos)) } } } impl fmt::Display for Expecting { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.minimum > self.maximum { write!(f, "(no valid call)") } else if self.minimum == self.maximum { write!(f, "exactly {}", self.minimum) } else if self.maximum == usize::MAX { write!(f, "at least {}", self.minimum) } else if self.minimum == usize::MIN { write!(f, "at most {}", self.maximum) } else { write!(f, "{} to {}", self.minimum, self.maximum) } } } impl fmt::Display for ExpectedShape { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ExpectedShape::VarDecl => write!(f, "variable declaration"), ExpectedShape::MacroDecl => write!(f, "macro declaration"), ExpectedShape::FnDecl => write!(f, "function declaration"), ExpectedShape::SuperclassDecl => write!(f, "superclass declaration"), ExpectedShape::SpecialRefValue => write!(f, "special reference value"), ExpectedShape::Symbol => write!(f, "symbol"), ExpectedShape::String => write!(f, "string"), ExpectedShape::Integer => write!(f, "integer"), ExpectedShape::EmptyList => write!(f, "empty list"), ExpectedShape::EndOfList => write!(f, "end of list"), ExpectedShape::NonemptyList => write!(f, "nonempty list"), ExpectedShape::SingletonList => write!(f, "singleton list"), ExpectedShape::YieldArg => write!(f, "additional argument (yield takes 0 or 2 arguments)"), ExpectedShape::InstanceFnName => write!(f, "instance function name"), ExpectedShape::SymbolOrPairOfSymbols => write!(f, "symbol or 2-element list of symbols"), ExpectedShape::AccessSlotName => write!(f, "the literal symbol '{}'", ACCESS_SLOT_FORM_NAME), } } } /// Panic if the vector is nonempty. This method is provided for /// symmetry with [`one`], [`two`], and [`three`]. /// /// This is intended to be used as a convenient destructuring method /// *after* a call to [`Expecting::validate`]. pub fn zero(args: Vec) { assert!(!args.is_empty(), "Assertion violated in gdlisp::compile::args::zero"); } /// Get the single element from the vector, panicking if the length is /// wrong. /// /// This is intended to be used as a convenient destructuring method /// *after* a call to [`Expecting::validate`]. pub fn one(mut args: Vec) -> T { assert!(args.len() == 1, "Assertion violated in gdlisp::compile::args::one"); args.pop().expect("Internal error in gdlisp::compile::args") } /// Get the two elements from the argstor, panicking if the length is /// wrong. /// /// This is intended to be used as a convenient destructuring method /// *after* a call to [`Expecting::validate`]. pub fn two(mut args: Vec) -> (T, T) { assert!(args.len() == 2, "Assertion violated in gdlisp::compile::args::two"); let y = args.pop().expect("Internal error in gdlisp::compile::args"); let x = args.pop().expect("Internal error in gdlisp::compile::args"); (x, y) } /// Get the three elements from the argstor, panicking if the length is /// wrong. /// /// This is intended to be used as a convenient destructuring method /// *after* a call to [`Expecting::validate`]. pub fn three(mut args: Vec) -> (T, T, T) { assert!(args.len() == 3, "Assertion violated in gdlisp::compile::args::three"); let z = args.pop().expect("Internal error in gdlisp::compile::args"); let y = args.pop().expect("Internal error in gdlisp::compile::args"); let x = args.pop().expect("Internal error in gdlisp::compile::args"); (x, y, z) } ================================================ FILE: src/compile/body/builder.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Builders for GDScript code. use crate::gdscript::decl::{self, Decl}; use crate::gdscript::stmt::Stmt; use crate::gdscript::class_extends::ClassExtends; /// A builder for an entire GDScript source file. #[derive(Clone)] pub struct CodeBuilder { toplevel: decl::TopLevelClass, } /// A builder for a sequence of GDScript statements. /// /// This structure also retains a list of "helper" declarations /// necessary for the statements to run correctly. These helper /// declarations should eventually be hoisted to the top-level scope /// containing the statements. These declarations include things like /// inner classes for implementing lambdas and other advanced GDLisp /// features. #[derive(Clone, Default)] pub struct StmtBuilder { body: Vec, helpers: Vec, } /// Trait for builder structures which can have declarations added to /// them in some meaningful way. /// /// This trait allows [`StmtBuilder::build_into`] to fold neatly into /// another [`StmtBuilder`] or into any other kind of builder (such as /// [`CodeBuilder`]) in a nice, uniform way. pub trait HasDecls { /// Adds a declaration to the builder. fn add_decl(&mut self, decl: Decl); /// Adds several declarations to the builder. fn add_decls(&mut self, decls: impl IntoIterator) { for decl in decls { self.add_decl(decl); } } } impl CodeBuilder { /// Construct a new builder for a class which extends the given /// class. /// /// The builder begins representing a class with no declarations and /// no name. pub fn new(extends: ClassExtends) -> CodeBuilder { CodeBuilder { toplevel: decl::TopLevelClass { name: None, extends: extends, body: vec!(), } } } /// Give the class referenced by this builder a name, i.e. a /// `class_name` header in GDScript. /// /// If the class already has a name, it is overwritten. pub fn named(&mut self, name: String) { self.toplevel.name = Some(name); } /// Change what the class extends. pub fn extends(&mut self, extends: ClassExtends) { self.toplevel.extends = extends; } /// Consume this builder and produce a top-level class declaration. pub fn build(self) -> decl::TopLevelClass { self.toplevel } } impl HasDecls for CodeBuilder { fn add_decl(&mut self, decl: Decl) { self.toplevel.body.push(decl); } } impl StmtBuilder { /// A new builder containing no statements. pub fn new() -> StmtBuilder { StmtBuilder::default() } /// Append a single statement to the builder. pub fn append(&mut self, stmt: Stmt) { self.body.push(stmt); } /// Append a collection of statements to the builder in order. pub fn append_all(&mut self, stmts: &mut dyn Iterator) { for stmt in stmts { self.append(stmt) } } /// Append a declaration to the builder's collection of helper /// declarations. pub fn add_helper(&mut self, decl: Decl) { self.helpers.push(decl); } /// Consume the builder and produce its statements and necessary /// helper declarations. /// /// The caller is responsible for ensuring that the helper /// declarations are safely transmitted to the enclosing scope. /// Often, [`StmtBuilder::build_into`] can be used to do this automatically. pub fn build(self) -> (Vec, Vec) { (self.body, self.helpers) } /// Consume the builder, passing any helper declarations onto the /// subsequent builder. /// /// This is more useful than [`StmtBuilder::build`] if you have access to the /// builder (often, but not necessary, a `StmtBuilder`) representing /// the enclosing scope. The helper declarations from `self` are /// added (via [`HasDecls::add_decl`]) to `other`, and the /// statements from `self` are returned. pub fn build_into(self, other: &mut impl HasDecls) -> Vec { let (body, helpers) = self.build(); for h in helpers { other.add_decl(h); } body } } impl HasDecls for StmtBuilder { fn add_decl(&mut self, decl: Decl) { self.add_helper(decl); } } ================================================ FILE: src/compile/body/class_initializer.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Builders for class initializers. use crate::gdscript::decl::{Decl, DeclF, ClassDecl, InitFnDecl, FnDecl, Static}; use crate::gdscript::stmt::Stmt; use crate::gdscript::expr::Expr; use crate::gdscript::arglist::ArgList; use crate::gdscript::library::READY_NAME; use crate::compile::error::{GDError, GDErrorF}; use crate::compile::names::fresh::FreshNameGenerator; use crate::util::find_or_else_mut; use crate::pipeline::source::SourceOffset; use super::builder::{StmtBuilder, HasDecls}; use super::synthetic_field::{SyntheticField, Getter, Setter}; use super::super_proxy::SuperProxy; use super::class_scope::DirectClassScope; use std::mem; use std::collections::HashMap; /// A builder for a GDScript class. /// /// This builder allows certain pieces of functionality for the class, /// such as `_init` and `_ready`, to build up over the course of /// compilation and eventually build it into the resulting class at /// the end. #[derive(Default, Clone)] pub struct ClassBuilder { /// The builder for `_init`. init_builder: StmtBuilder, /// The builder for `_ready`. ready_builder: StmtBuilder, /// The builder for any synthetic fields generated as a result of /// `get` and `set` declarations. synthetic_fields: Vec, /// The builder for any supermethod proxies generated as a result of /// `super` method calls. Note that a `super` constructor call is an /// entirely different mechanism which is parsed into the syntax /// earlier than other `super` method calls and is not included /// here. super_proxies: Vec, /// Any declarations which have been absorbed by another builder. /// This field exists for compatibility with [`HasDecls`] so that /// this builder can be composed with others easily. other_helpers: Vec, } /// This is the eventual result of a [`ClassBuilder`]. It contains /// information that can be used to modify a [`ClassDecl`]. #[derive(Default, Clone, Debug)] pub struct ClassInit { /// Statements to be prepended to the class' `_init` method. init: Vec, /// Statements to be prepended to the class' `_ready` method. ready: Vec, /// Proxy fields to be generated with appropriate `setget` /// declarations. synthetic_fields: Vec, /// Proxy methods to be generated for supermethod calls. super_proxies: Vec, } /// The time that an instance variable should be initialized. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum InitTime { /// The variable is initialized during `_init()`, i.e. when the /// instance itself is first constructed. Init, /// The variable is initialized during `_ready()`, i.e. when the /// instance is added to the scene tree. Ready, } impl ClassBuilder { /// A new, empty initializer builder. Equivalent to /// `ClassBuilder::default()`. pub fn new() -> ClassBuilder { ClassBuilder::default() } /// Returns the builder object for either `_init` or `_ready`, /// depending on the initialization time argument. pub fn builder_for(&mut self, init_time: InitTime) -> &mut StmtBuilder { match init_time { InitTime::Init => &mut self.init_builder, InitTime::Ready => &mut self.ready_builder, } } /// Declares a getter for the given proxy field. If a getter already /// exists, this method overwrites it and returns the old one. /// /// Note: This method takes the field name as argument, not the /// getter name. The field name will be prefixed appropriately (as /// per [`Getter::method_name`]) to determine the field name. pub fn declare_getter_for(&mut self, field_name: String) -> Option { let method_name = Getter::method_name(&field_name); for field in &mut self.synthetic_fields { if field.name == field_name { return mem::replace(&mut field.getter, Some(method_name)); } } // Name wasn't found at all, so add a new one to the end. self.synthetic_fields.push(SyntheticField { name: field_name, getter: Some(method_name), setter: None, }); None } /// Declares a setter for the given proxy field. If a setter already /// exists, this method overwrites it and returns the old one. /// /// Note: This method takes the field name as argument, not the /// setter name. The field name will be prefixed appropriately (as /// per [`Setter::method_name`]) to determine the field name. pub fn declare_setter_for(&mut self, field_name: String) -> Option { let method_name = Setter::method_name(&field_name); for field in &mut self.synthetic_fields { if field.name == field_name { return mem::replace(&mut field.setter, Some(method_name)); } } // Name wasn't found at all, so add a new one to the end. self.synthetic_fields.push(SyntheticField { name: field_name, getter: None, setter: Some(method_name), }); None } /// Declares a supermethod proxy which will delegate to the /// supermethod with the given name. Returns the name of the new /// proxy method we've generated. pub fn declare_super_proxy(&mut self, gen: &mut FreshNameGenerator, super_name: String, args: usize, pos: SourceOffset) -> String { let proxy = SuperProxy::generate(gen, super_name, args, pos); let name = proxy.name.clone(); self.super_proxies.push(proxy); name } /// Incorporates all of the supermethod proxies from the given /// direct class scope. pub fn declare_proxies_from_scope(&mut self, scope: DirectClassScope) { self.super_proxies.extend(scope.into_proxies()); } /// This method takes any of the synthetic fields defined on the /// builder which have either a getter *or* a setter but not both /// and fills out the missing accessor method with one that produces /// an error. After calling this method, the current builder will /// contain only complete synthetic fields, i.e. all of them have /// both a getter and a setter. pub fn fill_out_synthetic_fields(&mut self, decl: &mut ClassDecl, pos: SourceOffset) { for field in &mut self.synthetic_fields { // If getter is none, generate a synthetic one. if field.getter.is_none() { let method_name = Getter::method_name(&field.name); let method = ClassBuilder::implied_getter(&field.name, &method_name, pos); decl.body.push(Decl::new(DeclF::FnDecl(Static::NonStatic, method), pos)); field.getter = Some(method_name); } // If setter is none, generate a synthetic one. if field.setter.is_none() { let method_name = Setter::method_name(&field.name); let method = ClassBuilder::implied_setter(&field.name, &method_name, pos); decl.body.push(Decl::new(DeclF::FnDecl(Static::NonStatic, method), pos)); field.setter = Some(method_name); } } } fn implied_getter(field_name: &str, method_name: &str, pos: SourceOffset) -> FnDecl { let error_string = format!("Cannot access nonexistent field '{}'", field_name); FnDecl { name: method_name.to_owned(), args: ArgList::empty(), body: vec!( Stmt::expr(Expr::simple_call("push_error", vec!(Expr::from_value(error_string, pos)), pos)), ) } } fn implied_setter(field_name: &str, method_name: &str, pos: SourceOffset) -> FnDecl { let arglist = ArgList::required(vec!(String::from("_unused"))); let error_string = format!("Cannot assign to nonexistent field '{}'", field_name); FnDecl { name: method_name.to_owned(), args: arglist, body: vec!( Stmt::expr(Expr::simple_call("push_error", vec!(Expr::from_value(error_string, pos)), pos)), ) } } /// Builds the builder into a [`ClassInit`]. pub fn build(self) -> (ClassInit, Vec) { let mut helpers = self.other_helpers; // Initializer let (init, mut init_helpers) = self.init_builder.build(); helpers.append(&mut init_helpers); // Ready let (ready, mut ready_helpers) = self.ready_builder.build(); helpers.append(&mut ready_helpers); let initializer = ClassInit { init, ready, synthetic_fields: self.synthetic_fields, super_proxies: self.super_proxies, }; (initializer, helpers) } /// Builds the builder into a [`ClassInit`], passing any helper /// declarations onto the enclosing builder. See /// [`StmtBuilder::build_into`] for a summary of why this method /// might be preferred over [`ClassBuilder::build`]. pub fn build_into(self, other: &mut impl HasDecls) -> ClassInit { let (body, helpers) = self.build(); for h in helpers { other.add_decl(h); } body } } impl HasDecls for ClassBuilder { fn add_decl(&mut self, decl: Decl) { self.other_helpers.push(decl); } } impl Default for InitTime { /// [`InitTime::Init`] is the "default" initialization time, if no /// modifiers are applied. fn default() -> InitTime { InitTime::Init } } impl ClassInit { /// Applies the initializer information to the given class /// declaration. The class declaration is mutated in-place. If an /// error occurs, then the class declaration is left in a valid but /// unspecified state. pub fn apply(mut self, class: &mut ClassDecl, pos: SourceOffset) -> Result<(), GDError> { // Initializer if !self.init.is_empty() { let initializer = ClassInit::find_initializer(&mut class.body); self.init.append(&mut initializer.body); initializer.body = self.init; } // Ready if !self.ready.is_empty() { let ready = ClassInit::find_ready(&mut class.body); self.ready.append(&mut ready.body); ready.body = self.ready; } // Proxy fields for getters and setters ClassInit::apply_proxy_fields(self.synthetic_fields, class, pos)?; // Super-call proxy methods for method in self.super_proxies { let fn_decl = FnDecl::from(method); class.body.push(Decl::new(DeclF::FnDecl(Static::NonStatic, fn_decl), pos)); } Ok(()) } /// Generates all of the proxy fields associated with the synthetic /// field collection. In case of a conflict with an existing (non-proxy) /// field, an error is raised. In that case, the class declaration /// is left in a valid but unspecified state. fn apply_proxy_fields(fields: Vec, class: &mut ClassDecl, pos: SourceOffset) -> Result<(), GDError> { let existing_fields = ClassInit::all_var_decls(class); for field in fields { let var_decl = field.into_field(); if let Some(pos) = existing_fields.get(&*var_decl.name) { return Err(GDError::new(GDErrorF::FieldAccessorConflict(var_decl.name), *pos)); } class.body.push(Decl::new(DeclF::VarDecl(var_decl), pos)); } Ok(()) } /// Returns a collection of all of the variable declaration names in /// the given class. fn all_var_decls(class: &ClassDecl) -> HashMap { let mut acc = HashMap::new(); for decl in &class.body { if let DeclF::VarDecl(var_decl) = &decl.value { acc.insert(var_decl.name.to_owned(), decl.pos); } } acc } /// Find the initializer function defined on the current class. If /// no initializer is defined, then an empty one is created, /// appended to the class, and returned. pub fn find_initializer(decls: &mut Vec) -> &mut InitFnDecl { let init = find_or_else_mut(decls, ClassInit::empty_initializer, |d| matches!(d.value, DeclF::InitFnDecl(_))); if let DeclF::InitFnDecl(decl) = &mut init.value { decl } else { panic!("Internal error in ClassInit::find_initializer") } } fn empty_initializer() -> Decl { Decl::new(DeclF::InitFnDecl(InitFnDecl { args: ArgList::empty(), super_call: vec!(), body: vec!(), }), SourceOffset(0)) } /// Find the _ready function defined on the current class. If no /// _ready function is defined, then an empty one is created, /// appended to the class, and returned. pub fn find_ready(decls: &mut Vec) -> &mut FnDecl { let decl = find_or_else_mut(decls, ClassInit::empty_ready, |d| matches!(&d.value, DeclF::FnDecl(_, f) if f.name == READY_NAME)); if let DeclF::FnDecl(_, fdecl) = &mut decl.value { fdecl } else { panic!("Internal error in ClassInit::find_ready") } } fn empty_ready() -> Decl { Decl::new(DeclF::FnDecl(Static::NonStatic, FnDecl { name: READY_NAME.to_string(), args: ArgList::empty(), body: vec!(), }), SourceOffset(0)) } } ================================================ FILE: src/compile/body/class_scope.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Exposes the [`ClassScope`] trait and its various common //! implementations. // TODO Not a fan of all of the `dyn ClassScope` in this file. Can we // remove at least *some* of the indirection? use crate::compile::error::{GDError, GDErrorF}; use crate::compile::symbol_table::local_var::LocalVar; use crate::compile::names::lisp_to_gd; use crate::compile::names::fresh::FreshNameGenerator; use crate::gdscript::expr::Expr; use crate::pipeline::source::SourceOffset; use super::super_proxy::SuperProxy; /// A class scope for situations where we're not inside a class. Any /// attempt to call a superclass method in this scope will fail. #[derive(Clone, Debug)] pub struct OutsideOfClass; /// A simple wrapper around a [`ClassScope`] which delegates to the /// value it's borrowing. pub struct ClassScopeMut<'a>(pub &'a mut dyn ClassScope); /// A class scope for situations where we're *directly* inside of a /// class, i.e. we're inside of a class and there's no closure /// strictly between the current position and the class declaration in /// the scope hierarchy. #[derive(Clone, Debug, Default)] pub struct DirectClassScope { super_proxies: Vec, } /// A class scope within a closure that is transitively contained /// inside a class. This maintains a mutable reference to a /// [`DirectClassScope`] for the purposes of accessing a common /// supermethod proxy list with other closures of the same class. pub struct ClosedClassScope<'a>(pub &'a mut DirectClassScope); /// A `ClassScope` implementor keeps track of which class (if any) /// we're currently compiling inside. This is used to track what /// should be called when we invoke a `super` method. pub trait ClassScope { /// Compiles the necessary code to make a supermethod call to the /// GDLisp name `super_name`, and then returns an expression which /// will perform the call. fn super_call(&mut self, gen: &mut FreshNameGenerator, self_binding: &LocalVar, super_name: String, args: Vec, pos: SourceOffset) -> Result; /// Returns a new `ClassScope` which will handle supermethod calls /// inside of a closure within the current scope. fn closure_mut(&mut self) -> Box; } impl DirectClassScope { /// A new, empty `DirectClassScope`. Equivalent to /// `DirectClassScope::default()`. pub fn new() -> DirectClassScope { DirectClassScope::default() } /// Returns a vector of superclass proxy methods, in an unspecified /// order. pub fn into_proxies(self) -> Vec { self.super_proxies } /// Adds a new supermethod proxy to the current scope. pub fn add_proxy(&mut self, proxy: SuperProxy) { self.super_proxies.push(proxy); } } impl ClassScope for OutsideOfClass { fn super_call(&mut self, _gen: &mut FreshNameGenerator, _self_binding: &LocalVar, super_name: String, _args: Vec, pos: SourceOffset) -> Result { // Always fail. Err(GDError::new(GDErrorF::BadSuperCall(super_name), pos)) } fn closure_mut(&mut self) -> Box { Box::new(ClassScopeMut(self)) } } impl<'a> ClassScope for ClassScopeMut<'a> { fn super_call(&mut self, gen: &mut FreshNameGenerator, self_binding: &LocalVar, super_name: String, args: Vec, pos: SourceOffset) -> Result { // Delegate to inner self.0.super_call(gen, self_binding, super_name, args, pos) } fn closure_mut(&mut self) -> Box { // *shrug* Just another delegator. Kind of silly but gets the job // done. Box::new(ClassScopeMut(self)) } } impl ClassScope for DirectClassScope { fn super_call(&mut self, _gen: &mut FreshNameGenerator, _self_binding: &LocalVar, super_name: String, args: Vec, pos: SourceOffset) -> Result { Ok(Expr::super_call(&lisp_to_gd(&super_name), args, pos)) } fn closure_mut(&mut self) -> Box { // Create a closure scope with access to self. Box::new(ClosedClassScope(self)) } } impl<'a> ClassScope for ClosedClassScope<'a> { fn super_call(&mut self, gen: &mut FreshNameGenerator, self_binding: &LocalVar, super_name: String, args: Vec, pos: SourceOffset) -> Result { let proxy = SuperProxy::generate(gen, super_name, args.len(), pos); let method_name = proxy.name.clone(); self.0.add_proxy(proxy); Ok(Expr::call(Some(self_binding.expr(pos)), &method_name, args, pos)) } fn closure_mut(&mut self) -> Box { // Delegate to self; having two nested closures is no different // than having one closure for these purposes. Box::new(ClassScopeMut(self)) } } ================================================ FILE: src/compile/body/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helpers for working with bodies of code. pub mod builder; pub mod class_initializer; pub mod class_scope; pub mod synthetic_field; pub mod super_proxy; ================================================ FILE: src/compile/body/super_proxy.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Exposes the [`SuperProxy`] type, for construction of instance //! methods that delegate to a superclass method. use crate::pipeline::source::SourceOffset; use crate::gdscript::decl::FnDecl; use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::compile::names::lisp_to_gd; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::names::generator::NameGenerator; use crate::compile::special_form::lambda::simple_arg_names; /// A supermethod proxy is a method on the current class that, when /// invoked, simply delegates to a call of a given method on a /// superclass. #[derive(Default, Clone, Debug)] pub struct SuperProxy { /// The name of the proxy method. pub name: String, /// The name of the superclass method to delegate to. pub super_name: String, /// The total number of arguments to be passed to the superclass /// method. pub args: usize, /// The position in the code where the construction of this proxy /// became necessary. pub pos: SourceOffset, } impl SuperProxy { /// The prefix used to generate names for superclass proxy methods. pub const PROXY_NAME: &'static str = "__gdlisp_super"; /// Generates a superclass proxy method for the method with the /// given (GDLisp) name and argument count. The provided name /// generator is used to come up with a unique name for the proxy /// method. pub fn generate(gen: &mut FreshNameGenerator, super_name: String, args: usize, pos: SourceOffset) -> SuperProxy { let name = gen.generate_with(SuperProxy::PROXY_NAME); let super_name = lisp_to_gd(&super_name); SuperProxy { name, super_name, args, pos } } } impl From for FnDecl { fn from(proxy: SuperProxy) -> FnDecl { let proxy_params = simple_arg_names(proxy.args); let call_args: Vec<_> = proxy_params.all_args_iter().map(|name| Expr::var(name, proxy.pos)).collect(); let body = vec!( Stmt::return_stmt(Expr::super_call(&proxy.super_name, call_args, proxy.pos), proxy.pos), ); FnDecl { name: proxy.name, args: proxy_params, body: body, } } } ================================================ FILE: src/compile/body/synthetic_field.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`SyntheticField`] type, for generating proxy fields //! on GDScript classes. use crate::gdscript::decl::{FnDecl, VarDecl, Setget}; use crate::gdscript::arglist::ArgList; use crate::gdscript::stmt::Stmt; /// A synthetic field will be generated on the resulting class with /// the given getter and setter. Both parts of the synthetic field are /// optional. #[derive(Default, Clone, Debug)] pub struct SyntheticField { /// The name of the field. pub name: String, /// The name of the getter method for the field, if present. pub getter: Option, /// The name of the setter method for the field, if present. pub setter: Option, } /// A getter method is a zero-argument GDScript function which will /// return the value of the proxy field. #[derive(Clone, Debug)] pub struct Getter { name: String, body: Vec, } /// A setter method is a one-argument GDScript function which will set /// the value of the proxy field. #[derive(Clone, Debug)] pub struct Setter { name: String, argument_name: String, body: Vec, } impl SyntheticField { pub fn new() -> SyntheticField { SyntheticField::default() } /// Converts the synthetic field into a variable declaration. The /// resulting variable has no default value and no modifiers other /// than the `setget` modifier. pub fn into_field(self) -> VarDecl { VarDecl::simple(self.name).setget(Setget { setter: self.setter, getter: self.getter, }) } } impl Getter { /// Construct a getter from a method name and a method body. pub fn new(name: String, body: Vec) -> Getter { Getter { name, body } } /// By convention, the name of a GDLisp getter is produced using a /// set prefix to distinguish it. Given a proxy field name, this /// function returns the conventional getter name for that field. pub fn method_name(field_name: &str) -> String { format!("__gdlisp_get_{}", field_name) } } impl Setter { /// Construct a setter from a method name, an argument name, and a method body. pub fn new(name: String, argument_name: String, body: Vec) -> Setter { Setter { name, argument_name, body } } /// By convention, the name of a GDLisp setter is produced using a /// set prefix to distinguish it. Given a proxy field name, this /// function returns the conventional setter name for that field. pub fn method_name(field_name: &str) -> String { format!("__gdlisp_set_{}", field_name) } } impl From for FnDecl { fn from(getter: Getter) -> FnDecl { FnDecl { name: getter.name, args: ArgList::empty(), body: getter.body, } } } impl From for FnDecl { fn from(setter: Setter) -> FnDecl { FnDecl { name: setter.name, args: ArgList::required(vec!(setter.argument_name)), body: setter.body, } } } ================================================ FILE: src/compile/constant.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Functionality for checking whether an expression is actually an //! allowable constant expression. //! //! For the moment, what's allowable as a constant expression is //! *extremely* conservative. Even things like if-statements where all //! arguments are constant are disallowed, as we have no way to //! compile that on the GDScript side. Long term, it would be nice to //! support things like that which are constant "in spirit" and work //! around GDScript limitations. But for now, we're being super //! strict. use super::error::{GDError, GDErrorF}; use super::symbol_table::SymbolTable; use super::symbol_table::local_var::{LocalVar, ValueHint}; use crate::pipeline::source::SourceOffset; use crate::ir::expr::{Expr as IRExpr, ExprF as IRExprF, BareName, CallTarget}; use crate::ir::decl::{ClassInnerDecl, ClassInnerDeclF, Decl, DeclF}; use crate::ir::literal::Literal; use crate::ir::scope::decl::on_each_lambda_class; use phf::phf_set; pub const CONSTANT_GDSCRIPT_FUNCTIONS: phf::Set<&'static str> = phf_set! { "NodePath", "bool", "int", "float", "String", "str", "Rect2", "AABB", "RID", "Dictionary", "Array", "PoolColorArray", "PoolByteArray", "PoolIntArray", "PoolRealArray", "PoolStringArray", "PoolVector2Array", "PoolVector3Array", "Vector2", "Vector3", "Transform2D", "Plane", "Quat", "Basis", "Transform", "Color", }; pub fn validate_all_constant_scopes(decls: &[Decl], table: &SymbolTable) -> Result<(), GDError> { // Check all top-level constant and enum declarations, and delegate for top-level classes for decl in decls { match &decl.value { DeclF::ConstDecl(const_decl) => { validate_const_expr(&const_decl.name, &const_decl.value, table)?; } DeclF::EnumDecl(enum_decl) => { for (_, rhs) in &enum_decl.clauses { if let Some(rhs) = rhs { validate_const_expr(&enum_decl.name, rhs, table)?; } } } DeclF::ClassDecl(class_decl) => { validate_constant_names_in_class(&class_decl.decls, table)?; } _ => {} } } // Check all lambda classes on_each_lambda_class(decls, |cls| { validate_constant_names_in_class(&cls.decls, table) })?; Ok(()) } fn validate_constant_names_in_class(inner_decls: &[ClassInnerDecl], table: &SymbolTable) -> Result<(), GDError> { for decl in inner_decls { match &decl.value { ClassInnerDeclF::ClassConstDecl(const_decl) => { validate_const_expr(&const_decl.name, &const_decl.value, table)?; } ClassInnerDeclF::ClassVarDecl(var_decl) => { if let Some(export) = &var_decl.export { for arg in &export.args { // As a special exception, we allow any literal symbols // appearing here, since they can reference things like // `int` freely. (TODO Just generally make exports fit // better with the rest of GDLisp) if !(matches!(&arg.value, IRExprF::BareName(_))) { validate_const_expr(&var_decl.name, arg, table)?; } } } } _ => {} } } Ok(()) } pub fn is_const_expr(expr: &IRExpr, table: &SymbolTable) -> bool { validate_const_expr("UNUSED NAME IN is_const_expr", expr, table).is_ok() } pub fn validate_const_expr(name: &str, expr: &IRExpr, table: &SymbolTable) -> Result<(), GDError> { match &expr.value { IRExprF::BareName(var) => { match var { BareName::Plain(var_name) => { validate_const_var_name(name, var_name, table, expr.pos) } BareName::Atomic(_) => { // AtomicName is explicitly opting out of GDLisp's safety // checks, so we'll let it through and just trust the // programmer. Ok(()) } } } IRExprF::Literal(lit) => { if let Literal::Symbol(_) = lit { non_constant_error(name, expr.pos) } else { Ok(()) } } IRExprF::Progn(body) => { match &body[..] { [] => { // Empty progn, compiles to null. Ok(()) } [single_term] => { // Single-term progn, compiles to the inside. validate_const_expr(name, single_term, table) } _ => { // Multiple terms will require multiple statements, forbid // it. non_constant_error(name, expr.pos) } } } IRExprF::CondStmt(_) => { // Note: We always compile CondStmt to full-form multi-line "if" // statements first. Sometimes, the optimizer might simplify // them down to ternary expressions, but that's an optimization // detail. As far as we're concerned, it's a statement and is // non-const. // // If the architecture changes and we start compiling to ternary // directly, then this constraint will loosen. non_constant_error(name, expr.pos) } IRExprF::WhileStmt(_, _) => { non_constant_error(name, expr.pos) } IRExprF::ForStmt(_, _, _) => { non_constant_error(name, expr.pos) } IRExprF::Call(object, function_name, args) => { validate_const_call(name, object, function_name, args, table, expr.pos) } IRExprF::Let(_, _) => { non_constant_error(name, expr.pos) } IRExprF::FunctionLet(_, _, _) => { non_constant_error(name, expr.pos) } IRExprF::Lambda(_, _) => { non_constant_error(name, expr.pos) } IRExprF::FuncRef(_) => { non_constant_error(name, expr.pos) } IRExprF::Assign(_, _) => { non_constant_error(name, expr.pos) } IRExprF::Quote(_) => { non_constant_error(name, expr.pos) } IRExprF::FieldAccess(lhs, _name) => { if is_name_of_enum(lhs, table) { Ok(()) } else { non_constant_error(name, expr.pos) } } IRExprF::LambdaClass(_) => { non_constant_error(name, expr.pos) } IRExprF::Yield(_) => { non_constant_error(name, expr.pos) } IRExprF::Assert(_, _) => { non_constant_error(name, expr.pos) } IRExprF::Return(_) => { non_constant_error(name, expr.pos) } IRExprF::Break => { non_constant_error(name, expr.pos) } IRExprF::Continue => { non_constant_error(name, expr.pos) } IRExprF::SpecialRef(_) => { Ok(()) } IRExprF::ContextualFilename(_) => { // This resolves completely at compile-time and, as far as // GDScript is concerned, is just a constant string. Ok(()) } IRExprF::Split(_, _) => { // Split specifically requires a temporary variable in order to // work, which we can't do in a constant expression non_constant_error(name, expr.pos) } IRExprF::Preload(_) => { Ok(()) } } } fn validate_const_var_name(name: &str, var_name: &str, table: &SymbolTable, pos: SourceOffset) -> Result<(), GDError> { let var = table.get_var(var_name).ok_or_else(|| non_constant_error::<()>(name, pos).unwrap_err())?; if var.is_valid_const_expr() { Ok(()) } else { non_constant_error(name, pos) } } fn validate_const_call(name: &str, object: &CallTarget, function_name: &str, args: &[IRExpr], table: &SymbolTable, pos: SourceOffset) -> Result<(), GDError> { match object { CallTarget::Super | CallTarget::Object(_) => { // Always disallowed. return non_constant_error(name, pos); } CallTarget::Atomic => { // `Atomic` is explicitly opting out of GDLisp's safety checks, // so we'll let the name through and just trust the programmer. // We'll still check the arguments though. } CallTarget::Scoped => { // Check the name to make sure it's a constant enough function. validate_scoped_const_call(name, function_name, args.len(), table, pos)?; } } for arg in args { validate_const_expr(name, arg, table)?; } Ok(()) } fn validate_scoped_const_call(name: &str, function_name: &str, arg_count: usize, table: &SymbolTable, pos: SourceOffset) -> Result<(), GDError> { let (function, magic) = table.get_fn(function_name).ok_or_else(|| non_constant_error::<()>(name, pos).unwrap_err())?; if magic.is_default() { // If the call magic passes through to the implementation, then we // need to look at the function and see if it's sufficiently const // for GDScript's tastes. if function.can_be_called_as_const() { Ok(()) } else { non_constant_error(name, pos) } } else { // If there's call magic, use that to determine const-ness. if magic.can_be_called_as_const(arg_count) { Ok(()) } else { non_constant_error(name, pos) } } } fn is_name_of_enum(lhs: &IRExpr, table: &SymbolTable) -> bool { if let Some(lhs) = lhs.as_plain_name() { if let Some(LocalVar { value_hint: Some(ValueHint::Enum(_)), .. }) = table.get_var(lhs) { // Note: We don't care if the name we're referencing on the enum // is correct or not here. If we're subscripting an enum, then // it's fine. If the name is bad, then the compiler will catch // it in the next phase and we'll throw a much more accurate // `NoSuchEnumValue` (rather than `NotConstantEnough`). return true; } } false } fn non_constant_error(name: &str, pos: SourceOffset) -> Result { Err(GDError::new(GDErrorF::NotConstantEnough(name.to_owned()), pos)) } ================================================ FILE: src/compile/error.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Errors that can occur as a result of GDLisp code. use super::args::{Expecting, ExpectedShape}; use crate::sxp; use crate::sxp::ast::AST; use crate::ir::expr::{Expr as IRExpr}; use crate::ir::arglist::error::{ArgListParseError, ArgListParseErrorF}; use crate::ir::identifier::{Id, Namespace, ClassNamespace}; use crate::ir::decl::DuplicateMainClassError; use crate::ir::import::{ImportDeclParseError, ImportNameResolutionError}; use crate::ir::modifier::{ParseError as ModifierParseError, ParseErrorF as ModifierParseErrorF}; use crate::ir::scope::error::ScopeError; use crate::ir::loops::error::LoopPrimitiveError; use crate::ir::depends::DependencyError; use crate::compile::symbol_table::local_var::VarNameIntoExtendsError; use crate::runner::macro_server::response; use crate::pipeline::source::{SourceOffset, Sourced}; use std::fmt; use std::error::Error; /// This type captures all errors that can occur during compilation of /// GDLisp code. /// /// This type does *not* include I/O errors, which are considered to /// be outside of GDLisp's purview. This type also excludes parsing /// errors, for which LALRPOP provides its own error types. See /// [`crate::pipeline::error`] for an error type which includes this /// one and is more general. #[derive(PartialEq, Eq, Debug)] pub enum GDErrorF { /// A `DottedListError` indicates that a dotted list, such as `(1 2 /// . 3)`, was encountered where a proper list, such as `(1 2 3)`, /// was expecting. `DottedListError` is almost always constructed /// via a [`From`] conversion from /// [`sxp::dotted::TryFromDottedExprError`]. DottedListError, /// An error during parsing of an argument list. The sole argument /// to this constructor stores further information about what went /// wrong. ArgListParseError(ArgListParseError), /// An error during parsing of an import declaration. The sole /// argument to this constructor stores further information about /// what went wrong. ImportDeclParseError(ImportDeclParseError), /// Only a handful of `AST` shapes are valid callable objects, most /// namely symbols but also certain shapes of proper lists. If any /// non-callable `AST` is ever put into a position where it would be /// the head of a function call, then a `CannotCall` error shall be /// issued, with that `AST` as argument. CannotCall(AST), /// A function, macro, special form, or any other callable object /// was called with a different number of arguments than was /// expected. /// /// This error includes the name of the function, the expected /// (minimum and maximum) number of arguments, and the actual number /// of arguments provided. WrongNumberArgs(String, Expecting, usize), /// A function, macro, special form, or any other callable object /// was expecting a value of a particular type or shape but received /// something incompatible. /// /// This error includes the name of the callable object, the faulty /// argument that was passed, and a description of the sort of /// argument that was expected. InvalidArg(String, AST, ExpectedShape), /// A name was referenced in the variable namespace, but no such /// name was found in the symbol table. NoSuchVar(String), /// A name was referenced in the function namespace, but no such /// name was found in the symbol table. NoSuchFn(String), /// An enumeration constant was subscripted, but the subscripted /// name does not exist in the enumeration. /// /// Note that this is a fairly basic check and can be circumvented /// with relative ease. This error is designed to catch the most /// obvious of typos and is not even close to a full static /// typechecker for enumeration values. NoSuchEnumValue(String, String), /// A call magic entity was referenced, but no entity with that name /// exists. /// /// In general, users of GDLisp should not be dealing directly with /// call magic, so if you see this error in real code, please /// consider reporting a bug to the compiler's issue tracker. NoSuchMagic(String), /// An `AST` appeared in declaration context, but its head could not /// be interpreted as a known declaration type. /// /// This error is only produced after any macro expansion is /// attempted, so if you were expecting to call a macro, then /// consider double-checking the name of the macro. UnknownDecl(AST), /// An `unquote` expression appeared outside of a `quasiquote`. UnquoteOutsideQuasiquote, /// An `unquote-spliced` expression appeared outside of a /// `quasiquote`. UnquoteSplicedOutsideQuasiquote, /// An `unquote-spliced` expression appeared within a `quasiquote` /// but in a context where splicing does not make sense, such as in /// the cdr of a dotted list or as a part of a dictionary literal. BadUnquoteSpliced(AST), /// The current preload resolver or pipeline failed to resolve the /// file with the given path. NoSuchFile(String), /// An import declaration includes an explicit named import whose /// namespace is ambiguous. /// /// Normally, when importing particular names in a `use` statement, /// the namespace will be inferred. If either a function or a value /// exists with that name, then the matching namespace will be used /// in either case. However, if *both* a function and a value exist /// with the same name in the target module, then the import is /// ambiguous. Such imports can be disambiguated by specifying the /// namespace explicitly as follows. /// /// ```text /// (use "res://Example.lisp" ((my-example-value value) (my-example-function function))) /// (use "res://Example.lisp" ((my-example-value value as aliased-value) (my-example-function function as aliased-function))) /// ``` AmbiguousNamespace(String), /// The constant variable with the given name was initialized with a /// value that does not meet the GDLisp requirements for a constant. /// /// Note that GDLisp is fairly strict in what is considers a valid /// constant. In particular, if your intention is to import an /// external file, GDScript users would declare a constant whose /// value is initialized to the result of a `preload` call, but in /// GDLisp, we use the special `use` declaration for this. Generally /// speaking, constants should be used for simple atomic values, /// such as numbers and strings, not for aliasing complex data from /// another source. NotConstantEnough(String), /// An attempt was made to assign to a non-assignable variable, such /// as a constant or the special `self` value in a class. CannotAssignTo(String), /// An attempt was made to extend a value which cannot be extended /// as a class. CannotExtend(VarNameIntoExtendsError), /// An `export` declaration was used on a variable in an inner /// class. /// /// `export` declarations only make sense on the main class of a /// file, as they are used for interfacing with existing Godot /// tooling which assumes the class is the main class. ExportOnInnerClassVar(String), /// This error is issued if the type of import declaration was /// incorrect for the given file. Namely, explicit and open import /// lists are only allowed for GDLisp source files, not for GDScript /// source files or other resource types. InvalidImportOnResource(String), /// An error occurred in communication with the Godot macro server. GodotServerError(response::Failure), /// A constructor `_init` function was labeled as static. /// Constructor functions can never be static. StaticConstructor, /// A static method, or other non-instance entity such as a /// constant, was declared on a lambda class (i.e. a class /// constructed in an expression via the `new` special form). /// /// There's no philosophical reason a lambda class cannot have /// static methods or constants, but due to various awkward /// implementation issues, together with the questionable utility of /// such a feature, the feature was outright banned. See Issue #30 /// for more background. StaticOnLambdaClass(String), /// An error during parsing of a modifier or modifier list. The sole /// argument to this constructor stores further information about /// what went wrong. ModifierParseError(ModifierParseError), /// A macro call was encountered during a minimalist compilation. /// /// Generally, users should never be performing minimalist /// compilations on their own files, as this is an advanced feature /// intended for bootstrapping the standard library. As such, if you /// encounter this error, consider reporting a bug to GDLisp. MacroInMinimalistError(String), /// A macro call was encountered before the macro was defined. /// /// While GDLisp functions can be, generally speaking, called from /// before they're defined (subject to some constraints), macros /// are, for deeply technical reasons, only available after the /// point in the file where they've been defined. This error will /// arise if a macro call is performed at a point in the file above /// the point where the `defmacro` is actually written. MacroBeforeDefinitionError(String), /// Two or more main classes were declared in the same file. /// /// A file can have at most one main class. On the GDScript side, /// main classes will be compiled into the overarching class /// representing the file, and all other classes will get compiled /// into inner classes within that main class. If there's no main /// class, then a minimal stub inheriting from `Node` will be filled /// in in GDScript. DuplicateMainClass, /// A `sys/context-filename` call failed to resolve (using the /// current preload resolver) the current filename. /// /// If you ever encounter this error, please report it as a bug. ContextualFilenameUnresolved, /// Two or more constructors were defined in the same class or /// lambda class. DuplicateConstructor, /// The same name was declared twice in the same namespace and scope. DuplicateName(ClassNamespace, String), /// A getter was declared with an invalid argument list or modifier. BadGetterArguments(String), /// A setter was declared with an invalid argument list or modifier. BadSetterArguments(String), /// A field and either a getter or setter with the same name were /// declared in the same scope. FieldAccessorConflict(String), /// A `super` call was attempted in a situation where it makes no /// sense (such as outside of class scope). BadSuperCall(String), /// The "extends" clause for a `defclass` was incorrectly formatted. /// A `defclass` extends clause should be either a singleton list or /// an empty list. BadExtendsClause, /// A clause of `defenum` was incorrectly formatted. A `defenum` /// clause shall be a list of length either 1 or 2, indicating /// either a name or a name together with a value. BadEnumClause, /// The type of a `sys/declare` was invalid. The type of a /// `sys/declare` should be 'value', 'superglobal', 'function', or /// 'superfunction'. BadSysDeclare(String), /// An attempt was made to import a name from another file, but that /// name was not (publicly) declared in that file. UnknownImportedName(Id), /// The same name was declared twice in the same namespace and scope. DuplicateNameConflictInMainClass(ClassNamespace, String), /// An explicit `preload` call was made with a bad path. BadPreloadArgument(String), /// The directive of a `sys/bootstrap` was invalid. BadBootstrappingDirective(String), /// An error in the placement of a loop primitive, i.e. `break` or /// `continue`. LoopPrimitiveError(LoopPrimitiveError), /// An expression was found at the top-level of a file. /// /// In principle, we would like to support expressions at the /// top-level, but as of right now there does not seem to be a way /// to run code when a file in loaded in Godot. So we forbid the /// feature with a specific error message right now but leave all of /// the infrastructure in place so that if such a mechanism is /// found, or is added to Godot in a future version, we can allow it /// easily. ExprAtTopLevel(IRExpr), /// The minimalist flag was set during REPL evaluation. /// /// The minimalist flag is only intended for use in stdlib. It is at /// least sensible in other files, but in the REPL it's absolutely /// meaningless. MinimalistAtRepl, /// A file load was attempted in a cyclic order. CyclicImport(String), /// A constructor `_init` function was labeled as `&sys/nullargs`. NullargsConstructor, } /// Variant of [`GDErrorF`] with source offset information. See /// [`Sourced`]. #[derive(PartialEq, Eq, Debug)] pub struct GDError { pub value: GDErrorF, pub pos: SourceOffset, } const INTERNAL_ERROR_NOTE: &str = "Note: Unless you're doing something really strange, you should probably report this as a compiler bug"; impl GDError { /// Constructs a new error from an `GDErrorF` and a source offset. pub fn new(value: GDErrorF, pos: SourceOffset) -> GDError { GDError { value, pos } } /// Constructs a new error from a value compatible with `GDErrorF` and /// a source offset. pub fn from_value(value: T, pos: SourceOffset) -> GDError where GDErrorF: From { GDError::new(GDErrorF::from(value), pos) } /// Gets the unique identifier of the error shape. See /// [`GDErrorF::error_number`]. pub fn error_number(&self) -> u32 { self.value.error_number() } /// Returns whether the error is internal to GDLisp. See /// [`GDErrorF::is_internal`]. pub fn is_internal(&self) -> bool { self.value.is_internal() } } impl GDErrorF { /// Produces a unique numerical value representing this type of /// error, intended to make identifying the error easier for the /// user. pub fn error_number(&self) -> u32 { match self { // Note: Error code 0 is not used. GDErrorF::DottedListError => 1, GDErrorF::ArgListParseError(err) => { match &err.value { ArgListParseErrorF::InvalidArgument(_) => 2, ArgListParseErrorF::UnknownDirective(_) => 3, ArgListParseErrorF::DirectiveOutOfOrder(_) => 4, ArgListParseErrorF::SimpleArgListExpected => 5, ArgListParseErrorF::BadSelf(_) => 53, ArgListParseErrorF::SimpleArgExpected => 54, ArgListParseErrorF::ConstructorArgListExpected => 55, } } GDErrorF::ImportDeclParseError(err) => { match err { ImportDeclParseError::NoFilename => 6, ImportDeclParseError::BadFilename(_) => 7, ImportDeclParseError::InvalidPath(_) => 8, ImportDeclParseError::MalformedFunctionImport(_) => 9, ImportDeclParseError::InvalidEnding(_) => 10, } } GDErrorF::CannotCall(_) => 11, GDErrorF::WrongNumberArgs(_, _, _) => 12, GDErrorF::InvalidArg(_, _, _) => 13, GDErrorF::NoSuchVar(_) => 14, GDErrorF::NoSuchFn(_) => 15, GDErrorF::NoSuchEnumValue(_, _) => 16, GDErrorF::NoSuchMagic(_) => 17, GDErrorF::UnknownDecl(_) => 18, // NOTE: 19 belongs to the removed InvalidDecl, which has been // split into several different error types: InvalidArg, // BadExtendsClause, BadEnumClause, and BadSysDeclare. GDErrorF::UnquoteOutsideQuasiquote => 20, GDErrorF::UnquoteSplicedOutsideQuasiquote => 21, GDErrorF::BadUnquoteSpliced(_) => 22, GDErrorF::NoSuchFile(_) => 23, GDErrorF::AmbiguousNamespace(_) => 24, GDErrorF::NotConstantEnough(_) => 25, GDErrorF::CannotAssignTo(_) => 26, GDErrorF::CannotExtend(_) => 27, GDErrorF::ExportOnInnerClassVar(_) => 28, // NOTE: 29 belongs to the removed ResourceDoesNotExist, which // has been replaced by NoSuchFile and InvalidImportOnResource. GDErrorF::InvalidImportOnResource(_) => 30, GDErrorF::GodotServerError(_) => 31, GDErrorF::StaticConstructor => 32, GDErrorF::StaticOnLambdaClass(_) => 33, GDErrorF::ModifierParseError(err) => { match &err.value { ModifierParseErrorF::UniquenessError(_) => 34, ModifierParseErrorF::Expecting(_, _) => 35, ModifierParseErrorF::ExhaustedAlternatives => 36, } } GDErrorF::MacroInMinimalistError(_) => 37, GDErrorF::MacroBeforeDefinitionError(_) => 38, GDErrorF::DuplicateMainClass => 39, GDErrorF::ContextualFilenameUnresolved => 40, GDErrorF::DuplicateConstructor => 41, GDErrorF::DuplicateName(_, _) => 42, GDErrorF::BadGetterArguments(_) => 43, GDErrorF::BadSetterArguments(_) => 44, GDErrorF::FieldAccessorConflict(_) => 45, GDErrorF::BadSuperCall(_) => 46, GDErrorF::BadExtendsClause => 47, GDErrorF::BadEnumClause => 48, GDErrorF::BadSysDeclare(_) => 49, GDErrorF::UnknownImportedName(_) => 50, GDErrorF::DuplicateNameConflictInMainClass(_, _) => 51, GDErrorF::BadPreloadArgument(_) => 52, // NOTE: 53 is ArgListParseErrorF::BadSelf above. // // NOTE: 54 is ArgListParseErrorF::SimpleArgExpected above. // // NOTE: 55 is ArgListParseErrorF::ConstructorArgListExpected above. GDErrorF::BadBootstrappingDirective(_) => 56, GDErrorF::LoopPrimitiveError(_) => 57, GDErrorF::ExprAtTopLevel(_) => 58, GDErrorF::MinimalistAtRepl => 59, GDErrorF::CyclicImport(_) => 60, GDErrorF::NullargsConstructor => 61, } } /// Returns whether or not the error is an internal GDLisp error. /// Internal GDLisp errors are those that the compiler emits which /// should generally never be seen by users. If a user encounters /// such an error, it is probably a bug in the GDLisp compiler, and /// such errors are displayed with a disclaimer indicating as much. pub fn is_internal(&self) -> bool { #[allow(clippy::match_like_matches_macro)] // Suggested alternative is quite lengthy on one line match self { GDErrorF::NoSuchMagic(_) => true, GDErrorF::MacroInMinimalistError(_) => true, GDErrorF::ContextualFilenameUnresolved => true, GDErrorF::BadSysDeclare(_) => true, GDErrorF::BadBootstrappingDirective(_) => true, GDErrorF::MinimalistAtRepl => true, GDErrorF::NullargsConstructor => true, _ => false, } } } impl fmt::Display for GDErrorF { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Error {:04}: ", self.error_number())?; match self { GDErrorF::DottedListError => { write!(f, "Unexpected dotted list")?; } GDErrorF::ArgListParseError(err) => { write!(f, "Error parsing argument list: {}", err)?; } GDErrorF::ImportDeclParseError(err) => { write!(f, "Error parsing import declaration: {}", err)?; } GDErrorF::CannotCall(ast) => { write!(f, "Cannot make function call on expression {}", ast)?; } GDErrorF::WrongNumberArgs(name, expected, actual) => { write!(f, "Wrong number of arguments to call {}: expected {}, got {}", name, expected, actual)?; } GDErrorF::InvalidArg(name, provided, expected) => { write!(f, "Invalid argument to {}, given {}, expecting {}", name, provided, expected)?; } GDErrorF::NoSuchVar(name) => { write!(f, "No such variable {}", name)?; } GDErrorF::NoSuchFn(name) => { write!(f, "No such function {}", name)?; } GDErrorF::NoSuchEnumValue(name, subname) => { write!(f, "No such enum value {}:{}", name, subname)?; } GDErrorF::NoSuchMagic(name) => { write!(f, "No such call magic {}", name)?; } GDErrorF::UnknownDecl(ast) => { write!(f, "Unknown declaration {}", ast)?; } GDErrorF::UnquoteOutsideQuasiquote => { write!(f, "Unquote (,) can only be used inside quasiquote (`)")?; } GDErrorF::UnquoteSplicedOutsideQuasiquote => { write!(f, "Spliced unquote (,.) can only be used inside quasiquote (`)")?; } GDErrorF::BadUnquoteSpliced(ast) => { write!(f, "Spliced unquote (,.) does not make sense in this context: {}", ast)?; } GDErrorF::NoSuchFile(p) => { write!(f, "Cannot locate file {}", p)?; } GDErrorF::AmbiguousNamespace(s) => { write!(f, "Ambiguous namespace when importing {}", s)?; } GDErrorF::NotConstantEnough(s) => { write!(f, "Expression for constant declaration {} is not constant enough", s)?; } GDErrorF::CannotAssignTo(s) => { write!(f, "Cannot assign to immutable variable {}", s)?; } GDErrorF::CannotExtend(err) => { write!(f, "{}", err)?; } GDErrorF::ExportOnInnerClassVar(v) => { write!(f, "Export declarations can only be used on a file's main class, but one was found on {}", v)?; } GDErrorF::InvalidImportOnResource(s) => { write!(f, "Cannot use restricted or open import lists on resource import at {}", s)?; } GDErrorF::GodotServerError(err) => { write!(f, "Error during Godot server task execution (error code {}): {}", err.error_code, err.error_string)?; } GDErrorF::StaticConstructor => { write!(f, "Class constructors cannot be static")?; } GDErrorF::StaticOnLambdaClass(s) => { write!(f, "Static name {} is not allowed on anonymous class instance", s)?; } GDErrorF::ModifierParseError(m) => { write!(f, "Modifier error: {}", m)?; } GDErrorF::MacroInMinimalistError(m) => { write!(f, "Attempt to expand macro {} in minimalist file", m)?; } GDErrorF::MacroBeforeDefinitionError(m) => { write!(f, "Attempt to use macro {} before definition was available", m)?; } GDErrorF::DuplicateMainClass => { write!(f, "File has two main classes")?; // TODO Would be nice to have the source offset of the *original* main class here as well. } GDErrorF::ContextualFilenameUnresolved => { write!(f, "Could not resolve contextual filename of current file")?; } GDErrorF::DuplicateConstructor => { write!(f, "Class has two constructors")?; // TODO Would be nice to have the source offset of the *original* constructor here as well. } GDErrorF::DuplicateName(ns, name) => { write!(f, "The {} '{}' was already declared in this scope", ns.name(), name)?; // TODO Would be nice to have the source offset of the *original* name here as well. } GDErrorF::BadGetterArguments(field_name) => { write!(f, "The getter '{}' has a bad signature; getters must be 0-ary non-static functions", field_name)?; } GDErrorF::BadSetterArguments(field_name) => { write!(f, "The setter '{}' has a bad signature; getters must be 1-ary non-static functions", field_name)?; } GDErrorF::FieldAccessorConflict(field_name) => { write!(f, "The value '{}' was declared as both an instance variable and either a getter or a setter", field_name)?; } GDErrorF::BadSuperCall(super_method) => { write!(f, "Cannot call superclass method '{}'; there is no superclass in the current scope", super_method)?; } GDErrorF::BadExtendsClause => { write!(f, "Bad 'extends' clause; a class 'extends' clause should either be the empty list or a singleton list")?; } GDErrorF::BadEnumClause => { write!(f, "Bad 'defenum' clause; expected a list of one or two elements")?; } GDErrorF::BadSysDeclare(v) => { write!(f, "Bad 'sys/declare' type; expected 'value', 'superglobal', 'function', or 'superfunction', got {}", v)?; } GDErrorF::UnknownImportedName(id) => { write!(f, "Unknown {} name '{}' in import", id.namespace.name(), &id.name)?; } GDErrorF::DuplicateNameConflictInMainClass(ns, name) => { write!(f, "The {} '{}' was declared at the top-level and is also declared in the main class", ns.name(), name)?; // TODO Would be nice to have the source offset of the *original* name here as well. } GDErrorF::BadPreloadArgument(name) => { write!(f, "Argument to 'preload' must be a valid resource path, got '{}'", name)?; } GDErrorF::BadBootstrappingDirective(name) => { write!(f, "Bad directive to sys/bootstrap, got '{}'", name)?; } GDErrorF::LoopPrimitiveError(err) => { write!(f, "{}", err)?; } GDErrorF::ExprAtTopLevel(_) => { write!(f, "Expressions at the top-level of a file are not allowed")?; } GDErrorF::MinimalistAtRepl => { write!(f, "Minimalist flag makes no sense at the REPL")?; } GDErrorF::CyclicImport(filename) => { write!(f, "Attempted to load '{}' while it was being loaded (cyclic import)", filename)?; } GDErrorF::NullargsConstructor => { write!(f, "Class constructors cannot be marked sys/nullargs")?; } } if self.is_internal() { write!(f, " ({})", INTERNAL_ERROR_NOTE)?; } Ok(()) } } impl fmt::Display for GDError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", &self.value) } } impl Error for GDError { fn source(&self) -> Option<&(dyn Error + 'static)> { match &self.value { GDErrorF::ArgListParseError(err) => { Some(err) } GDErrorF::ImportDeclParseError(err) => { Some(err) } GDErrorF::ModifierParseError(err) => { Some(err) } GDErrorF::LoopPrimitiveError(err) => { Some(err) } _ => { None } } } } impl Sourced for GDError { type Item = GDErrorF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &GDErrorF { &self.value } } impl From for GDErrorF { fn from(_: sxp::dotted::TryFromDottedExprError) -> GDErrorF { GDErrorF::DottedListError } } impl From for GDError { fn from(err: sxp::dotted::TryFromDottedExprError) -> GDError { GDError::new(GDErrorF::DottedListError, err.pos) } } impl From for GDErrorF { fn from(err: ArgListParseError) -> GDErrorF { GDErrorF::ArgListParseError(err) } } impl From for GDError { fn from(err: ArgListParseError) -> GDError { let pos = err.pos; GDError::new(GDErrorF::from(err), pos) } } impl From for GDErrorF { fn from(err: ImportDeclParseError) -> GDErrorF { GDErrorF::ImportDeclParseError(err) } } impl From for GDErrorF { fn from(err: ImportNameResolutionError) -> GDErrorF { match err { ImportNameResolutionError::UnknownName(id) => { GDErrorF::UnknownImportedName(id) } ImportNameResolutionError::AmbiguousNamespace(s) => { GDErrorF::AmbiguousNamespace(s) } } } } impl From for GDErrorF { fn from(err: VarNameIntoExtendsError) -> GDErrorF { GDErrorF::CannotExtend(err) } } impl From for GDErrorF { fn from(err: response::Failure) -> GDErrorF { GDErrorF::GodotServerError(err) } } impl From for GDErrorF { fn from(err: ModifierParseError) -> GDErrorF { GDErrorF::ModifierParseError(err) } } impl From for GDError { fn from(err: ModifierParseError) -> GDError { let pos = err.pos; GDError::new(GDErrorF::from(err), pos) } } impl From for GDError { fn from(err: DuplicateMainClassError) -> GDError { GDError::new(GDErrorF::DuplicateMainClass, err.0) } } impl From> for GDError { fn from(err: ScopeError) -> GDError { match err { ScopeError::DuplicateName(n, s, p) => GDError::new(GDErrorF::DuplicateName(n, s), p), ScopeError::NameConflictWithMainClass(n, s, p) => GDError::new(GDErrorF::DuplicateNameConflictInMainClass(n, s), p), } } } impl From> for GDError { fn from(err: ScopeError) -> GDError { GDError::from( ScopeError::::from(err), ) } } impl From for GDError { fn from(e: DependencyError) -> GDError { match e { DependencyError::UnknownName(id, pos) => { let error_value = match id.namespace { Namespace::Function => GDErrorF::NoSuchFn(id.name), Namespace::Value => GDErrorF::NoSuchVar(id.name), }; GDError::new(error_value, pos) } } } } impl From for GDError { fn from(err: LoopPrimitiveError) -> GDError { let pos = err.pos; GDError::new( GDErrorF::LoopPrimitiveError(err), pos, ) } } ================================================ FILE: src/compile/factory.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Factory functions for building and working with some common //! declaration types. use super::import::ImportTable; use super::frame::CompilerFrame; use super::stateful::NeedsResult; use super::names; use super::names::generator::NameGenerator; use super::names::registered::RegisteredNameGenerator; use super::body::builder::{StmtBuilder, CodeBuilder, HasDecls}; use super::body::class_initializer::ClassBuilder; use super::body::class_scope::DirectClassScope; use super::stmt_wrapper::{self, StmtWrapper}; use super::symbol_table::{HasSymbolTable, ClassTablePair}; use super::symbol_table::local_var::LocalVar; use super::symbol_table::function_call::OuterStaticRef; use super::error::GDError; use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::gdscript::decl::{self, Decl, DeclF}; use crate::gdscript::class_extends::ClassExtends; use crate::gdscript::library; use crate::gdscript::inner_class::{self, NeedsOuterClassRef}; use crate::pipeline::source::SourceOffset; use crate::ir; use crate::ir::access_type::AccessType; use crate::ir::arglist::simple::SimpleArgList; type IRLiteral = ir::literal::Literal; type IRExpr = ir::expr::Expr; type IRArgList = ir::arglist::ordinary::ArgList; /// This is the structure returned by [`declare_function_with_init`]. struct DeclaredFnWithInit { function: decl::FnDecl, inits: Vec, } /// Appends (to the builder) a variable declaration statement. /// /// The variable will have a generated name produced by `gen`. The /// generated name is returned. pub fn declare_var(gen: &mut impl NameGenerator, builder: &mut StmtBuilder, prefix: &str, value: Option, pos: SourceOffset) -> String { let var_name = gen.generate_with(prefix); let value = value.unwrap_or_else(|| Expr::null(pos)); builder.append(Stmt::var_decl(var_name.clone(), value, pos)); var_name } /// As far as the IR is concerned, main classes and other classes are /// roughly equivalent in functionality, except that the former can /// have variables with custom export declarations. When we compile /// classes to GDScript, non-main classes compile to inner classes on /// the script (i.e. to a /// [`ClassDecl`](crate::gdscript::decl::ClassDecl)), while main /// classes flatten all of their declarations (and "extends" clauses) /// into the main content of the file itself. This function implements /// the latter behavior. pub fn flatten_class_into_main(import_table: &ImportTable, builder: &mut CodeBuilder, class: decl::ClassDecl) { let decl::ClassDecl { name: _, mut extends, body } = class; transform_extends_path_for_main(import_table, &mut extends); builder.extends(extends); for decl in body { builder.add_decl(decl); } } fn transform_extends_path_for_main(import_table: &ImportTable, extends: &mut ClassExtends) { match extends { ClassExtends::Qualified(lhs, _) => { transform_extends_path_for_main(import_table, lhs); } ClassExtends::SimpleIdentifier(name) => { // If the name is an import name, convert it *back* to its path // name, since we can't access import names at this point in the // code. if let Some(path) = import_table.get(name) { *extends = ClassExtends::StringLit(path.to_owned()); } } ClassExtends::StringLit(_) => { // Already a path, so do nothing. } } } pub fn declare_function(frame: &mut CompilerFrame, gd_name: String, args: IRArgList, body: &IRExpr, result_destination: &(impl StmtWrapper + ?Sized)) -> Result { let result = declare_function_with_init(frame, gd_name, args, &[], body, result_destination)?; assert!(result.inits.is_empty(), "declare_function got nonempty initializers: {:?}", result.inits); Ok(result.function) } fn declare_function_with_init(frame: &mut CompilerFrame, gd_name: String, args: IRArgList, inits: &[IRExpr], body: &IRExpr, result_destination: &(impl StmtWrapper + ?Sized)) -> Result { let local_vars = body.get_locals(); let (arglist, gd_args) = args.into_gd_arglist(&mut RegisteredNameGenerator::new_local_var(frame.table)); let mut stmt_builder = StmtBuilder::new(); for arg in &gd_args { if local_vars.get(&arg.lisp_name).unwrap_or(&AccessType::None).requires_cell() { // Special behavior to wrap the argument in a cell. library::cell::wrap_var_in_cell(&mut stmt_builder, &arg.gd_name, body.pos) } } frame.with_local_vars(&mut gd_args.into_iter().map(|x| (x.lisp_name.to_owned(), LocalVar::local(x.gd_name, *local_vars.get(&x.lisp_name).unwrap_or(&AccessType::None)))), |frame| { frame.with_builder(&mut stmt_builder, |frame| { frame.compile_stmt(result_destination, body) })?; let function = decl::FnDecl { name: gd_name, args: arglist, body: stmt_builder.build_into(frame.builder), }; let inits = inits.iter().map::, _>(|expr| { let mut inner_builder = StmtBuilder::new(); let cexpr = frame.with_builder(&mut inner_builder, |frame| { frame.compile_expr(expr, NeedsResult::Yes).map(|x| x.expr) })?; let (stmts, decls) = inner_builder.build(); if stmts.is_empty() { // If we never used the stmts, then it's a simple enough // expression to compile directly. frame.builder.add_decls(decls); Ok(cexpr) } else { // We used accessory statements, so we need an IIFE to get // to a place where we can compile statements. let mut new_inner_builder = StmtBuilder::new(); let cexpr = frame.with_builder(&mut new_inner_builder, |frame| { frame.compile_expr(&expr.clone().self_evaluating_lambda(), NeedsResult::Yes).map(|x| x.expr) })?; let (stmts, decls) = new_inner_builder.build(); assert!(stmts.is_empty(), "Non-empty statements in self-evaluating lambda at declare_function_with_init: got {:?}", stmts); frame.builder.add_decls(decls); Ok(cexpr) } }).collect::, _>>()?; Ok(DeclaredFnWithInit { function, inits }) }) } pub fn declare_class(frame: &mut CompilerFrame, gd_name: String, extends: ClassExtends, main_class: bool, constructor: &ir::decl::ConstructorDecl, decls: &[ir::decl::ClassInnerDecl], pos: SourceOffset) -> Result { let mut class_scope = DirectClassScope::new(); let self_var = LocalVar::self_var(); let mut body = vec!(); let mut outer_ref_name = String::new(); let needs_outer_ref = constructor.needs_outer_class_ref(frame.table) || decls.iter().any(|x| x.needs_outer_class_ref(frame.table)); if needs_outer_ref && !main_class { outer_ref_name = frame.name_generator().generate_with(inner_class::OUTER_REFERENCE_NAME); } let mut class_init_builder = ClassBuilder::new(); let mut instance_table = frame.table.clone(); let mut static_table = frame.table.clone(); instance_table.with_local_var::, _>(String::from("self"), self_var, |instance_table| { // Modify all of the names in the instance / static table. We run // this even if needs_outer_ref is false, because we might still // need the static_table updates, and the instance_table updates // will be harmlessly ignored in that case. if !main_class { for (_, call, _) in instance_table.fns_mut() { call.object.update_for_inner_scope(&OuterStaticRef::InnerInstanceVar(&outer_ref_name), frame.preload_resolver(), frame.pipeline); } for (_, call, _) in static_table.fns_mut() { call.object.update_for_inner_scope(&OuterStaticRef::InnerStatic, frame.preload_resolver(), frame.pipeline); } } let (constructor, constructor_helpers) = declare_constructor(&mut CompilerFrame::new(frame.compiler, frame.pipeline, frame.builder, instance_table, &mut class_scope), constructor)?; body.push(Decl::new(DeclF::InitFnDecl(constructor), pos)); for helper in constructor_helpers { body.push(Decl::new(DeclF::FnDecl(decl::Static::NonStatic, helper), pos)); } for d in decls { // Do the actual declaration. let tables = ClassTablePair { instance_table, static_table: &mut static_table }; body.push(frame.compiler.compile_class_inner_decl(frame.pipeline, &mut class_init_builder, tables, &mut class_scope, d)?); // Sync up the instance and static tables' synthetic variables. // (TODO (HACK) Can we make the tables sync up properly, rather // than just blindly throwing information at each other at every // turn?) instance_table.dump_synthetics_to(&mut static_table); static_table.dump_synthetics_to(instance_table); } // The instance and static tables should have the same synthetics, // so dump one into the parent. instance_table.dump_synthetics_to(frame.table); Ok(()) })?; let mut decl = decl::ClassDecl { name: gd_name, extends: extends, body: body, }; if needs_outer_ref && !main_class { inner_class::add_outer_class_ref_named(&mut decl, frame.preload_resolver(), frame.pipeline, outer_ref_name, pos); } class_init_builder.declare_proxies_from_scope(class_scope); class_init_builder.fill_out_synthetic_fields(&mut decl, pos); let class_init = class_init_builder.build_into(frame.builder); class_init.apply(&mut decl, pos)?; Ok(decl) } pub fn declare_constructor(frame: &mut CompilerFrame, constructor: &ir::decl::ConstructorDecl) -> Result<(decl::InitFnDecl, Vec), GDError> { let pos = constructor.body.pos; // First, we save which original arguments which are instance // fields. let has_any_instance_field_args = constructor.args.has_any_instance_fields(); let instance_fields_to_init: Vec<(String, bool)> = constructor.args.iter_vars() .map(|(name, is_instance_field)| (name.to_owned(), is_instance_field)) .collect(); let simple_arglist = SimpleArgList { args: constructor.args.args.iter().map(|(name, is_instance_field)| { if *is_instance_field { frame.compiler.name_generator().generate_with(&names::lisp_to_gd(name)) } else { name.to_owned() } }).collect() }; let constructor_with_init = declare_function_with_init(frame, String::from(library::CONSTRUCTOR_NAME), IRArgList::from(simple_arglist), &constructor.super_call.call, &constructor.body, &stmt_wrapper::Vacuous)?; let DeclaredFnWithInit { function: constructor, inits } = constructor_with_init; let decl::FnDecl { name: _, args, mut body } = constructor; // Handle the instance field initialization if has_any_instance_field_args { let mut new_body: Vec = Vec::new(); let all_args: Vec<&str> = args.all_args_iter().collect(); // It should be the case that the argument list we put in at the // beginning should have the same length as the one we got out at // the end. I'm asserting that here, because if I change the // calling convention somewhere down the line and this assumption // breaks, I want to know. assert!(all_args.len() == instance_fields_to_init.len(), "Calling convention not compatible with factory.rs assumptions"); for (arg_name, (field_name, is_instance_field)) in all_args.into_iter().zip(instance_fields_to_init.into_iter()) { if is_instance_field { let field_name = names::lisp_to_gd(&field_name); new_body.push(make_instance_var_initializer(&field_name, arg_name, pos)); } } new_body.append(&mut body); body = new_body; } let constructor = decl::InitFnDecl { args, super_call: inits, body }; Ok((constructor, vec!())) } /// Returns a statement which assigns the value of the given local /// variable to an instance variable with the given name. fn make_instance_var_initializer(instance_var_name: &str, local_var_name: &str, pos: SourceOffset) -> Stmt { Stmt::simple_assign( Expr::self_var(pos).attribute(instance_var_name, pos), Expr::var(local_var_name, pos), pos, ) } /// Compiles a GDLisp literal to a GDScript expression, using `pos` as /// the source offset of the resulting expression. Literals are simple /// expressions and, as such, this transformation cannot fail. pub fn compile_literal(literal: &IRLiteral, pos: SourceOffset) -> Expr { match literal { IRLiteral::Nil => Expr::null(pos), IRLiteral::Int(n) => Expr::from_value(*n, pos), IRLiteral::Float(f) => Expr::from_value(*f, pos), IRLiteral::Bool(b) => Expr::from_value(*b, pos), IRLiteral::String(s) => Expr::from_value(s.to_owned(), pos), IRLiteral::Symbol(s) => Expr::call(Some(library::gdlisp_root(pos)), "intern", vec!(Expr::from_value(s.to_owned(), pos)), pos), } } ================================================ FILE: src/compile/frame.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! A compiler frame contains a [`Compiler`], as well as information //! about the current stack frame data. use super::Compiler; use super::factory; use super::special_form; use super::names; use super::constant::validate_all_constant_scopes; use super::special_form::lambda; use super::special_form::flet; use super::special_form::lambda_class; use super::special_form::let_block; use super::preload_resolver::{PreloadResolver, DefaultPreloadResolver}; use super::symbol_table::{SymbolTable, HasSymbolTable}; use super::symbol_table::local_var::{LocalVar, ValueHint, VarName}; use super::names::fresh::FreshNameGenerator; use super::names::registered::RegisteredNameGenerator; use super::error::{GDError, GDErrorF}; use super::stateful::{StExpr, NeedsResult, SideEffects}; use super::body::builder::{StmtBuilder, CodeBuilder, HasDecls}; use super::body::class_scope::ClassScope; use super::stmt_wrapper::{self, StmtWrapper}; use crate::pipeline::Pipeline; use crate::pipeline::error::PError; use crate::pipeline::source::SourceOffset; use crate::ir; use crate::ir::expr::{FuncRefTarget, AssignTarget, BareName, CallTarget, FunctionBindingType}; use crate::ir::special_ref::SpecialRef; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::stmt::Stmt; use crate::gdscript::decl::{self, Decl, DeclF}; use crate::gdscript::inner_class; use crate::gdscript::metadata; use crate::sxp::reify::pretty::reify_pretty_expr; use crate::runner::path::RPathBuf; use std::cmp::max; use std::convert::TryFrom; type IRExpr = ir::expr::Expr; type IRExprF = ir::expr::ExprF; type IRDecl = ir::decl::Decl; type IRDeclF = ir::decl::DeclF; type IRArgList = ir::arglist::ordinary::ArgList; /// Quoted S-expressions which are nested deeper than this constant /// will be split into several local variables, for efficiency /// reasons. See [Godot /// #52113](https://github.com/godotengine/godot/issues/52113) for the /// reason this is necessary. This constant is passed to /// [`reify_pretty_expr`]. pub const MAX_QUOTE_REIFY_DEPTH: u32 = 4; /// A `CompilerFrame` contains references to all of the pertinent /// information about a particular frame (hence, scope) of a GDScript /// body during compilation. /// /// `CompilerFrame` never takes ownership of any of its fields; it /// always mutably borrows every field. The type parameter `B` is the /// type of the builder. This structure can be used in declaration /// context /// ([`CodeBuilder`](crate::compile::body::builder::CodeBuilder)) or in /// statement context /// ([`StmtBuilder`](crate::compile::body::builder::StmtBuilder)), and /// the type of the builder determines which functionality is /// available. /// /// Note that several of the methods on this structure are /// conditionally available, based on the type of `B`. There are three /// categories of methods. /// /// * Some methods are always available, for all compiler frames. /// /// * Some methods are available whenever `B` implements [`HasDecls`]. /// /// * Some methods are only available when `B` is the concrete type /// [`StmtBuilder`]. pub struct CompilerFrame<'a, 'b, 'c, 'd, 'e, B> { /// The compiler for the file. The compiler is mutable, but it is /// unlikely to be completely replaced in local scopes. pub compiler: &'a mut Compiler, /// The pipeline for the compilation. The pipeline is mutable, but /// it is unlikely to be completely replaced in local scopes. pub pipeline: &'b mut Pipeline, /// The builder for the current block of code, whose type is /// parameterized by the structure's `B` type parameter. /// /// Generally, this is one of [`StmtBuilder`] (if building /// statements), [`CodeBuilder`](super::body::builder::CodeBuilder) /// (if building declarations), or `()` (if not using the /// builder argument), though there is no specific requirement that /// the type `B` implement any particular trait in general. /// /// The builder is replaced using calls to /// [`CompilerFrame::with_builder`] and company. pub builder: &'c mut B, /// The symbol table for the current scope. `CompilerFrame` /// implements [`HasSymbolTable`], so methods such as /// [`HasSymbolTable::with_local_var`] will work on `CompilerFrame`. pub table: &'d mut SymbolTable, /// The scope for the class we're currently conceptually inside of, /// including information about whether we're in a closure. This is /// important, because when we're in a closure, we're still /// conceptually within the class as far as GDLisp is concerned, but /// we're compiling to something outside of the class, so special /// care needs to be taken when referencing information on the /// current class. pub class_scope: &'e mut dyn ClassScope, } impl<'a, 'b, 'c, 'd, 'e, B> CompilerFrame<'a, 'b, 'c, 'd, 'e, B> { /// Convenience function to construct a `CompilerFrame`. pub fn new(compiler: &'a mut Compiler, pipeline: &'b mut Pipeline, builder: &'c mut B, table: &'d mut SymbolTable, class_scope: &'e mut dyn ClassScope) -> Self { CompilerFrame { compiler, pipeline, builder, table, class_scope } } /// Gets the [`FreshNameGenerator`] from the frame's [`Compiler`]. pub fn name_generator(&mut self) -> &mut FreshNameGenerator { self.compiler.name_generator() } /// Gets the [`PreloadResolver`] from the frame's [`Compiler`]. pub fn preload_resolver(&self) -> &dyn PreloadResolver { self.compiler.preload_resolver() } /// Constructs a new [`CompilerFrame`] identical to `self` except /// with `new_builder` as its builder. The new frame is passed to /// `block` as its sole argument, and the result of the block is /// returned. pub fn with_builder(&mut self, new_builder: &mut B1, block: F) -> R where F : FnOnce(&mut CompilerFrame) -> R { let mut new_frame = CompilerFrame::new(self.compiler, self.pipeline, new_builder, self.table, self.class_scope); block(&mut new_frame) } /// Compiles the expression `expr`, as though through /// [`CompilerFrame::compile_expr`]. /// /// This method constructs a temporary [`StmtBuilder`] for use in /// the compilation. If the temporary builder ends up being used /// (i.e. if the compilation constructs any intermediate statements /// or declarations that require its use), then this method will /// return an [`GDErrorF::NotConstantEnough`] error. If the /// temporary builder goes unused, then the resulting [`Expr`] is /// returned. pub fn compile_simple_expr(&mut self, src_name: &str, expr: &IRExpr, needs_result: NeedsResult) -> Result { let mut tmp_builder = StmtBuilder::new(); let value = self.with_builder(&mut tmp_builder, |frame| { frame.compile_expr(expr, needs_result).map(|x| x.expr) })?; let (stmts, decls) = tmp_builder.build(); if stmts.is_empty() && decls.is_empty() { Ok(value) } else { Err(GDError::new(GDErrorF::NotConstantEnough(String::from(src_name)), expr.pos)) } } } impl<'a, 'b, 'c, 'd, 'e, B: HasDecls> CompilerFrame<'a, 'b, 'c, 'd, 'e, B> { /// This method allows a block of code to run, given access to a /// compiler frame identical to `self` but with a new /// [`StmtBuilder`]. /// /// Specifically, this method constructs a new, empty `StmtBuilder`, /// then runs the block using that builder via /// [`CompilerFrame::with_builder`]. At the end of the block, the /// new local builder will be built into (via /// [`StmtBuilder::build_into`]) the outer builder `self.builder`. /// Finally, the result of the block and all of the statements built /// using the local builder are returned. pub fn with_local_builder_value(&mut self, block: F) -> (R, Vec) where F : FnOnce(&mut CompilerFrame) -> R { let mut local_builder = StmtBuilder::new(); let result = self.with_builder(&mut local_builder, block); let stmts = local_builder.build_into(self.builder); (result, stmts) } /// As /// [`with_local_builder_value`](CompilerFrame::with_local_builder_value), /// but the block is expected to return a [`Result`], /// whose error values are propagated outward to the return value of /// this method. pub fn with_local_builder_result(&mut self, block: F) -> Result<(R, Vec), GDError> where F : FnOnce(&mut CompilerFrame) -> Result { let (error_value, vec) = self.with_local_builder_value(block); error_value.map(|r| (r, vec)) } /// As /// [`with_local_builder_result`](CompilerFrame::with_local_builder_result), /// but with no return value `R`. Only the vector of statements from /// the builder is returned. Equivalent to /// `self.with_local_builder_result(block).map(|x| x.1)`. pub fn with_local_builder(&mut self, block: F) -> Result, GDError> where F : FnOnce(&mut CompilerFrame) -> Result<(), GDError> { let (error_value, vec) = self.with_local_builder_value(block); error_value.map(|_| vec) } /// As /// [`with_local_builder_value`](CompilerFrame::with_local_builder_value), /// but with no return value `R`. Only the vector of statements from /// the builder is returned. Equivalent to /// `self.with_local_builder_value(block).1`. pub fn with_local_builder_ok(&mut self, block: F) -> Vec where F : FnOnce(&mut CompilerFrame) { let ((), vec) = self.with_local_builder_value(block); vec } } impl<'a, 'b, 'c, 'd, 'e> CompilerFrame<'a, 'b, 'c, 'd, 'e, CodeBuilder> { pub fn compile_toplevel(&mut self, toplevel: &ir::decl::TopLevel) -> Result<(), PError> { // Special check to make sure there is only one main class. let _ = toplevel.find_main_class()?; for imp in &toplevel.imports { self.compiler.resolve_import(self.pipeline, self.builder, self.table, imp)?; } self.compile_decls(&toplevel.decls)?; Ok(()) } pub fn compile_decls(&mut self, decls: &[IRDecl]) -> Result<(), GDError> { // Bind all declarations into the symbol table. for decl in decls { Compiler::bind_decl(&self.compiler.magic_table, self.pipeline, self.table, decl)?; } // Validate the const-ness of any constants, enum values, or // `export` clause bodies. validate_all_constant_scopes(decls, self.table)?; // Now compile. for decl in decls { self.table.clear_synthetic_locals(); self.compile_decl(decl)?; } Ok(()) } pub fn compile_decl(&mut self, decl: &IRDecl) -> Result<(), GDError> { match &decl.value { IRDeclF::FnDecl(ir::decl::FnDecl { visibility: _, call_magic: _, name, args, body }) => { let gd_name = names::lisp_to_gd(name); let function = factory::declare_function(self, gd_name, args.clone(), body, &stmt_wrapper::Return)?; self.builder.add_decl(Decl::new(DeclF::FnDecl(decl::Static::IsStatic, function), decl.pos)); Ok(()) } IRDeclF::MacroDecl(ir::decl::MacroDecl { visibility: _, name, args, body }) => { // Note: Macros compile identically to functions, as far as // this stage of compilation is concerned. They'll be resolved // and then purged during the IR phase. let gd_name = names::lisp_to_gd(name); let function = factory::declare_function(self, gd_name, args.clone(), body, &stmt_wrapper::Return)?; self.builder.add_decl(Decl::new(DeclF::FnDecl(decl::Static::IsStatic, function), decl.pos)); Ok(()) } IRDeclF::SymbolMacroDecl(ir::decl::SymbolMacroDecl { visibility: _, name, body }) => { // Note: Macros compile identically to functions, as far as // this stage of compilation is concerned. They'll be resolved // and then purged during the IR phase. let gd_name = metadata::symbol_macro(&names::lisp_to_gd(name)); let function = factory::declare_function(self, gd_name, IRArgList::empty(), body, &stmt_wrapper::Return)?; self.builder.add_decl(Decl::new(DeclF::FnDecl(decl::Static::IsStatic, function), decl.pos)); Ok(()) } IRDeclF::ConstDecl(ir::decl::ConstDecl { visibility: _, name, value }) => { let gd_name = names::lisp_to_gd(name); let value = self.compile_simple_expr(name, value, NeedsResult::Yes)?; self.builder.add_decl(Decl::new(DeclF::ConstDecl(gd_name, value), decl.pos)); Ok(()) } IRDeclF::ClassDecl(ir::decl::ClassDecl { visibility: _, name, extends, main_class, constructor, decls }) => { let gd_name = names::lisp_to_gd(name); let extends = Compiler::resolve_extends(self.table, extends, decl.pos)?; // Synthesize default constructor if needed let default_constructor: ir::decl::ConstructorDecl; let constructor = match constructor { None => { default_constructor = ir::decl::ConstructorDecl::empty(decl.pos); &default_constructor } Some(c) => { c } }; let class = factory::declare_class(self, gd_name, extends, *main_class, constructor, decls, decl.pos)?; if *main_class { factory::flatten_class_into_main(self.compiler.import_path_table(), self.builder, class); Ok(()) } else { self.builder.add_decl(Decl::new(DeclF::ClassDecl(class), decl.pos)); Ok(()) } } IRDeclF::EnumDecl(ir::decl::EnumDecl { visibility: _, name, clauses }) => { let gd_name = names::lisp_to_gd(name); let gd_clauses = clauses.iter().map(|(const_name, const_value)| { let gd_const_name = names::lisp_to_gd(const_name); let gd_const_value = const_value.as_ref().map(|x| self.compile_simple_expr(const_name, x, NeedsResult::Yes)).transpose()?; Ok((gd_const_name, gd_const_value)) }).collect::>()?; self.builder.add_decl(Decl::new(DeclF::EnumDecl(decl::EnumDecl { name: Some(gd_name), clauses: gd_clauses }), decl.pos)); Ok(()) } IRDeclF::DeclareDecl(_) => { // (sys/declare ...) statements have no runtime presence and do // nothing here. Ok(()) } } } } impl<'a, 'b, 'c, 'd, 'e> CompilerFrame<'a, 'b, 'c, 'd, 'e, StmtBuilder> { /// Compiles the sequence of statements into the current frame's /// builder. /// /// If `stmts` is empty, then the builder is unmodified and /// [`Expr::null`] is returned. Otherwise, all except the *final* /// statement in `stmts` are compiled using /// [`compile_stmt`](CompilerFrame::compile_stmt) (with destination /// of [`stmt_wrapper::Vacuous`]). The final statement is compiled /// with [`compile_expr`](CompilerFrame::compile_expr), using /// `needs_result` to determine if the result value will be used. /// /// The source offset `pos` is only used if `stmts` is empty, in /// which case it is reported as the source location of the compiled /// null value. pub fn compile_stmts(&mut self, stmts: &[&IRExpr], needs_result: NeedsResult, pos: SourceOffset) -> Result { if stmts.is_empty() { Ok(Compiler::nil_expr(pos)) } else { let prefix = &stmts[..stmts.len()-1]; let end = &stmts[stmts.len()-1]; for x in prefix { self.compile_stmt(&stmt_wrapper::Vacuous, x)?; } self.compile_expr(end, needs_result) } } /// Compiles a single statement into the current builder. The IR /// expression `stmt` is compiled (via /// [`compile_expr`](CompilerFrame::compile_expr)) into an [`Expr`]. /// Then that expression object is converted into a [`Stmt`] using /// [`destination.wrap_to_builder`](StmtWrapper::wrap_to_builder). /// /// During the `compile_expr` call, `needs_result` is /// [`NeedsResult::Yes`] if and only if `destination.is_vacuous()` /// is false. That is, the expression result will be discarded if /// and only if the statement destination is vacuous. pub fn compile_stmt(&mut self, destination: &(impl StmtWrapper + ?Sized), stmt: &IRExpr) -> Result<(), GDError> { let needs_result = NeedsResult::from(!destination.is_vacuous()); let expr = self.compile_expr(stmt, needs_result)?; destination.wrap_to_builder(self.builder, expr); Ok(()) } // TODO Document me pub fn compile_expr(&mut self, expr: &IRExpr, needs_result: NeedsResult) -> Result { match &expr.value { IRExprF::BareName(name) => { self.compile_bare_name(name, expr.pos) } IRExprF::Literal(lit) => { let lit = factory::compile_literal(lit, expr.pos); Ok(StExpr { expr: lit, side_effects: SideEffects::None }) } IRExprF::Progn(body) => { let body: Vec<_> = body.iter().collect(); self.compile_stmts(&body[..], needs_result, expr.pos) } IRExprF::CondStmt(clauses) => { special_form::compile_cond_stmt(self, clauses, needs_result, expr.pos) } IRExprF::WhileStmt(cond, body) => { special_form::compile_while_stmt(self, cond, body, needs_result, expr.pos) } IRExprF::ForStmt(name, iter, body) => { special_form::compile_for_stmt(self, name, iter, body, needs_result, expr.pos) } IRExprF::Call(object, f, args) => { self.compile_function_call(object, f, args, expr.pos) } IRExprF::Let(clauses, body) => { let_block::compile_let(self, clauses, body, needs_result, expr.pos) } IRExprF::FunctionLet(FunctionBindingType::OuterScoped, clauses, body) => { flet::compile_flet(self, clauses, body, needs_result, self.compiler.is_minimalist(), expr.pos) } IRExprF::FunctionLet(FunctionBindingType::Recursive, clauses, body) => { flet::compile_labels(self, clauses, body, needs_result, expr.pos) } IRExprF::Lambda(args, body) => { lambda::compile_lambda_stmt(self, args, body, self.compiler.is_minimalist(), expr.pos) } IRExprF::FuncRef(name) => { match name { FuncRefTarget::SimpleName(name) => { let func = self.table.get_fn(name).ok_or_else(|| GDError::new(GDErrorF::NoSuchFn(name.clone()), expr.pos))?.0.clone(); lambda::compile_function_ref(self.compiler, self.pipeline, self.builder, self.table, func, self.compiler.is_minimalist(), expr.pos) } } } IRExprF::Assign(target, expr) => { self.compile_assignment(target, expr, needs_result) } IRExprF::Quote(ast) => { let mut gen = RegisteredNameGenerator::new_local_var(self.table); let (stmts, result) = reify_pretty_expr(ast, MAX_QUOTE_REIFY_DEPTH, &mut gen); self.builder.append_all(&mut stmts.into_iter()); Ok(StExpr { expr: result, side_effects: SideEffects::None }) } IRExprF::FieldAccess(lhs, sym) => { // This is a special case to validate enum names, as an extra sanity check. if let Some(lhs) = lhs.as_plain_name() { if let Some(LocalVar { value_hint: Some(ValueHint::Enum(vs)), .. }) = self.table.get_var(lhs) { // It's an enum and we know its values; validate if !vs.contains(&names::lisp_to_gd(sym)) { return Err(GDError::new(GDErrorF::NoSuchEnumValue(lhs.to_owned(), sym.clone()), expr.pos)); } } } let StExpr { expr: lhs, side_effects: state } = self.compile_expr(lhs, NeedsResult::Yes)?; let side_effects = max(SideEffects::ReadsState, state); Ok(StExpr { expr: Expr::new(ExprF::Attribute(Box::new(lhs), names::lisp_to_gd(sym)), expr.pos), side_effects }) } IRExprF::LambdaClass(cls) => { lambda_class::compile_lambda_class(self, cls, expr.pos) } IRExprF::Yield(arg) => { match arg { None => Ok(StExpr { expr: Expr::yield_expr(None, expr.pos), side_effects: SideEffects::ModifiesState }), Some((x, y)) => { let x = self.compile_expr(x, NeedsResult::Yes)?.expr; let y = self.compile_expr(y, NeedsResult::Yes)?.expr; Ok(StExpr { expr: Expr::yield_expr(Some((x, y)), expr.pos), side_effects: SideEffects::ModifiesState }) } } } IRExprF::Assert(cond, message) => { let cond = self.compile_expr(cond, NeedsResult::Yes)?.expr; let message = message.as_ref().map(|x| self.compile_expr(x, NeedsResult::Yes).map(|y| y.expr)).transpose()?; Ok(StExpr { expr: Expr::assert_expr(cond, message, expr.pos), side_effects: SideEffects::ModifiesState }) } IRExprF::Return(expr) => { self.compile_stmt(&stmt_wrapper::Return, expr)?; Ok(Compiler::nil_expr(expr.pos)) } IRExprF::Break => { self.builder.append(Stmt::break_stmt(expr.pos)); Ok(Compiler::nil_expr(expr.pos)) } IRExprF::Continue => { self.builder.append(Stmt::continue_stmt(expr.pos)); Ok(Compiler::nil_expr(expr.pos)) } IRExprF::SpecialRef(special_ref) => { Ok(self.compile_special_ref(*special_ref, expr.pos)) } IRExprF::ContextualFilename(filename) => { let new_filename = self.preload_resolver().resolve_preload(filename) .ok_or_else(|| GDError::new(GDErrorF::ContextualFilenameUnresolved, expr.pos))?; Ok(StExpr { expr: Expr::from_value(new_filename, expr.pos), side_effects: SideEffects::None }) } IRExprF::Split(name, expr) => { let pos = expr.pos; let expr = self.compile_expr(expr, NeedsResult::Yes)?.expr; let mut gen = RegisteredNameGenerator::new_local_var(self.table); let tmp_var = factory::declare_var(&mut gen, self.builder, name, Some(expr), pos); // TODO Contextual name generator? Ok(StExpr { expr: Expr::new(ExprF::Var(tmp_var), pos), side_effects: SideEffects::None, }) } IRExprF::Preload(arg) => { let path = RPathBuf::try_from(arg.to_owned()).map_err(|_| { GDError::new(GDErrorF::BadPreloadArgument(arg.to_owned()), expr.pos) })?; let preload_call = self.compiler.make_preload_expr(&path, expr.pos)?; Ok(StExpr { expr: preload_call, side_effects: SideEffects::None, }) } /* // This will eventually be an optimization. IRExprF::Funcall(f, args) => { let func_expr = self.compile_expr(builder, table, f, NeedsResult::Yes)?.0; let args_expr = args.iter().map(|arg| { self.compile_expr(builder, table, arg, NeedsResult::Yes).map(|x| x.0) }).collect::, _>>()?; let fn_name = String::from("call_func"); let expr = Expr::Call(Some(Box::new(func_expr)), fn_name, args_expr); Ok(StExpr(expr, true)) } */ } } fn compile_bare_name(&mut self, bare_name: &BareName, pos: SourceOffset) -> Result { match bare_name { BareName::Plain(s) => { self.table.get_var(s).ok_or_else(|| GDError::new(GDErrorF::NoSuchVar(s.clone()), pos)).map(|var| { StExpr { expr: var.expr(pos), side_effects: SideEffects::from(var.access_type) } }) } BareName::Atomic(s) => { Ok(StExpr { expr: Expr::var(&names::lisp_to_gd_bare(s), pos), side_effects: SideEffects::ReadsState }) } } } fn compile_function_call(&mut self, object: &CallTarget, name: &str, args: &[IRExpr], pos: SourceOffset) -> Result { match object { CallTarget::Scoped => { self.compile_ordinary_function_call(name, args, pos) } CallTarget::Object(lhs) => { // Note: No call magic, no optional/rest arguments. When // calling a method, we assume all arguments are required, we // perform no optimization, we do not check arity, and we // simply blindly forward the call on the GDScript side. let lhs = self.compile_expr(lhs, NeedsResult::Yes)?.expr; let args = args.iter() .map(|arg| self.compile_expr(arg, NeedsResult::Yes).map(|x| x.expr)) .collect::, _>>()?; Ok(StExpr { expr: Expr::call(Some(lhs), &names::lisp_to_gd(name), args, pos), side_effects: SideEffects::ModifiesState }) } CallTarget::Super => { let args = args.iter() .map(|arg| self.compile_expr(arg, NeedsResult::Yes).map(|x| x.expr)) .collect::, _>>()?; let self_binding = self.table.get_var("self").ok_or_else(|| GDError::new(GDErrorF::BadSuperCall(String::from(name)), pos))?; let expr = self.class_scope.super_call(self.compiler.name_generator(), self_binding, name.to_owned(), args, pos)?; Ok(StExpr { expr: expr, side_effects: SideEffects::ModifiesState, }) } CallTarget::Atomic => { let name = names::lisp_to_gd_bare(name); let args = args.iter() .map(|x| self.compile_expr(x, NeedsResult::Yes).map(|x| x.expr)) .collect::, _>>()?; Ok(StExpr { expr: Expr::call(None, &name, args, pos), side_effects: SideEffects::ModifiesState, }) } } } /// Compiles a function call, given the name of the function and its /// argument list. pub fn compile_ordinary_function_call(&mut self, name: &str, args: &[IRExpr], pos: SourceOffset) -> Result { let (fcall, call_magic) = match self.table.get_fn(name) { None => return Err(GDError::new(GDErrorF::NoSuchFn(name.to_owned()), pos)), Some((p, m)) => (p.clone(), dyn_clone::clone_box(m)) }; // Macro calls should not occur at this stage in compilation. if fcall.is_macro { return Err(GDError::new(GDErrorF::MacroBeforeDefinitionError(name.to_owned()), pos)); } // Call magic is used to implement some commonly used wrappers // for simple GDScript operations. let args = args.iter() .map(|x| self.compile_expr(x, NeedsResult::Yes)) .collect::, _>>()?; Ok(StExpr { expr: fcall.into_expr_with_magic(&call_magic, self.compiler, self.builder, self.table, args, pos)?, side_effects: SideEffects::ModifiesState, }) } pub fn compile_special_ref(&mut self, special_ref: SpecialRef, pos: SourceOffset) -> StExpr { match special_ref { SpecialRef::ThisFile => { let current_filename = inner_class::get_current_filename(self.pipeline, self.compiler.preload_resolver()) .expect("Error identifying current file"); let expr = VarName::load_expr(current_filename, pos); StExpr { expr, side_effects: SideEffects::None } } SpecialRef::ThisFileName => { let current_filename = inner_class::get_current_filename(self.pipeline, self.compiler.preload_resolver()) .expect("Error identifying current file"); let expr = Expr::from_value(current_filename, pos); StExpr { expr, side_effects: SideEffects::None } } SpecialRef::ThisTrueFileName => { let current_filename = inner_class::get_current_filename(self.pipeline, &DefaultPreloadResolver) .expect("Error identifying current file"); let expr = Expr::from_value(current_filename, pos); StExpr { expr, side_effects: SideEffects::None } } SpecialRef::GodotVersion => { let godot_version = self.pipeline.config().godot_version.version; let expr = Expr::from_value(godot_version.into_i32(), pos); StExpr { expr, side_effects: SideEffects::None } } } } fn compile_assignment(&mut self, target: &AssignTarget, expr: &IRExpr, needs_result: NeedsResult) -> Result { match target { AssignTarget::Variable(pos, name) => { let var = self.table.get_var(name).ok_or_else(|| GDError::new(GDErrorF::NoSuchVar(name.clone()), *pos))?.to_owned(); if !var.assignable { return Err(GDError::new(GDErrorF::CannotAssignTo(var.name.to_gd(*pos)), expr.pos)); } self.compile_stmt(&stmt_wrapper::AssignToExpr(var.expr(*pos)), expr)?; Ok(StExpr { expr: var.expr(*pos), side_effects: SideEffects::from(var.access_type) }) } AssignTarget::InstanceField(pos, lhs, name) => { let StExpr { expr: lhs, side_effects: _ } = self.compile_expr(lhs, NeedsResult::Yes)?; let lhs = Expr::new(ExprF::Attribute(Box::new(lhs), names::lisp_to_gd(name)), expr.pos); let StExpr { expr: mut rhs, side_effects } = self.compile_expr(expr, NeedsResult::Yes)?; // Assign to a temporary if the RHS is stateful and we need a result. if needs_result == NeedsResult::Yes && side_effects.modifies_state() { let mut gen = RegisteredNameGenerator::new_local_var(self.table); let var = factory::declare_var(&mut gen, self.builder, "_assign", Some(rhs), expr.pos); rhs = Expr::new(ExprF::Var(var), *pos); } self.builder.append( Stmt::simple_assign(lhs, rhs.clone(), *pos), ); if needs_result == NeedsResult::Yes { Ok(StExpr { expr: rhs, side_effects: SideEffects::None }) } else { Ok(Compiler::nil_expr(expr.pos)) } } } } } impl<'a, 'b, 'c, 'd, 'e, B> HasSymbolTable for CompilerFrame<'a, 'b, 'c, 'd, 'e, B> { fn get_symbol_table(&self) -> &SymbolTable { self.table } fn get_symbol_table_mut(&mut self) -> &mut SymbolTable { self.table } } ================================================ FILE: src/compile/import.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`ImportTable`] type, which maps constant names //! representing imports to the target pathname. use std::collections::HashMap; /// An `ImportTable` is effectively a `HashMap` /// mapping imported variable names to strings representing the target /// path. #[derive(Clone, Debug, Default)] pub struct ImportTable { values: HashMap, } impl ImportTable { /// An empty `ImportTable`, equivalent to `ImportTable::default()`. pub fn new() -> ImportTable { ImportTable::default() } /// Gets the import path associated to the given variable name. pub fn get(&self, name: &str) -> Option<&str> { self.values.get(name).map(String::as_ref) } /// Assigns a path to the given name, replacing any previous path /// associated to the name. pub fn set(&mut self, name: String, value: String) { self.values.insert(name, value); } /// Removes the path associated to the given name, if it exists. pub fn del(&mut self, name: &str) { self.values.remove(name); } /// Converts `self` into a hash map containing the same information. pub fn into_hashmap(self) -> HashMap { self.values } } ================================================ FILE: src/compile/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod names; pub mod body; pub mod error; pub mod stmt_wrapper; pub mod symbol_table; pub mod special_form; pub mod stateful; pub mod preload_resolver; pub mod resource_type; pub mod constant; pub mod args; pub mod factory; pub mod frame; pub mod import; use frame::CompilerFrame; use body::builder::{CodeBuilder, StmtBuilder, HasDecls}; use body::class_initializer::{ClassBuilder, InitTime}; use body::class_scope::{ClassScope, OutsideOfClass, DirectClassScope}; use names::fresh::FreshNameGenerator; use names::generator::NameGenerator; use preload_resolver::PreloadResolver; use constant::is_const_expr; use crate::gdscript::literal::Literal; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::decl::{self, Decl, DeclF, Onready, Setget}; use crate::gdscript::class_extends::ClassExtends; use crate::gdscript::library; use crate::gdscript::arglist::ArgList; use error::{GDError, GDErrorF}; use symbol_table::{SymbolTable, ClassTablePair}; use symbol_table::local_var::{LocalVar, ValueHint, VarName, VarScope, VarNameIntoExtendsError}; use symbol_table::function_call; use symbol_table::call_magic::CallMagic; use symbol_table::call_magic::table::MagicTable; use crate::ir; use crate::ir::import::{ImportName, ImportDecl, ImportDetails}; use crate::ir::identifier::{Namespace, ClassNamespace}; use crate::ir::access_type::AccessType; use crate::ir::decl::InstanceFunctionName; use crate::runner::path::RPathBuf; use crate::pipeline::error::PError; use crate::pipeline::Pipeline; use crate::pipeline::can_load::CanLoad; use crate::pipeline::source::SourceOffset; use stateful::{StExpr, NeedsResult, SideEffects}; use resource_type::ResourceType; use import::ImportTable; use std::ffi::OsStr; use std::convert::TryFrom; use std::borrow::Borrow; type IRDecl = ir::decl::Decl; type IRDeclF = ir::decl::DeclF; type IRExpr = ir::expr::Expr; type IRExprF = ir::expr::ExprF; type IRArgList = ir::arglist::ordinary::ArgList; pub struct Compiler { gen: FreshNameGenerator, resolver: Box, magic_table: MagicTable, import_table: ImportTable, minimalist: bool, } impl Compiler { /// Constructs a new compiler associated with the given name /// generator and preload resolver. pub fn new(gen: FreshNameGenerator, resolver: Box, minimalist: bool) -> Compiler { let magic_table = library::magic::standard_magic_table(); let import_table = ImportTable::default(); Compiler { gen, resolver, magic_table, import_table, minimalist } } pub fn nil_expr(pos: SourceOffset) -> StExpr { StExpr { expr: Expr::null(pos), side_effects: SideEffects::None } } pub fn name_generator(&mut self) -> &mut FreshNameGenerator { &mut self.gen } pub fn preload_resolver(&self) -> &dyn PreloadResolver { &*self.resolver } pub fn import_path_table(&self) -> &ImportTable { &self.import_table } pub fn is_minimalist(&self) -> bool { self.minimalist } pub fn frame<'a, 'b, 'c, 'd, 'e, B>(&'a mut self, pipeline: &'b mut Pipeline, builder: &'c mut B, table: &'d mut SymbolTable, class_scope: &'e mut dyn ClassScope) -> CompilerFrame<'a, 'b, 'c, 'd, 'e, B> { CompilerFrame::new(self, pipeline, builder, table, class_scope) } fn compile_export(&mut self, pipeline: &mut Pipeline, table: &mut SymbolTable, expr: &IRExpr) -> Result { // Any expression valid as a const is valid here, but then so are // Expr::BareName since we need to allow type names. // // TODO Validate that the local vars appearing here make sense. match &expr.value { IRExprF::BareName(s) => Ok(Expr::new(ExprF::Var(s.to_gd_name_bare()), expr.pos)), _ => { let mut class_scope = OutsideOfClass; let expr = self.frame(pipeline, &mut (), table, &mut class_scope).compile_simple_expr("export", expr, NeedsResult::Yes)?; Ok(expr) } } } // TODO To CompilerFrame (???) pub fn compile_class_inner_decl(&mut self, pipeline: &mut Pipeline, builder: &mut ClassBuilder, tables: ClassTablePair<'_, '_>, class_scope: &mut DirectClassScope, decl: &ir::decl::ClassInnerDecl) -> Result { let table = tables.into_table(decl.is_static()); match &decl.value { ir::decl::ClassInnerDeclF::ClassSignalDecl(s) => { let name = names::lisp_to_gd(&s.name); let args = s.args.args.iter().map(|x| names::lisp_to_gd(x)).collect(); Ok(Decl::new(DeclF::SignalDecl(name, ArgList::required(args)), decl.pos)) } ir::decl::ClassInnerDeclF::ClassConstDecl(c) => { // TODO Merge this with IRDecl::ConstDecl above let gd_name = names::lisp_to_gd(&c.name); let value = self.frame(pipeline, &mut (), table, class_scope).compile_simple_expr(&c.name, &c.value, NeedsResult::Yes)?; Ok(Decl::new(DeclF::ConstDecl(gd_name, value), decl.pos)) } ir::decl::ClassInnerDeclF::ClassVarDecl(v) => { let export = v.export.as_ref().map(|export| { export.args.iter().map(|expr| self.compile_export(pipeline, table, expr)).collect::, _>>() }).transpose()?; let export = export.map(|args| decl::Export { args }); let name = names::lisp_to_gd(&v.name); // Note: immediate_value is *only* values which will be // compiled directly into the declaration. Initializers added // to _init or _ready are not included in this return value // (they are added to the mutable builder frame instead). let immediate_value = { let mut local_frame = self.frame(pipeline, builder.builder_for(v.init_time), table, class_scope); Compiler::compile_inner_var_value(&name, v.value.as_ref(), &mut local_frame, decl.pos)? }; let onready = Onready::from(immediate_value.is_some() && v.init_time == InitTime::Ready); Ok(Decl::new(DeclF::VarDecl(decl::VarDecl { export, onready, name, value: immediate_value, setget: Setget::default(), }), decl.pos)) } ir::decl::ClassInnerDeclF::ClassFnDecl(f) => { // If we're dealing with a setter or a getter, inform the // builder that we will need a synthetic field and validate // the argument list and modifiers. match &f.name { InstanceFunctionName::Ordinary(_) => { // No action required; there is no proxy field for // ordinary methods. } InstanceFunctionName::Setter(field_name) => { if f.is_static == decl::Static::IsStatic || f.args.len() != 1 { return Err(GDError::new( GDErrorF::BadSetterArguments(field_name.to_owned()), decl.pos, )); } let field_name = names::lisp_to_gd(field_name); let conflicting_name = builder.declare_setter_for(field_name.to_owned()); if conflicting_name.is_some() { return Err(GDError::new( GDErrorF::DuplicateName(ClassNamespace::Value, field_name), decl.pos, )); } } InstanceFunctionName::Getter(field_name) => { if f.is_static == decl::Static::IsStatic || !f.args.is_empty() { return Err(GDError::new( GDErrorF::BadGetterArguments(field_name.to_owned()), decl.pos, )); } let field_name = names::lisp_to_gd(field_name); let conflicting_name = builder.declare_getter_for(field_name.to_owned()); if conflicting_name.is_some() { return Err(GDError::new( GDErrorF::DuplicateName(ClassNamespace::Value, field_name), decl.pos, )) } } } let gd_name = names::lisp_to_gd(f.name.method_name().borrow()); let destination: Box = if matches!(&f.name, InstanceFunctionName::Setter(_)) { // Setters do not return anything Box::new(stmt_wrapper::Vacuous) } else { Box::new(stmt_wrapper::Return) }; let mut func = factory::declare_function(&mut self.frame(pipeline, builder, table, class_scope), gd_name, IRArgList::from(f.args.clone()), &f.body, &*destination)?; if f.is_nullargs { func.args.optionalize_all(&Expr::null(decl.pos)); } Ok(Decl::new(DeclF::FnDecl(f.is_static, func), decl.pos)) } } } fn compile_inner_var_value(name: &str, value: Option<&IRExpr>, frame: &mut CompilerFrame, pos: SourceOffset) -> Result, GDError> { match value { None => Ok(None), Some(value) => { // If the value is a constant expression, then compile it // inline. if is_const_expr(value, frame.table) { let simple_expr = frame.compile_simple_expr(name, value, NeedsResult::Yes)?; return Ok(Some(simple_expr)); } // Otherwise, compile as an ordinary expression into the // builder. let destination = stmt_wrapper::AssignToExpr( Expr::self_var(pos).attribute(name.to_owned(), pos), ); frame.compile_stmt(&destination, value)?; Ok(None) } } } /// Given the (simple) name of an `extends` clause for a class or /// singleton object, resolve that name (using the symbol table) /// into a [`ClassExtends`]. /// /// The name `extends` is looked up in the symbol table `table`. /// Then the resulting variable's GDScript [`LocalVar`] is converted /// into a [`ClassExtends`] via [`TryFrom::try_from`]. If either the /// lookup or the conversion fails, then the appropriate error is /// returned. /// /// Additionally, if the scope of the [`LocalVar`] is *not* /// [`VarScope::GlobalVar`](crate::compile::symbol_table::local_var::VarScope::GlobalVar), /// then [`GDErrorF::CannotExtend`] is returned. This situation should /// never happen for top-level class or object declarations, but it /// can occur in lambda classes. pub fn resolve_extends(table: &SymbolTable, extends: &str, pos: SourceOffset) -> Result { let var = table.get_var(extends).ok_or_else(|| GDError::new(GDErrorF::NoSuchVar(extends.to_owned()), pos))?; if var.scope != VarScope::GlobalVar { return Err(GDError::new(GDErrorF::CannotExtend(VarNameIntoExtendsError::CannotExtendLocal(extends.to_owned())), pos)); } let var_name = var.name.clone(); ClassExtends::try_from(var_name).map_err(|x| GDError::from_value(x, pos)) } fn bind_decl(magic_table: &MagicTable, pipeline: &mut Pipeline, table: &mut SymbolTable, decl: &IRDecl) -> Result<(), GDError> { match &decl.value { IRDeclF::FnDecl(ir::decl::FnDecl { visibility: _, call_magic, name, args, body: _ }) => { let func = function_call::FnCall::file_constant( function_call::FnSpecs::from(args.to_owned()), function_call::FnScope::Global, names::lisp_to_gd(name), ); let call_magic: CallMagic = match call_magic { None => CallMagic::DefaultCall, Some(m) => { // If a call magic declaration was specified, it MUST // exist or it's a compile error. match magic_table.get(m) { None => return Err(GDError::new(GDErrorF::NoSuchMagic(m.to_owned()), decl.pos)), Some(magic) => magic.clone(), } } }; table.set_fn(name.clone(), func, call_magic); } IRDeclF::MacroDecl(ir::decl::MacroDecl { visibility: _, name, args, body: _ }) => { // As above, macros compile basically the same as functions in // terms of call semantics and should be resolved during the // IR stage. let func = function_call::FnCall::file_macro( function_call::FnSpecs::from(args.to_owned()), function_call::FnScope::Global, names::lisp_to_gd(name), ); table.set_fn(name.clone(), func, CallMagic::DefaultCall); } IRDeclF::SymbolMacroDecl(ir::decl::SymbolMacroDecl { name, .. }) => { // No action; symbol macros have no runtime binding presence. table.set_var(name.clone(), LocalVar { name: VarName::Null, access_type: AccessType::Read, scope: VarScope::GlobalVar, assignable: false, value_hint: Some(ValueHint::SymbolMacro), }); } IRDeclF::ConstDecl(ir::decl::ConstDecl { visibility: _, name, value }) => { let mut var = LocalVar::file_constant(names::lisp_to_gd(name)); // Can't assign to constants // Do a literal value hint if possible, but fall back to // GlobalConstant if we can't do better. var = var.with_hint(ValueHint::GlobalConstant); if let IRExprF::Literal(value) = &value.value { if let Ok(value) = Literal::try_from(value.clone()) { var = var.with_hint(ValueHint::Literal(value)); } } table.set_var(name.clone(), var); } IRDeclF::ClassDecl(ir::decl::ClassDecl { name, main_class, .. }) => { if *main_class { let var = LocalVar::current_file(pipeline.current_filename().to_string()).with_hint(ValueHint::ClassName); table.set_var(name.clone(), var); } else { let var = LocalVar::file_constant(names::lisp_to_gd(name)) .no_assign() // Can't assign to class names .with_hint(ValueHint::ClassName); table.set_var(name.clone(), var); } } IRDeclF::EnumDecl(edecl) => { let name = edecl.name.clone(); let var = LocalVar::file_constant(names::lisp_to_gd(&name)) .no_assign() // Can't assign to constants .with_hint(ValueHint::enumeration(edecl.value_names())); table.set_var(name, var); } IRDeclF::DeclareDecl(ddecl) => { let ir::decl::DeclareDecl { visibility: _, declare_type, name, target_name } = ddecl; let target_name: String = match target_name { // Default behavior: Target name is `lisp_to_gd` on the // declared name. Overriding built-ins is forbidden in this // case. None => names::lisp_to_gd(name), // Custom behavior: Target name is `lisp_to_gd_bare` on the // specified name. Overriding built-ins is allowed in this // case. This essentially acts as though the target name was // placed inside a `(literally ...)` block. Some(target_name) => names::lisp_to_gd_bare(target_name), }; match declare_type { ir::decl::DeclareType::Value => { let var = LocalVar::file_constant(target_name); table.set_var(name.clone(), var); } ir::decl::DeclareType::Constant => { let var = LocalVar::file_constant(target_name) .with_hint(ValueHint::GlobalConstant); table.set_var(name.clone(), var); } ir::decl::DeclareType::Superglobal => { let var = LocalVar::superglobal(target_name) .with_hint(ValueHint::Superglobal); table.set_var(name.clone(), var); } ir::decl::DeclareType::Function(args) => { let func = function_call::FnCall::file_constant( function_call::FnSpecs::from(args.to_owned()), function_call::FnScope::Global, target_name, ); table.set_fn(name.clone(), func, CallMagic::DefaultCall); } ir::decl::DeclareType::SuperglobalFn(args) => { let func = function_call::FnCall::superglobal( function_call::FnSpecs::from(args.to_owned()), function_call::FnScope::Superglobal, target_name, ); table.set_fn(name.clone(), func, CallMagic::DefaultCall); } } } }; Ok(()) } fn make_preload_line(&self, var: String, path: &RPathBuf, pos: SourceOffset) -> Result { let expr = self.make_preload_expr(path, pos)?; Ok(Decl::new( DeclF::ConstDecl(var, expr), pos, )) } fn get_preload_resolved_path(&self, path: &RPathBuf, pos: SourceOffset) -> Result { let mut path = path.clone(); if path.path().extension() == Some(OsStr::new("lisp")) { path.path_mut().set_extension("gd"); } self.resolver.resolve_preload(&path) .ok_or_else(|| GDError::new(GDErrorF::NoSuchFile(path.to_string()), pos)) } /// Compile a `preload` call, using the current preload resolver on /// `self` to resolve the path. pub fn make_preload_expr(&self, path: &RPathBuf, pos: SourceOffset) -> Result { if self.resolver.include_resource(ResourceType::from(path.path())) { let path = self.get_preload_resolved_path(path, pos)?; Ok(Expr::simple_call("preload", vec!(Expr::from_value(path, pos)), pos)) } else { // We null out any resources we don't understand. This means // that GDScript source files (those NOT written in GDLisp) and // other resources like PackedScene instances cannot be used in // macros, as they'll just be seen as "null" during macro // resolution. I do not verify that you follow this rule; you // are expected to be responsible with your macro resource // usage. Ok(Expr::null(pos)) } } fn import_name(&mut self, import: &ImportDecl) -> String { let prefix = match &import.details { ImportDetails::Named(s) => names::lisp_to_gd(s), ImportDetails::Restricted(_) | ImportDetails::Open => String::from("_Import"), }; self.gen.generate_with(&prefix) } pub fn translate_call(import_name: String, mut call: function_call::FnCall) -> function_call::FnCall { if call.scope != function_call::FnScope::Superglobal { call.object = call.object.into_imported(import_name); } call } pub fn resolve_import(&mut self, pipeline: &mut Pipeline, builder: &mut CodeBuilder, table: &mut SymbolTable, import: &ImportDecl) -> Result<(), PError> { let preload_name = self.import_name(import); builder.add_decl(self.make_preload_line(preload_name.clone(), &import.filename, import.pos)?); let res_type = ResourceType::from(import); // If the path should be included and isn't being `null`ed out // right now, then we add it to the import table. if self.resolver.include_resource(ResourceType::from(import.filename.path())) { let target_path = self.get_preload_resolved_path(&import.filename, import.pos)?; self.import_table.set(preload_name.clone(), target_path); } ResourceType::check_import(pipeline, import)?; if res_type == ResourceType::GDLispSource { // Now add the pertinent symbols to the symbol table let unit = pipeline.load_file(&import.filename.path(), import.pos)?; let unit_table = &unit.table; let exports = &unit.exports; let names = import.names(&unit.exports); for imp in names { // TODO Can these situations happen? It seems like // ImportNameResolutionError should've already caught any // nonexistent names. Review this and see if we can turn these // into panic! calls. let ImportName { namespace: namespace, in_name: import_name, out_name: export_name } = imp; match namespace { Namespace::Function => { let (call, _) = unit_table.get_fn(&export_name).ok_or_else(|| GDError::new(GDErrorF::NoSuchFn(export_name), import.pos))?; let call = Compiler::translate_call(preload_name.clone(), call.clone()); table.set_fn(import_name.clone(), call, CallMagic::DefaultCall); } Namespace::Value => { let mut var = unit_table.get_var(&export_name).ok_or_else(|| GDError::new(GDErrorF::NoSuchVar(export_name), import.pos))?.clone(); var.name = var.name.into_imported(preload_name.clone()); table.set_var(import_name.clone(), var); } } } // If it was a restricted import list, validate the import names if let ImportDetails::Restricted(vec) = &import.details { for imp in vec { imp.refine(exports).map_err(|x| GDError::from_value(x, import.pos))?; } } } else { // Simple resource import let name = match &import.details { ImportDetails::Named(s) => s.to_owned(), _ => return Err(PError::from(GDError::new(GDErrorF::InvalidImportOnResource(import.filename.to_string()), import.pos))), }; // TODO Check that the file exists? let mut var = LocalVar::file_constant(preload_name); if let Some(value_hint) = import.filename.extension().and_then(ValueHint::from_file_ext) { var = var.with_hint(value_hint); } table.set_var(name, var); } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::decl::Decl; use crate::gdscript::stmt::{Stmt, StmtF}; use crate::sxp::ast::{AST, ASTF}; use crate::compile::symbol_table::function_call::{FnCall, FnScope, FnSpecs}; use crate::compile::preload_resolver::DefaultPreloadResolver; use crate::compile::body::builder::StmtBuilder; use crate::pipeline::config::ProjectConfig; use crate::pipeline::source::SourceOffset; use crate::ir::incremental::IncCompiler; use crate::runner::version::VersionInfo; use std::path::PathBuf; // TODO A lot more of this fn int(n: i32) -> AST { AST::new(ASTF::int(n), SourceOffset::default()) } fn nil() -> AST { AST::nil(SourceOffset::default()) } #[allow(dead_code)] fn cons(a: AST, b: AST) -> AST { AST::new(ASTF::cons(a, b), SourceOffset::default()) } fn list(data: Vec) -> AST { AST::dotted_list(data, nil()) } fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } fn bind_helper_symbols(table: &mut SymbolTable) { // Binds a few helper names to the symbol table for the sake of // debugging. table.set_fn(String::from("foo1"), FnCall::file_constant(FnSpecs::new(1, 0, None), FnScope::Global, String::from("foo1")), CallMagic::DefaultCall); table.set_fn(String::from("foo"), FnCall::file_constant(FnSpecs::new(0, 0, None), FnScope::Global, String::from("foo")), CallMagic::DefaultCall); table.set_fn(String::from("bar"), FnCall::file_constant(FnSpecs::new(0, 0, None), FnScope::Global, String::from("bar")), CallMagic::DefaultCall); table.set_var(String::from("foobar"), LocalVar::read(String::from("foobar"))); } fn compile_stmt(ast: &AST) -> Result<(Vec, Vec), PError> { let mut pipeline = Pipeline::new(ProjectConfig { root_directory: PathBuf::from("."), optimizations: false, godot_version: VersionInfo::default() }); let used_names = ast.all_symbols(); let mut compiler = Compiler::new(FreshNameGenerator::new(used_names), Box::new(DefaultPreloadResolver), false); let mut table = SymbolTable::new(); bind_helper_symbols(&mut table); library::bind_builtins(&mut table, true); let mut builder = StmtBuilder::new(); let expr = { let mut icompiler = IncCompiler::new(ast.all_symbols()); icompiler.bind_builtin_macros(&mut pipeline); icompiler.compile_expr(&mut pipeline, ast) }?; { let mut class_scope = OutsideOfClass; let mut frame = compiler.frame(&mut pipeline, &mut builder, &mut table, &mut class_scope); let () = frame.compile_stmt(&mut stmt_wrapper::Return, &expr)?; } Ok(builder.build()) } #[test] fn compile_var() { let ast = AST::symbol("foobar", SourceOffset::default()); let expected = s(StmtF::ReturnStmt(e(ExprF::Var(String::from("foobar"))))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, vec!(expected)); assert_eq!(actual.1, vec!()); } #[test] fn compile_call() { let ast = list(vec!(AST::symbol("foo1", SourceOffset::default()), int(10))); let expected = s(StmtF::ReturnStmt(e(ExprF::Call(None, String::from("foo1"), vec!(e(ExprF::from(10))))))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, vec!(expected)); assert_eq!(actual.1, vec!()); } #[test] fn compile_int() { let ast = int(99); let expected = s(StmtF::ReturnStmt(e(ExprF::from(99)))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, vec!(expected)); assert_eq!(actual.1, vec!()); } #[test] fn compile_bool_t() { let ast = AST::new(ASTF::from(true), SourceOffset::default()); let expected = s(StmtF::ReturnStmt(e(ExprF::from(true)))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, vec!(expected)); assert_eq!(actual.1, vec!()); } #[test] fn compile_bool_f() { let ast = AST::new(ASTF::from(false), SourceOffset::default()); let expected = s(StmtF::ReturnStmt(e(ExprF::from(false)))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, vec!(expected)); assert_eq!(actual.1, vec!()); } #[test] fn compile_string() { let ast = AST::string("foobar", SourceOffset::default()); let expected = s(StmtF::ReturnStmt(e(ExprF::from("foobar".to_owned())))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, vec!(expected)); assert_eq!(actual.1, vec!()); } #[test] fn compile_progn_vacuous() { let ast = list(vec!(AST::symbol("progn", SourceOffset::default()), int(1), int(2))); let expected = vec!(s(StmtF::ReturnStmt(e(ExprF::from(2))))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, expected); assert_eq!(actual.1, vec!()); } #[test] fn compile_progn_stateful() { let ast = list(vec!(AST::symbol("progn", SourceOffset::default()), list(vec!(AST::symbol("foo", SourceOffset::default()))), list(vec!(AST::symbol("bar", SourceOffset::default()))))); let expected = vec!(s(StmtF::Expr(e(ExprF::Call(None, String::from("foo"), vec!())))), s(StmtF::ReturnStmt(e(ExprF::Call(None, String::from("bar"), vec!()))))); let actual = compile_stmt(&ast).unwrap(); assert_eq!(actual.0, expected); assert_eq!(actual.1, vec!()); } #[test] fn compile_nil() { let result1 = compile_stmt(&nil()).unwrap(); assert_eq!(result1, (vec!(s(StmtF::ReturnStmt(Compiler::nil_expr(SourceOffset::default()).expr))), vec!())); let result2 = compile_stmt(&list(vec!(AST::symbol("progn", SourceOffset::default())))).unwrap(); assert_eq!(result2, (vec!(s(StmtF::ReturnStmt(Compiler::nil_expr(SourceOffset::default()).expr))), vec!())); } } ================================================ FILE: src/compile/names/contextual.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the struct [`ContextualNameGenerator`] for safely //! generating unused names based on a symbol table. use super::generator::NameGenerator; use crate::ir::identifier::Namespace; use crate::compile::symbol_table::SymbolTable; use std::borrow::ToOwned; /// A `ContextualNameGenerator` is based on a [`SymbolTable`] and /// generates names which do not appear in the symbol table. /// /// A contextual name generator is designed to generate names in a /// particular namespace and cannot be used in other namespaces, since /// conflicts are only detected in one namespace. /// /// **Note:** `ContextualNameGenerator` does not modify the symbol /// table. For a variant of this type which keeps a mutable reference /// to a symbol table and modifies it to be aware of the new names, /// see [`super::registered::RegisteredNameGenerator`]. #[derive(Debug, Clone)] pub struct ContextualNameGenerator<'a> { context: &'a SymbolTable, namespace: Namespace, } impl<'a> ContextualNameGenerator<'a> { /// Construct a new `ContextualNameGenerator` from a symbol table. pub fn new(context: &SymbolTable, namespace: Namespace) -> ContextualNameGenerator<'_> { ContextualNameGenerator { context, namespace } } fn is_name_in_use(&self, name: &str) -> bool { match self.namespace { Namespace::Value => self.context.has_var_with_gd_name(name), Namespace::Function => self.context.has_fn_with_gd_name(name), } } } impl<'a> NameGenerator for ContextualNameGenerator<'a> { fn generate_with(&mut self, prefix: &str) -> String { let mut index = -1; let mut name: String = prefix.to_owned(); while self.is_name_in_use(&name) { index += 1; name = format!("{}_{}", prefix, index); } name } } #[cfg(test)] mod tests { use super::*; #[test] fn empty_generate() { let table = SymbolTable::new(); let mut gen = ContextualNameGenerator::new(&table, Namespace::Value); assert_eq!(gen.generate(), "_G"); assert_eq!(gen.generate(), "_G"); assert_eq!(gen.generate(), "_G"); } #[test] fn contextual_generate() { let mut table = SymbolTable::new(); table.add_synthetic_var(String::from("_G"), true); let mut gen = ContextualNameGenerator::new(&table, Namespace::Value); assert_eq!(gen.generate(), "_G_0"); assert_eq!(gen.generate(), "_G_0"); assert_eq!(gen.generate(), "_G_0"); } } ================================================ FILE: src/compile/names/fresh.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the struct [`FreshNameGenerator`] for safely generating //! unused names. use super::generator::NameGenerator; use json::{JsonValue, object}; use std::borrow::ToOwned; /// A `FreshNameGenerator` retains state for generating names which /// are guaranteed to be unused. /// /// The name generator retains a list of strings which it considers /// "reserved words". Those words will never be generated by the /// generator. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FreshNameGenerator { reserved: Vec, index: u32, } // Currently a placeholder enum that has a single dummy error value // right now. We can fill in more details later if we feel the need to // be any more specific than "JSON parse failed". /// Parse errors that can occur during parsing of JSON as a /// [`FreshNameGenerator`]. #[derive(Clone, Debug)] pub enum ParseError { MalformedInput, } impl FreshNameGenerator { /// Construct a new `FreshNameGenerator` given a collection /// `reserved` of reserved words. The words in `reserved` are /// guaranteed to never be generated by this name generator object. pub fn new(reserved: Vec<&str>) -> FreshNameGenerator { let reserved: Vec<_> = reserved.into_iter().map(|x| x.to_owned()).collect(); FreshNameGenerator { reserved: reserved, index: 0, } } /// Convert the `FreshNameGenerator` to a JSON value, suitable for /// restoring the generator later with /// [`FreshNameGenerator::from_json`]. pub fn to_json(&self) -> JsonValue { let reserved: Vec<_> = self.reserved.iter().map(|x| (**x).to_owned()).collect(); object!{ "reserved" => reserved, "index" => self.index, } } /// Produce a `FreshNameGenerator` from a JSON value. /// /// For any `gen: FreshNameGenerator`, it should be the case that /// `from_json(&gen.to_json())` should be an object /// indistinguishable from `gen` for all practical purposes. pub fn from_json(value: &JsonValue) -> Result { let value = match value { JsonValue::Object(value) => value, _ => return Err(ParseError::MalformedInput), }; // reserved let reserved = match value.get("reserved") { Some(JsonValue::Array(reserved)) => reserved, _ => return Err(ParseError::MalformedInput), }; let reserved = reserved.iter() .map(|value| value.as_str().ok_or(ParseError::MalformedInput).map(|string| { string.to_owned() })) .collect::>()?; // index let index = match value.get("index") { Some(JsonValue::Number(index)) => u32::from(*index), _ => return Err(ParseError::MalformedInput), }; Ok(FreshNameGenerator { reserved, index }) } } impl NameGenerator for FreshNameGenerator { fn generate_with(&mut self, prefix: &str) -> String { let mut name: String; loop { name = format!("{}_{}", prefix, self.index); self.index += 1; if !self.reserved.contains(&name) { break; } } name } } #[cfg(test)] mod tests { use super::*; #[test] fn empty_generate() { let mut gen = FreshNameGenerator::new(vec!()); assert_eq!(gen.generate(), "_G_0"); assert_eq!(gen.generate(), "_G_1"); assert_eq!(gen.generate(), "_G_2"); } #[test] fn unrelated_generate() { let mut gen = FreshNameGenerator::new(vec!("foo", "bar", "these_names_change_nothing", "a99")); assert_eq!(gen.generate(), "_G_0"); assert_eq!(gen.generate(), "_G_1"); assert_eq!(gen.generate(), "_G_2"); } #[test] fn conflicting_generate() { let mut gen = FreshNameGenerator::new(vec!("_G_1", "_G_3")); assert_eq!(gen.generate(), "_G_0"); assert_eq!(gen.generate(), "_G_2"); assert_eq!(gen.generate(), "_G_4"); } #[test] fn custom_prefix() { let mut gen = FreshNameGenerator::new(vec!("foo_1", "foo_3", "_G_0")); assert_eq!(gen.generate_with("foo"), "foo_0"); assert_eq!(gen.generate_with("foo"), "foo_2"); assert_eq!(gen.generate_with("foo"), "foo_4"); } #[test] fn through_json() { let mut gen = FreshNameGenerator::new(vec!("abc", "def", "foo_1", "_example_names")); assert_eq!(gen.generate_with("foo"), "foo_0"); assert_eq!(gen.generate_with("foo"), "foo_2"); assert_eq!(gen.index, 3); let json = gen.to_json(); let gen1 = FreshNameGenerator::from_json(&json).unwrap(); assert_eq!(gen, gen1); } } ================================================ FILE: src/compile/names/generator.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`NameGenerator`] trait, whose implementations are //! capable of generating names. /// `NameGenerator` represents structures intended to generate new, /// unique symbolic names. pub trait NameGenerator { /// Generate a new name, beginning with `prefix`. fn generate_with(&mut self, prefix: &str) -> String; /// Generate a new name, using /// [`generate_with`](NameGenerator::generate_with) and /// [`DEFAULT_PREFIX`] as the prefix. fn generate(&mut self) -> String { self.generate_with(DEFAULT_PREFIX) } } /// The default `prefix` argument to /// [`NameGenerator::generate_with`]. pub const DEFAULT_PREFIX: &str = "_G"; ================================================ FILE: src/compile/names/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helpers for managing GDLisp and GDScript names. use phf::phf_map; use std::fmt::Write; pub mod contextual; pub mod fresh; pub mod generator; pub mod registered; pub mod reserved; // Note: Many of these translations are based on the similar // translations performed by the Scala compiler. // // https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/util/NameTransformer.scala const TRANSLATIONS: phf::Map = phf_map! { '-' => "_", '<' => "_LT_", '>' => "_GT_", '=' => "_EQ_", '~' => "_TILDE_", '!' => "_BANG_", '#' => "_HASH_", '%' => "_PERCENT_", '^' => "_UP_", '&' => "_AMP_", '|' => "_BAR_", '*' => "_TIMES_", '/' => "_DIV_", '+' => "_PLUS_", ':' => "_COLON_", '\\' => "_BSLASH_", '?' => "_QMARK_", '@' => "_AT_", '$' => "_DSIGN_", }; /// A `NameTrans` is stored information about how a given GDLisp name /// translates into a GDScript name. #[derive(Clone, Debug, Eq, PartialEq)] pub struct NameTrans { /// The GDLisp name. pub lisp_name: String, /// The GDScript name which corresponds to the GDLisp name. pub gd_name: String, } /// Whether `ch` is a valid GDScript identifier character. /// /// Valid GDScript identifier characters are: uppercase and lowercase /// ASCII letters, the digits 0-9, and underscore. Equivalently, this /// function returns true if the character satisfies the regex /// `[A-Za-z0-9_]`. pub fn is_valid_gd_char(ch: char) -> bool { ch.is_digit(36) || ch == '_' } /// Converts `name` to a GDScript-friendly name. /// /// First, if the name begins with a digit, then an underscore is /// prefixed before continuing to the next steps. /// /// Second, for every character which is *not* a valid GDScript /// character (under [`is_valid_gd_char`]), a transformation takes /// place to translate that character. The following transformations /// are tried, in order. /// /// 1. The sequence `->` is replaced by `_to_`. /// /// 2. A question mark `?` at the end of an identifier is replaced by /// `is_` at the beginning. /// /// 3. `-`, `<`, `>`, and `=` are converted to, respectively, `_`, /// `_LT_`, `_GT_`, `_EQ_`. /// /// 4. All other characters are converted to `_uXXXX` where `XXXX` is /// the hex value (minimum four digits) of the Unicode code point for /// the character. If the character lies outside the basic /// multilingual plane, then all digits will be printed. /// /// Finally, if the resulting string is a GDScript reserved word (See /// the [`reserved`] module documentation), then an underscore is /// added before the beginning of the string. /// /// # Examples /// /// ``` /// # use gdlisp::compile::names::lisp_to_gd; /// // No escaping necessary /// assert_eq!(lisp_to_gd("foobar"), "foobar"); /// assert_eq!(lisp_to_gd("_private0"), "_private0"); /// assert_eq!(lisp_to_gd("xposition3"), "xposition3"); /// assert_eq!(lisp_to_gd("a0_0_EEe"), "a0_0_EEe"); /// /// // Leading digit must be escaped /// assert_eq!(lisp_to_gd("3e"), "_3e"); /// assert_eq!(lisp_to_gd("2_"), "_2_"); /// /// // Special transformation rules /// assert_eq!(lisp_to_gd("foo->bar"), "foo_to_bar"); /// assert_eq!(lisp_to_gd("failure?"), "is_failure"); /// /// // Known character translations /// assert_eq!(lisp_to_gd("my-identifier"), "my_identifier"); /// assert_eq!(lisp_to_gd("<=>"), "_LT__EQ__GT_"); /// /// // General codepoints /// assert_eq!(lisp_to_gd("w~~w"), "w_TILDE__TILDE_w"); /// assert_eq!(lisp_to_gd("α"), "_u03B1"); /// assert_eq!(lisp_to_gd("😃"), "_u1F603"); /// /// // Known GDScript keywords /// assert_eq!(lisp_to_gd("preload"), "_preload"); /// assert_eq!(lisp_to_gd("assert"), "_assert"); /// assert_eq!(lisp_to_gd("class_name"), "_class_name"); /// assert_eq!(lisp_to_gd("class-name"), "_class_name"); /// ``` pub fn lisp_to_gd(name: &str) -> String { // Escape known GDScript keywords let transformed = lisp_to_gd_bare(name); if reserved::RESERVED_WORDS.contains(&*transformed) { format!("_{}", transformed) } else { transformed } } /// This implements the same transformations as [`lisp_to_gd`], except /// that known GDScript keywords are not underscore-escaped. See the /// documentation for that function for details on the translations /// which take place. /// /// This function should be used sparingly, as it can result in /// generating GDScript code that attempts to use names like `if` as /// an identifier, which is obviously an error. However, it can also /// be used to bypass the usual keyword-escaping rules, to allow /// direct calls to the GDScript `Vector2` or `Dictionary` types, /// which are normally barred in GDLisp. pub fn lisp_to_gd_bare(name: &str) -> String { let length = name.chars().count(); let mut result = String::with_capacity(2 * length); let mut iter = name.chars().peekable(); let mut first = true; while let Some(ch) = iter.next() { if is_valid_gd_char(ch) { // Special exception if it's the first character and a digit. // Otherwise, leave it as is. if first && ch.is_ascii_digit() { result.push('_'); } result.push(ch); } else { let next = iter.peek(); match (ch, next) { ('-', Some('>')) => { iter.next(); result.push_str("_to_"); } ('?', None) => { result = format!("is_{}", result); } (_, _) => { if let Some(s) = TRANSLATIONS.get(&ch) { result.push_str(s); } else { write!(result, "_u{:04X}", ch as u32).expect("Failed to write to local string"); } } } } first = false; } result } #[cfg(test)] mod tests { use super::*; #[test] fn special_cases() { assert_eq!(lisp_to_gd("bar->baz"), "bar_to_baz"); assert_eq!(lisp_to_gd("success?"), "is_success"); assert_eq!(lisp_to_gd("->->?"), "is__to__to_"); } #[test] fn translations() { assert_eq!(lisp_to_gd("foo-bar"), "foo_bar"); assert_eq!(lisp_to_gd("foo-bar_baz"), "foo_bar_baz"); assert_eq!(lisp_to_gd(">>="), "_GT__GT__EQ_"); assert_eq!(lisp_to_gd("~a~"), "_TILDE_a_TILDE_"); assert_eq!(lisp_to_gd("!"), "_BANG_"); assert_eq!(lisp_to_gd("+1+"), "_PLUS_1_PLUS_"); assert_eq!(lisp_to_gd("1^2"), "_1_UP_2"); assert_eq!(lisp_to_gd("a&&b"), "a_AMP__AMP_b"); assert_eq!(lisp_to_gd("a||b"), "a_BAR__BAR_b"); assert_eq!(lisp_to_gd("a*b"), "a_TIMES_b"); assert_eq!(lisp_to_gd("a/b"), "a_DIV_b"); assert_eq!(lisp_to_gd("a+b"), "a_PLUS_b"); assert_eq!(lisp_to_gd("keyword:"), "keyword_COLON_"); assert_eq!(lisp_to_gd("\\"), "_BSLASH_"); assert_eq!(lisp_to_gd("?-not-at-end"), "_QMARK__not_at_end"); assert_eq!(lisp_to_gd("@annotation"), "_AT_annotation"); assert_eq!(lisp_to_gd("$jquery"), "_DSIGN_jquery"); } #[test] fn starts_with_number() { assert_eq!(lisp_to_gd("99"), "_99"); assert_eq!(lisp_to_gd("d99"), "d99"); } #[test] fn keywords() { assert_eq!(lisp_to_gd("if"), "_if"); assert_eq!(lisp_to_gd("while"), "_while"); assert_eq!(lisp_to_gd("While"), "While"); // No translation necessary assert_eq!(lisp_to_gd("PI"), "_PI"); assert_eq!(lisp_to_gd("BUTTON_LEFT"), "_BUTTON_LEFT"); } #[test] fn special_cases_bare() { assert_eq!(lisp_to_gd_bare("bar->baz"), "bar_to_baz"); assert_eq!(lisp_to_gd_bare("success?"), "is_success"); assert_eq!(lisp_to_gd_bare("->->?"), "is__to__to_"); } #[test] fn translations_bare() { assert_eq!(lisp_to_gd_bare("foo-bar"), "foo_bar"); assert_eq!(lisp_to_gd_bare("foo-bar_baz"), "foo_bar_baz"); assert_eq!(lisp_to_gd_bare(">>="), "_GT__GT__EQ_"); } #[test] fn keywords_bare() { assert_eq!(lisp_to_gd_bare("if"), "if"); assert_eq!(lisp_to_gd_bare("while"), "while"); assert_eq!(lisp_to_gd_bare("While"), "While"); // No translation necessary assert_eq!(lisp_to_gd_bare("PI"), "PI"); assert_eq!(lisp_to_gd_bare("BUTTON_LEFT"), "BUTTON_LEFT"); } #[test] fn translations_unicode() { assert_eq!(lisp_to_gd("α"), "_u03B1"); assert_eq!(lisp_to_gd("aaβaa"), "aa_u03B2aa"); assert_eq!(lisp_to_gd("⊕"), "_u2295"); assert_eq!(lisp_to_gd("😎"), "_u1F60E"); } } ================================================ FILE: src/compile/names/registered.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the struct [`RegisteredNameGenerator`] for safely //! generating (and registering) unused names based on a symbol table. use super::generator::NameGenerator; use super::contextual::ContextualNameGenerator; use crate::ir::identifier::Namespace; use crate::compile::symbol_table::SymbolTable; /// A `RegisteredNameGenerator` is based on a [`SymbolTable`] and /// generates names which do not appear in the symbol table. /// /// A contextual name generator is designed to generate names in a /// particular namespace and cannot be used in other namespaces, since /// conflicts are only detected in one namespace. Whenever a name is /// generated, it is also stored as a synthetic name in the symbol /// table. For a variant of this type which does *not* modify the /// symbol table, see [`super::contextual::ContextualNameGenerator`]. #[derive(Debug)] pub struct RegisteredNameGenerator<'a> { context: &'a mut SymbolTable, namespace: Namespace, is_local: bool, // Note: Only used if namespace is Namespace::Value } impl<'a> RegisteredNameGenerator<'a> { /// Construct a new `RegisteredNameGenerator` for function names. pub fn new_fn(context: &mut SymbolTable) -> RegisteredNameGenerator<'_> { RegisteredNameGenerator { context: context, namespace: Namespace::Function, is_local: false, } } /// Construct a new `RegisteredNameGenerator` for *local* variable names. pub fn new_local_var(context: &mut SymbolTable) -> RegisteredNameGenerator<'_> { RegisteredNameGenerator { context: context, namespace: Namespace::Value, is_local: true, } } /// Construct a new `RegisteredNameGenerator` for *global* variable names. pub fn new_global_var(context: &mut SymbolTable) -> RegisteredNameGenerator<'_> { RegisteredNameGenerator { context: context, namespace: Namespace::Value, is_local: false, } } fn register_name(&mut self, name: String) { match self.namespace { Namespace::Value => self.context.add_synthetic_var(name, self.is_local), Namespace::Function => self.context.add_synthetic_fn(name), } } } impl<'a> NameGenerator for RegisteredNameGenerator<'a> { fn generate_with(&mut self, prefix: &str) -> String { // Get the name, using the same implementation as // ContextualNameGenerator. let mut contextual_generator = ContextualNameGenerator::new(self.context, self.namespace); let name = contextual_generator.generate_with(prefix); self.register_name(name.clone()); name } } ================================================ FILE: src/compile/names/reserved.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helper constants for the collection of reserved words in GDScript. use crate::gdscript::library::gdnative::NativeClasses; use std::collections::HashSet; use std::borrow::Cow; /// All of the words which have special syntactic meaning in GDScript. /// /// Pulled from `godot/modules/gdscript_editor.cpp`. const GDSCRIPT_KEYWORDS: [&str; 42] = [ "if", "elif", "else", "for", "while", "match", "break", "continue", "pass", "return", "class", "class_name", "extends", "is", "as", "self", "tool", "signal", "func", "static", "const", "enum", "var", "onready", "export", "setget", "breakpoint", "preload", "yield", "assert", "remote", "master", "slave", "puppet", "remotesync", "mastersync", "puppetsync", "sync", "not", "and", "or", "in", ]; /// Built-in GDScript function names, which cannot be overridden by /// the programmer. const GDSCRIPT_FUNCTIONS: [&str; 93] = [ "Color8", "ColorN", "abs", "acos", "asin", "assert", "atan", "atan2", "bytes2var", "cartesian2polar", "ceil", "char", "clamp", "convert", "cos", "cosh", "db2linear", "decimals", "dectime", "deg2rad", "dict2inst", "ease", "exp", "floor", "fmod", "fposmod", "funcref", "get_stack", "hash", "inst2dict", "instance_from_id", "inverse_lerp", "is_equal_approx", "is_inf", "is_instance_valid", "is_nan", "is_zero_approx", "len", "lerp", "lerp_angle", "linear2db", "load", "log", "max", "min", "move_toward", "nearest_po2", "ord", "parse_json", "polar2cartesian", "posmod", "pow", "preload", "print", "print_debug", "print_stack", "printerr", "printraw", "prints", "printt", "push_error", "push_warning", "rad2deg", "rand_range", "rand_seed", "randf", "randi", "randomize", "range", "range_lerp", "round", "seed", "sign", "sin", "sinh", "smoothstep", "sqrt", "step_decimals", "stepify", "str", "str2var", "tan", "tanh", "to_json", "type_exists", "typeof", "validate_json", "var2bytes", "var2str", "weakref", "wrapf", "wrapi", "yield", ]; /// The GDScript top-level global constant names which are not /// included in `api.json`. /// /// Pulled from `godot/modules/gdscript_editor.cpp`. const GLOBAL_CONSTANTS: [&str; 6] = [ "TAU", "INF", "NAN", "PI", "true", "false", ]; /// The types in GDScript whose names are considered reserved. /// /// Pulled from `godot/modules/gdscript_editor.cpp`. const NAMED_PRIMITIVE_TYPES: [&str; 27] = [ "null", "bool", "int", "float", "String", "Vector2", "Rect2", "Vector3", "Transform2D", "Plane", "Quat", "AABB", "Basis", "Transform", "Color", "NodePath", "RID", "Object", "Array", "Dictionary", "PoolByteArray", "PoolIntArray", "PoolRealArray", "PoolStringArray", "PoolVector2Array", "PoolVector3Array", "PoolColorArray", ]; fn get_all_reserved_words() -> HashSet> { let api = NativeClasses::get_api_from_godot().expect("Could not read GDNative API from Godot binary"); let mut set: HashSet> = HashSet::with_capacity(100); // GDScript keywords (hard-coded into GDLisp above) set.extend(GDSCRIPT_KEYWORDS.iter().map(|x| Cow::Borrowed(*x))); // GDScript built-in function names (hard-coded into GDLisp above) set.extend(GDSCRIPT_FUNCTIONS.iter().map(|x| Cow::Borrowed(*x))); // Global constants defined in the GlobalConstants class let global_constants = api.get_global_constants(); set.extend(global_constants.constants.keys().map(|x| Cow::Owned(x.clone()))); // Extra global constants set.extend(GLOBAL_CONSTANTS.iter().map(|x| Cow::Borrowed(*x))); // Named primitive types set.extend(NAMED_PRIMITIVE_TYPES.iter().map(|x| Cow::Borrowed(*x))); set } lazy_static! { /// All of the reserved words, as a hash set which can be checked /// for membership efficiently. pub static ref RESERVED_WORDS: HashSet> = get_all_reserved_words(); } ================================================ FILE: src/compile/preload_resolver.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Mechanism for controlling how GDScript `preload` calls are //! resolved. //! //! Normally, when we need to transform a GDLisp `use` directive into //! a GDScript `preload` call, we translate the name directly, //! preserving the path and potentially changing the file extension //! (`.lisp` to `.gd`). However, for macro loading, we may end up //! needing to load using an alternative translation scheme, probably //! to some temporary directory. The trait [`PreloadResolver`] //! encompasses these behaviors. The first behavior is provided by the //! singleton struct [`DefaultPreloadResolver`] and the second by //! [`LookupPreloadResolver`]. use crate::runner::path::RPathBuf; use crate::compile::resource_type::ResourceType; use std::collections::HashMap; use std::path::PathBuf; /// A `DefaultPreloadResolver` is a [`PreloadResolver`] which passes /// through all information. This is the default resolution rule, used /// for generating runtime `.gd` files. #[derive(Debug, Clone)] pub struct DefaultPreloadResolver; /// A [`LookupPreloadResolver`] acts as a sort of proxy, translating /// names which reference real files into names which reference /// temporary files in some virtual file system constructed by the /// compiler. /// /// A `LookupPreloadResolver` retains a hashmap mapping known /// pathnames to virtual pathnames. This hashmap is used in /// [`LookupPreloadResolver::resolve_preload`]. #[derive(Debug, Clone)] pub struct LookupPreloadResolver(pub HashMap); /// A `PreloadResolver` controls how Godot `preload` calls will be /// constructed and provides a way to inject custom pathname /// resolution behavior into the compilation process. pub trait PreloadResolver { /// Given a [`RPathBuf`], this method resolves the path into a /// Godot-friendly string, suitable for use as the argument to /// `preload`. /// /// If the `PreloadResolver` cannot resolve the path according to /// its rules, then `None` should be returned. fn resolve_preload(&self, path: &RPathBuf) -> Option; /// Given a [`ResourceType`], indicates whether or not we should /// include the resource under `self`'s rules. If this returns /// false, then callers should replace the `preload` call with /// `null` or, if the resource is completely unavoidable, issue an /// error. fn include_resource(&self, res: ResourceType) -> bool; } impl LookupPreloadResolver { /// Add a new binding to the known paths for `self`, overwriting any /// existing bindings. /// /// If the key already existed in the `LookupPreloadResolver`, /// returns its previous value. Otherwise, returns `None`. pub fn insert(&mut self, key: PathBuf, value: PathBuf) -> Option { self.0.insert(key, value) } } impl PreloadResolver for DefaultPreloadResolver { /// Calls `path.to_string` and succeeds unconditionally. fn resolve_preload(&self, path: &RPathBuf) -> Option { Some(path.to_string()) } /// Constantly returns true. All resource types are included in the /// default resolver rule. fn include_resource(&self, _res: ResourceType) -> bool { true } } impl PreloadResolver for LookupPreloadResolver { /// Translates the name according to the hashmap within `self`. If /// the name is not found in the hashmap, then this method returns /// `None`. fn resolve_preload(&self, path: &RPathBuf) -> Option { self.0.get(path.path()).map(RPathBuf::path_to_string) } /// `LookupPreloadResolver` only suggests including GDLisp source /// files, i.e. [`ResourceType::GDLispSource`]. All other resource /// types result in false. fn include_resource(&self, res: ResourceType) -> bool { res == ResourceType::GDLispSource } } ================================================ FILE: src/compile/resource_type.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! The various types of resources GDLisp might encounter. //! //! [`ResourceType`] enumerates the possible resource types GDLisp //! might attempt to load. use crate::ir::import::{ImportDecl, ImportDetails}; use crate::pipeline::Pipeline; use super::error::{GDError, GDErrorF}; use std::convert::AsRef; use std::path::Path; use std::ffi::OsStr; /// The various resource types a `use` statement might encounter. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ResourceType { /// A GDLisp source file. Must be compiled to GDScript. GDLisp /// source files support the full breadth of import declaration /// types. GDLispSource, /// A GDScript source file. The file must exist, and the import must /// be a simple import or an aliased import. Open or restricted /// imports are not allowed. GDScriptSource, /// A packed scene file. The file must exist, and the import must be /// a simple import or an aliased import. Open or restricted imports /// are not allowed. PackedScene, /// Any other resource file. The file must exist, and the import /// must be a simple import or an aliased import. Open or restricted /// imports are not allowed. Miscellaneous, } impl ResourceType { /// Constructs a [`ResourceType`] for the type of resource at the /// given path. The file at `path` will not be read; all resource /// type inference is done by looking at the file name, specifically /// its extension. pub fn from_path

+ ?Sized>(path: &P) -> ResourceType { ResourceType::from_file_extension(&path.as_ref().extension().unwrap_or_else(|| OsStr::new(""))) } /// Returns the appropriate resource type given a file extension. /// `ext` should be an all-lowercase file extension excluding the /// initial dot. If the extension is not recognized, then /// [`ResourceType::Miscellaneous`] is returned. pub fn from_file_extension + ?Sized>(ext: &S) -> ResourceType { let ext = ext.as_ref(); if ext == "lisp" { ResourceType::GDLispSource } else if ext == "gd" { ResourceType::GDScriptSource } else if ext == "tscn" { ResourceType::PackedScene } else { ResourceType::Miscellaneous } } /// Whether or not the resource type can have macros defined in it. /// Only [`ResourceType::GDLispSource`] can have macros. pub fn can_have_macros(&self) -> bool { *self == ResourceType::GDLispSource } /// Whether the given import declaration is allowed for this /// particular resource type. /// /// GDLisp source files allow all import types, whereas other /// resource types are restricted to simple or aliased imports. pub fn is_import_allowed(&self, import: &ImportDecl) -> bool { if *self == ResourceType::GDLispSource { true // GDLispSource allows all imports } else { // For other resources, it must be ImportDetails::Named matches!(import.details, ImportDetails::Named(_)) } } /// Checks [`ResourceType::is_import_allowed`]. If it is false, this /// method issues an appropriate error via `Err`. Otherwise, returns /// `Ok(())`. pub fn check_import(_pipeline: &Pipeline, import: &ImportDecl) -> Result<(), GDError> { // if !pipeline.file_exists(import.filename.path()) { // return Err(GDError::ResourceDoesNotExist(import.filename.to_string())); // } let res_type = ResourceType::from_path(import.filename.path()); if !res_type.is_import_allowed(import) { return Err(GDError::new(GDErrorF::InvalidImportOnResource(import.filename.to_string()), import.pos)); } Ok(()) } } /// Construct a `ResourceType` from a reference to a path. Delegates /// to [`ResourceType::from_path`]. impl From<&Path> for ResourceType { fn from(path: &Path) -> ResourceType { ResourceType::from_path(path) } } /// Construct a `ResourceType` from the path referenced by the import /// declaration. impl From<&ImportDecl> for ResourceType { fn from(imp: &ImportDecl) -> ResourceType { ResourceType::from_path(imp.filename.path()) } } ================================================ FILE: src/compile/special_form/closure.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helpers for generating closures, for use in constructs such as //! lambdas and lambda classes. use crate::ir::expr::{Locals, Functions, LambdaClass, LocalFnClause}; use crate::compile::symbol_table::function_call::FnCall; use crate::compile::symbol_table::local_var::VarScope; use crate::compile::symbol_table::SymbolTable; use crate::pipeline::source::SourceOffset; type IRArgList = crate::ir::arglist::ordinary::ArgList; type IRExpr = crate::ir::expr::Expr; /// A `ClosureData` contains information about collections of /// variables and functions to close over. #[derive(PartialEq, Eq, Debug, Clone)] pub struct ClosureData { /// The collection of all local variables in-scope which need to be /// closed over. pub closure_vars: Locals, /// The collection of all local variables introduced in the /// closure's scope or a strictly larger one. This is similar to /// `closed_vars` and will always be a superset of that collection, /// but this collection also includes variables which are introduced /// as part of the closure's scope, most commonly arguments to the /// lambda which created the closure. Such variables do not need to /// be closed over (since they do not exist outside the closure), /// but it is still meaningful to ask what their `access_type` is /// and whether they need a GDLisp cell. pub all_vars: Locals, /// The collection of all local functions in-scope which need to be /// closed over. pub closure_fns: Functions, } /// A function consists of an argument list and a body expression. /// This simple wrapper couples the two, so that we can pass them as a /// pair to [`ClosureData`] methods. #[derive(Clone, Debug)] pub struct Function<'a, 'b> { pub args: &'a IRArgList, pub body: &'b IRExpr, } /// A simple wrapper around a collection of [`LocalFnClause`]. A /// `labels` SCC is a collection of interconnected local function /// clauses, which reference each other in a circular fashion. More /// generally, this structure can be used for any collection of local /// function clauses, regardless of referencing requirements. #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] pub struct LabelsComponent<'a, 'b>(pub &'a [&'b LocalFnClause]); impl ClosureData { /// Purges globals from `self.closure_vars`, as though via the /// module-level function [`purge_globals`]. /// /// Equivalent to `purge_globals(&mut self.closure_vars, table)`. pub fn purge_globals(&mut self, table: &SymbolTable) { purge_globals(&mut self.closure_vars, table) } /// Assuming `self` contains all of the relevant closure information /// on the GDLisp side, this function constructs a list of the /// necessary variables which need to be looked up on the GDScript /// side to construct or utilize the closure. /// /// Every variable in `self.closure_vars` and every function in /// `self.closure_fns` will be considered for inclusion in the /// result. Specifically, each variable will be looked up in `table` /// and, if the result has a /// [`LocalVar::simple_name`](crate::compile::symbol_table::local_var::LocalVar::simple_name), /// then it will be included in the returned translation vector. /// Likewise, each function is looked up in `table` and, if /// [`closure_fn_to_gd_var`] returns a name, then that name is added /// to the translation vector. /// /// `self.all_vars` is not considered during this calculation. /// /// This function can be used in two similar ways. By passing the /// lambda symbol table as `table` to this function, the resulting /// vector will contain the GDScript names used to refer to the /// closure variables from *within* the closure. This is useful for /// building the lambda's constructor function. By passing the /// enclosing outer table as `table`, on the other hand, the /// resulting vector will contain the names used to refer to the /// closure variables from the scope *surrounding* the closure. This /// is useful for building the expression that will *call* the /// lambda's constructor function. /// /// # Panics /// /// It is a precondition of this function that every name in /// `self.closure_vars` and in `self.closure_fns` appears in `table` /// under the appropriate namespace. If any names are missing, then /// this function will panic. pub fn to_gd_closure_vars(&self, table: &SymbolTable) -> Vec { let mut gd_closure_vars = Vec::new(); // Get closure variables for lisp_name in self.closure_vars.names() { let var = table.get_var(lisp_name).unwrap_or_else(|| { panic!("Internal error compiling lambda variable {}", lisp_name); }); if let Some(name) = var.simple_name() { gd_closure_vars.push(name.to_owned()); } } // Get closure functions (functions also get moved to the variable // namespace when closed around, since GDScript doesn't treat // function names as first-class objects) for lisp_name in self.closure_fns.names() { let (call, _) = table.get_fn(lisp_name).unwrap_or_else(|| { panic!("Internal error compiling lambda variable {}", lisp_name); }); if let Some(var) = closure_fn_to_gd_var(call) { gd_closure_vars.push(var); } } gd_closure_vars } } impl<'a, 'b> Function<'a, 'b> { /// Convenience function to construct a new `Function`. pub fn new(args: &'a IRArgList, body: &'b IRExpr) -> Self { Function { args, body } } } impl<'a, 'b> From> for ClosureData { /// If we're constructing a simple lambda function, we can convert /// its [`Function`] value into a [`ClosureData`] in a well-defined /// way. fn from(function: Function<'a, 'b>) -> ClosureData { let (all_vars, closure_fns) = function.body.get_names(); let mut closure_vars = all_vars.clone(); for arg in function.args.iter_vars() { closure_vars.remove(arg); } ClosureData { closure_vars, all_vars, closure_fns } } } impl<'a> From<&'a LambdaClass> for ClosureData { /// A lambda class involves closing around any variables inside of /// it, similar to a lambda function. The lambda class case is /// somewhat more complex, as it consists of multiple functions and, /// in general, a `self` variable that gets implicitly overwritten /// by the class' `self`. fn from(class: &'a LambdaClass) -> ClosureData { let (mut closure_vars, mut closure_fns) = class.constructor_or_default(SourceOffset::from(0)).get_names(); for d in &class.decls { let (decl_vars, decl_fns) = d.get_names(); closure_vars.merge_with(decl_vars); closure_fns.merge_with(decl_fns); } closure_vars.remove("self"); // Don't close around self; we get a new self ClosureData { closure_vars: closure_vars.clone(), all_vars: closure_vars, closure_fns } } } impl<'a, 'b> From> for ClosureData { /// A strongly-connected component (SCC) of a `labels` clause is a /// collection of interconnected local function clauses. Those /// function clauses each have their own closure, and together they /// have a common closure for the object which encapsulates them. /// /// Note that all of the function names in the SCC are in scope for /// the duration of all of the function bodies, so the function /// names themselves will never appear in the resulting /// `closure_fns`. fn from(comp: LabelsComponent<'a, 'b>) -> ClosureData { let LabelsComponent(clauses) = comp; let mut closure_vars = Locals::new(); let mut closure_fns = Functions::new(); let mut all_vars = Locals::new(); for clause in clauses { let (mut inner_vars, inner_fns) = clause.body.get_names(); all_vars.merge_with(inner_vars.clone()); for arg in clause.args.iter_vars() { inner_vars.remove(arg); } closure_vars.merge_with(inner_vars); closure_fns.merge_with(inner_fns); } // Function names are in scope for the duration of their own bodies for clause in clauses { closure_fns.remove(&clause.name); } ClosureData { closure_vars, all_vars, closure_fns } } } /// Removes all of the variables from `vars` whose scope (according to /// the corresponding entry in `table`) is /// [`VarScope::GlobalVar`](crate::compile::symbol_table::local_var::VarScope::GlobalVar). /// /// Lambdas are lifted to the file-level scope. A variable with scope /// `VarScope::GlobalVar` is defined either at the file-level scope or /// as a superglobal. In either case, the lambda will still have a /// reference to the variable without any help, so we don't need to /// close around those variables. This function removes from `vars` /// the variables which it is unnecessary to explicitly create /// closures around. pub fn purge_globals(vars: &mut Locals, table: &SymbolTable) { vars.retain(|var, _| { table.get_var(var).map_or(true, |v| v.scope != VarScope::GlobalVar) }); } /// If the function call comes from a local variable (most commonly, /// if it was built out of `flet` or `labels`), then this function /// returns the name of that variable, as a `String`. If the function /// does *not* come from a local variable, then this function returns /// `None`. pub fn closure_fn_to_gd_var(call: &FnCall) -> Option { call.scope.local_name().map(str::to_owned) } ================================================ FILE: src/compile/special_form/flet.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::ir; use crate::ir::expr::LocalFnClause; use crate::compile::body::builder::StmtBuilder; use crate::compile::symbol_table::{HasSymbolTable, SymbolTable}; use crate::compile::symbol_table::function_call::{FnCall, FnSpecs, FnScope, FnName}; use crate::compile::symbol_table::local_var::VarName; use crate::compile::error::GDError; use crate::compile::stateful::{StExpr, NeedsResult}; use crate::compile::stmt_wrapper; use crate::compile::factory; use crate::compile::frame::CompilerFrame; use crate::compile::names::generator::NameGenerator; use crate::compile::names::registered::RegisteredNameGenerator; use crate::gdscript::decl::{self, Decl, DeclF}; use crate::graph::Graph; use crate::graph::top_sort::top_sort; use crate::graph::tarjan; use crate::pipeline::source::SourceOffset; use super::lambda; use std::convert::AsRef; type IRExpr = ir::expr::Expr; type IRArgList = ir::arglist::ordinary::ArgList; pub fn compile_flet(frame: &mut CompilerFrame, clauses: &[LocalFnClause], body: &IRExpr, needs_result: NeedsResult, minimalist: bool, pos: SourceOffset) -> Result { let local_fns = clauses.iter().map(|clause| { let call = compile_flet_call(frame, clause.args.to_owned(), &clause.body, minimalist, pos)?; Ok((clause.name.to_owned(), call)) }).collect::, GDError>>()?; frame.with_local_fns(&mut local_fns.into_iter(), |frame| { frame.compile_expr(body, needs_result) }) } fn compile_flet_call(frame: &mut CompilerFrame, args: IRArgList, body: &IRExpr, minimalist: bool, pos: SourceOffset) -> Result { if is_declaration_semiglobal(&args, body, frame.table) { // No closure vars and any closure fns (if there are any) are // free of closures, so we can compile to SemiGlobal. let gd_name = RegisteredNameGenerator::new_fn(frame.table).generate_with("_flet"); let func = factory::declare_function(frame, gd_name.clone(), args.clone(), body, &stmt_wrapper::Return)?; frame.builder.add_helper(Decl::new(DeclF::FnDecl(decl::Static::IsStatic, func), pos)); let specs = FnSpecs::from(args); Ok(FnCall { scope: FnScope::SemiGlobal, object: FnName::FileConstant, function: gd_name, specs, is_macro: false, }) } else { // Have to make a full closure object. let stmt = lambda::compile_lambda_stmt(frame, &args, body, minimalist, pos)?.expr; let local_name = factory::declare_var(&mut RegisteredNameGenerator::new_local_var(frame.table), frame.builder, "_flet", Some(stmt), pos); let specs = FnSpecs::from(args); Ok(FnCall { scope: FnScope::Local(local_name.clone()), object: FnName::on_local_var(VarName::Local(local_name)), function: "call_func".to_owned(), specs, is_macro: false, }) } } pub fn compile_labels(frame: &mut CompilerFrame, clauses: &[LocalFnClause], body: &IRExpr, needs_result: NeedsResult, pos: SourceOffset) -> Result { // TODO This is rife with string cloning, because of the sloppy way // Graph is implemented. Once we fix Graph, we can eliminate some // clones here. let mut dependencies = Graph::from_nodes(clauses.iter().map(|clause| clause.name.clone())); for clause in clauses { for ref_name in clause.body.get_functions().into_names() { if dependencies.has_node(&ref_name) { dependencies.add_edge_no_dup(clause.name.clone(), ref_name); } } } let sccs = tarjan::find_scc(&dependencies); let collated_graph = tarjan::build_scc_graph(&dependencies, &sccs); let collated_graph = collated_graph.transpose(); // We need the arrows pointing in load order, not dependency order let ordering = top_sort(&collated_graph) .expect("SCC detection failed (cycle in resulting graph)") .into_iter().copied(); let mut alg = CompileLabelsRecAlgorithm { frame, body, needs_result, pos, clauses, full_graph: &dependencies, sccs: &sccs, ordering: ordering }; alg.compile_labels_rec() } struct CompileLabelsRecAlgorithm<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, I> { frame: &'a mut CompilerFrame<'b, 'c, 'd, 'e, 'f, StmtBuilder>, body: &'g IRExpr, needs_result: NeedsResult, pos: SourceOffset, clauses: &'h [LocalFnClause], full_graph: &'i Graph, sccs: &'j tarjan::SCCSummary<'k, String>, ordering: I, } impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, I : Iterator> CompileLabelsRecAlgorithm<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, I> { fn compile_labels_rec(&mut self) -> Result { if let Some(current_scc_idx) = self.ordering.next() { let tarjan::SCC(current_scc) = self.sccs.get_scc_by_id(current_scc_idx).expect("SCC detection failed (invalid ID)"); if current_scc.is_empty() { // That's weird. But whatever. No action needed. self.compile_labels_rec() } else { let name = current_scc.iter().next().expect("Internal error in SCC detection (no first element?)"); if current_scc.len() == 1 && !self.full_graph.has_edge(name, name) { // Simple FLet-like case. let name = current_scc.iter().next().expect("Internal error in SCC detection (no first element?)"); let clause = self.clauses.iter().find(|clause| clause.name == **name).expect("Internal error in SCC detection (no function found?)"); let call = compile_flet_call(self.frame, clause.args.to_owned(), &clause.body, self.frame.compiler.is_minimalist(), self.pos)?; self.with_local_fn((*name).to_owned(), call, |alg| { alg.compile_labels_rec() }) } else { // Complicated mutual recursion case. let mut relevant_clauses = Vec::new(); for name in current_scc { let clause = self.clauses.iter().find(|clause| clause.name == **name).expect("Internal error in SCC detection (no function found?)"); relevant_clauses.push(clause); } // Go ahead and sort them just so we guarantee a consistent order for testing purposes. relevant_clauses.sort_by_key(|clause| &clause.name); let calls = lambda::compile_labels_scc(self.frame, &relevant_clauses[..], self.pos)?; self.with_local_fns(&mut calls.into_iter(), |alg| { alg.compile_labels_rec() }) } } } else { self.frame.compile_expr(self.body, self.needs_result) } } } impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, I> HasSymbolTable for CompileLabelsRecAlgorithm<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, I> { fn get_symbol_table(&self) -> &SymbolTable { self.frame.get_symbol_table() } fn get_symbol_table_mut(&mut self) -> &mut SymbolTable { self.frame.get_symbol_table_mut() } } /// A function declaration is eligible to be semiglobal if all of the /// following are true. /// /// * All functions referenced in the body of the function are /// non-local (i.e. [`FnScope::is_local`] returns false on their /// scope). /// /// * All variables referenced in the body of the function are /// arguments to the function. /// /// Semiglobal functions do not need to have explicit closure objects /// constructed for them and can instead be hoisted on the GDScript /// side into top-level global functions. pub fn is_declaration_semiglobal(args: &IRArgList, body: &IRExpr, table: &SymbolTable) -> bool { let (closure_vars, closure_fns) = body.get_names(); let arg_var_names: Vec<_> = args.iter_vars().collect(); // All referenced functions should be Global or SemiGlobal and all // referenced local variables should be found in the argument list. let mut closure_names = closure_vars.names(); closure_names.all(|x| arg_var_names.contains(&x)) && all_names_are_nonlocal(closure_fns.names(), table) } fn all_names_are_nonlocal(mut names: I, table: &SymbolTable) -> bool where I : Iterator, T : AsRef { names.all(|name| { table.get_fn(name.as_ref()).map_or(false, |(call, _)| !call.scope.is_local()) }) } ================================================ FILE: src/compile/special_form/lambda.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::util::unzip_err; use crate::ir; use crate::ir::expr::{Locals, Functions, LocalFnClause}; use crate::ir::access_type::AccessType; use crate::compile::{Compiler, StExpr}; use crate::compile::frame::CompilerFrame; use crate::compile::body::builder::StmtBuilder; use crate::compile::symbol_table::SymbolTable; use crate::compile::symbol_table::inner::InnerSymbolTable; use crate::compile::symbol_table::local_var::{LocalVar, VarScope, VarName}; use crate::compile::symbol_table::function_call::{FnCall, FnSpecs, FnScope, FnName, OuterStaticRef}; use crate::compile::symbol_table::call_magic::CallMagic; use crate::compile::stmt_wrapper; use crate::compile::error::{GDError, GDErrorF}; use crate::compile::stateful::SideEffects; use crate::compile::names::{self, NameTrans}; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::names::registered::RegisteredNameGenerator; use crate::compile::names::generator::NameGenerator; use crate::gdscript::stmt::{Stmt, StmtF}; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::decl::{self, Decl, DeclF, VarDecl}; use crate::gdscript::class_extends::ClassExtends; use crate::gdscript::arglist::ArgList; use crate::gdscript::library; use crate::gdscript::inner_class::{self, NeedsOuterClassRef}; use crate::pipeline::Pipeline; use crate::pipeline::can_load::CanLoad; use crate::pipeline::source::SourceOffset; use super::lambda_vararg::generate_lambda_class; use super::closure::{ClosureData, Function, LabelsComponent}; use std::borrow::Borrow; use std::cmp::max; type IRExpr = ir::expr::Expr; type IRArgList = ir::arglist::ordinary::ArgList; pub fn compile_labels_scc(frame: &mut CompilerFrame, clauses: &[&LocalFnClause], pos: SourceOffset) -> Result, GDError> { // In the perfect world, we would do all of our operations *on* // frame rather than destructuring that variable here. But, the way // this function is currently written, we need access to both // frame.table and lambda_table throughout most of the computation. // Perhaps there's a way to refactor it, but it won't be easy. let CompilerFrame { compiler, pipeline, table, builder, class_scope } = frame; let mut class_scope = class_scope.closure_mut(); let closure = { let mut closure = ClosureData::from(LabelsComponent(clauses)); // No need to close around global variables, as they're available everywhere closure.purge_globals(table); closure }; // Generate an outer class ref if we need access to the scope from // within the lambda class. let mut outer_ref_name = String::new(); let needs_outer_ref = closure.closure_fns.needs_outer_class_ref(table); if needs_outer_ref { outer_ref_name = compiler.name_generator().generate_with(inner_class::OUTER_REFERENCE_NAME); } // Determine a name for the global class to represent the labels. let class_name = RegisteredNameGenerator::new_global_var(table).generate_with("_Labels"); // Bind all of the closure variables, closure functions, and global // variables inside. let mut lambda_table = SymbolTable::with_synthetics_from(table); locally_bind_vars(compiler, table, &mut lambda_table, &closure.closure_vars, &[], pos)?; locally_bind_fns(compiler, *pipeline, table, &mut lambda_table, &closure.closure_fns, pos, &OuterStaticRef::InnerInstanceVar(&outer_ref_name))?; copy_global_vars(table, &mut lambda_table); // Convert the closures to GDScript names. let gd_closure_vars = closure.to_gd_closure_vars(&lambda_table); let gd_src_closure_vars = closure.to_gd_closure_vars(table); let local_var_name = RegisteredNameGenerator::new_local_var(table).generate_with("_locals"); let mut lambda_table = InnerSymbolTable::new(lambda_table, table); // Bind the functions themselves let named_clauses = generate_names_for_scc_clauses(clauses.iter().copied(), compiler.name_generator()); for (func_name, clause) in &named_clauses { let specs = FnSpecs::from(clause.args.to_owned()); let fn_call = special_local_fn_call(local_var_name.clone(), func_name.clone(), specs); lambda_table.set_fn(clause.name.to_owned(), fn_call, CallMagic::DefaultCall); } let (bound_calls, functions) = unzip_err::, Vec<_>, _, _, _>(named_clauses.iter().map(|(func_name, clause)| { let mut lambda_table = InnerSymbolTable::cloned_from(&mut lambda_table); // New table for this particular function let mut lambda_builder = StmtBuilder::new(); let (arglist, gd_args) = clause.args.clone().into_gd_arglist(&mut RegisteredNameGenerator::new_local_var(&mut lambda_table)); // Bind the function arguments for NameTrans { lisp_name: arg, gd_name: gd_arg } in &gd_args { let access_type = *closure.all_vars.get(arg).unwrap_or(&AccessType::None); lambda_table.set_var(arg.to_owned(), LocalVar::local(gd_arg.to_owned(), access_type)); wrap_in_cell_if_needed(arg, gd_arg, &closure.all_vars, &mut lambda_builder, pos); } compiler.frame(pipeline, &mut lambda_builder, &mut lambda_table, &mut *class_scope).compile_stmt(&stmt_wrapper::Return, &clause.body)?; let lambda_body = lambda_builder.build_into(*builder); let func_name = func_name.to_owned(); let func = decl::FnDecl { name: func_name.clone(), args: arglist, body: lambda_body, }; let call = FnCall { scope: FnScope::SpecialLocal(local_var_name.clone()), object: FnName::on_local_var(VarName::local(&local_var_name)), function: func_name, specs: FnSpecs::from(clause.args.to_owned()), is_macro: false, }; Ok(((clause.name.to_owned(), call), func)) }))?; let mut constructor_body = Vec::new(); for var in &gd_closure_vars { constructor_body.push(super::assign_to_compiler(var.to_string(), var.to_string(), pos)); } let constructor = decl::FnDecl { name: String::from(library::CONSTRUCTOR_NAME), args: ArgList::required(gd_closure_vars.iter().map(|x| x.to_owned()).collect()), body: constructor_body, }; let mut class_body = vec!(); for var in &gd_closure_vars { class_body.push(Decl::new(DeclF::VarDecl(VarDecl::simple(var.clone())), pos)); } class_body.push(Decl::new(DeclF::FnDecl(decl::Static::NonStatic, constructor), pos)); for func in functions { class_body.push(Decl::new(DeclF::FnDecl(decl::Static::NonStatic, func), pos)); } let mut class = decl::ClassDecl { name: class_name.clone(), extends: ClassExtends::SimpleIdentifier(String::from("Reference")), body: class_body, }; if needs_outer_ref { inner_class::add_outer_class_ref_named(&mut class, compiler.preload_resolver(), *pipeline, outer_ref_name, pos); } builder.add_helper(Decl::new(DeclF::ClassDecl(class), pos)); let constructor_args: Vec<_> = gd_src_closure_vars.into_iter().map(|x| Expr::new(ExprF::Var(x), pos)).collect(); let expr = Expr::call(Some(Expr::new(ExprF::Var(class_name), pos)), "new", constructor_args, pos); builder.append(Stmt::new(StmtF::VarDecl(local_var_name, expr), pos)); Ok(bound_calls) } /// Generate an appropriate name for an SCC function generated from a /// GDLisp function with the given name. fn generate_scc_name(original_name: &str, gen: &mut FreshNameGenerator) -> String { let name_prefix = format!("_fn_{}", names::lisp_to_gd(original_name)); gen.generate_with(&name_prefix) } fn generate_names_for_scc_clauses<'a>(clauses: impl Iterator, gen: &mut FreshNameGenerator) -> Vec<(String, &'a LocalFnClause)> { clauses.map(|clause| { let func_name = generate_scc_name(&clause.name, gen); (func_name, clause) }).collect() } /// Compiles an [`FnCall`] for a `labels`-style function with the /// given name and specs, on the given labels object. pub fn special_local_fn_call(labels_var: String, function: String, specs: FnSpecs) -> FnCall { FnCall { scope: FnScope::SpecialLocal(labels_var), object: FnName::OnLocalScope, function, specs, is_macro: false, } } pub fn locally_bind_vars(compiler: &mut Compiler, table: &SymbolTable, lambda_table: &mut SymbolTable, closure_vars: &Locals, forbidden_names: &[&str], _pos: SourceOffset) // _pos unused right now, might need it later :) -> Result<(), GDError> { for (var, _access_type, var_pos) in closure_vars.iter_with_offset() { // Ensure the variable actually exists match table.get_var(var.borrow()) { None => return Err(GDError::new(GDErrorF::NoSuchVar(var.borrow().to_owned()), var_pos)), Some(gdvar) => { let mut new_var = gdvar.to_owned(); // Ad-hoc rule for closing around self (TODO Generalize?) if new_var == LocalVar::self_var() { // TODO Special case over in VarName? new_var = LocalVar::local(compiler.name_generator().generate_with("_self"), AccessType::ClosedRead); } let mut name_generator = RegisteredNameGenerator::new_local_var(lambda_table); protect_closure_var_name(&mut name_generator, &mut new_var, forbidden_names); lambda_table.set_var(var.borrow().to_owned(), new_var); } }; } Ok(()) } fn protect_closure_var_name(gen: &mut impl NameGenerator, var: &mut LocalVar, forbidden_names: &[&str]) { if let Some(original_name) = var.simple_name() { let mut final_name = original_name.to_owned(); while forbidden_names.contains(&&*final_name) { final_name = gen.generate_with(original_name); } var.set_simple_name(final_name); } } pub fn locally_bind_fns(compiler: &mut Compiler, pipeline: &L, table: &SymbolTable, lambda_table: &mut SymbolTable, closure_fns: &Functions, _pos: SourceOffset, // Unused right now, might need it later :) outer_static_ref: &OuterStaticRef<'_>) -> Result<(), GDError> where L : CanLoad { for (func, (), func_pos) in closure_fns.iter_with_offset() { // Ensure the function actually exists match table.get_fn(func.borrow()) { None => { return Err(GDError::new(GDErrorF::NoSuchFn(func.borrow().to_owned()), func_pos)) } Some((call, magic)) => { let mut call = call.clone(); call.object.update_for_inner_scope(outer_static_ref, compiler.preload_resolver(), pipeline); lambda_table.set_fn(func.borrow().to_owned(), call, magic.clone()); } }; } Ok(()) } /// Copies all of the variables from `src_table` to `dest_table` whose /// [`VarScope`] is [`VarScope::GlobalVar`]. This function does not /// modify the function namespace in either table. pub fn copy_global_vars(src_table: &SymbolTable, dest_table: &mut SymbolTable) { for (name, var) in src_table.vars() { if var.scope == VarScope::GlobalVar { dest_table.set_var(name.to_owned(), var.clone()); } } } /// Compiles a call to a lambda class constructor, where the lambda /// class has name `class_name`. The closure variables are given by /// `gd_src_closure_vars`, and the resulting expression will be given /// source offset `pos`. /// /// Any expression in `suffix_args` will be suffixed onto the end of /// the closure variable constructor arguments verbatim and can be /// used to pass custom arguments to the constructor. /// /// Despite technically being a method call, lambda constructors are /// never stateful, so `side_effects` on the result will always be /// [`SideEffects::None`]. pub fn make_constructor_call(class_name: String, gd_src_closure_vars: impl IntoIterator, suffix_args: Vec, pos: SourceOffset) -> StExpr { let side_effects = suffix_args.iter().map(|x| x.side_effects).fold(SideEffects::None, max); let mut constructor_args: Vec<_> = gd_src_closure_vars.into_iter().map(|x| Expr::new(ExprF::Var(x), pos)).collect(); constructor_args.extend(suffix_args.into_iter().map(|x| x.expr)); let expr = Expr::call(Some(Expr::new(ExprF::Var(class_name), pos)), "new", constructor_args, pos); StExpr { expr, side_effects } } fn wrap_in_cell_if_needed(name: &str, gd_name: &str, all_vars: &Locals, lambda_builder: &mut StmtBuilder, pos: SourceOffset) { if all_vars.get(name).unwrap_or(&AccessType::None).requires_cell() { lambda_builder.append(Stmt::simple_assign(Expr::var(gd_name, pos), library::cell::construct_cell(Expr::var(gd_name, pos)), pos)); } } pub fn compile_lambda_stmt(frame: &mut CompilerFrame, args: &IRArgList, body: &IRExpr, minimalist: bool, pos: SourceOffset) -> Result { // In the perfect world, we would do all of our operations *on* // frame rather than destructuring that variable here. But, the way // this function is currently written, we need access to both // frame.table and lambda_table throughout most of the computation. // Perhaps there's a way to refactor it, but it won't be easy. let CompilerFrame { compiler, pipeline, table, builder, class_scope } = frame; let mut class_scope = class_scope.closure_mut(); let closure = { let mut closure = ClosureData::from(Function::new(args, body)); // No need to close around global variables, as they're available // everywhere. closure.purge_globals(table); closure }; // Determine the eventual class name. let class_name = RegisteredNameGenerator::new_global_var(table).generate_with("_LambdaBlock"); let mut lambda_table = SymbolTable::with_synthetics_from(table); let (arglist, gd_args) = args.clone().into_gd_arglist(&mut RegisteredNameGenerator::new_local_var(&mut lambda_table)); // Bind the arguments to the lambda in the new lambda table. for arg in &gd_args { let access_type = *closure.all_vars.get(&arg.lisp_name).unwrap_or(&AccessType::None); lambda_table.set_var(arg.lisp_name.to_owned(), LocalVar::local(arg.gd_name.to_owned(), access_type)); } // Generate an outer class ref if we need access to the scope from // within the lambda. let mut outer_ref_name = String::new(); let needs_outer_ref = closure.closure_fns.needs_outer_class_ref(table); if needs_outer_ref { outer_ref_name = compiler.name_generator().generate_with(inner_class::OUTER_REFERENCE_NAME); } // Bind all of the closure variables, closure functions, and global // variables inside. locally_bind_vars(compiler, table, &mut lambda_table, &closure.closure_vars, &[], pos)?; locally_bind_fns(compiler, *pipeline, table, &mut lambda_table, &closure.closure_fns, pos, &OuterStaticRef::InnerInstanceVar(&outer_ref_name))?; copy_global_vars(table, &mut lambda_table); // Convert the closures to GDScript names. let gd_closure_vars = closure.to_gd_closure_vars(&lambda_table); let gd_src_closure_vars = closure.to_gd_closure_vars(table); let mut lambda_table = InnerSymbolTable::new(lambda_table, table); let lambda_body = { let mut lambda_builder = StmtBuilder::new(); // Wrap arguments in cells, as needed. for NameTrans { lisp_name: arg, gd_name: gd_arg } in &gd_args { wrap_in_cell_if_needed(arg, gd_arg, &closure.all_vars, &mut lambda_builder, pos); } // Compile the lambda body. compiler.frame(pipeline, &mut lambda_builder, &mut lambda_table, &mut *class_scope).compile_stmt(&stmt_wrapper::Return, body)?; lambda_builder.build_into(*builder) }; // Generate the enclosing class. let mut class = generate_lambda_class(class_name.clone(), args.clone().into(), arglist, &gd_closure_vars, lambda_body, minimalist, pos); // Add outer class reference. if needs_outer_ref { inner_class::add_outer_class_ref_named(&mut class, compiler.preload_resolver(), *pipeline, outer_ref_name, pos); } // Place the resulting values in the builder. builder.add_helper(Decl::new(DeclF::ClassDecl(class), pos)); Ok(make_constructor_call(class_name, gd_src_closure_vars, vec!(), pos)) } /// This function compiles a GDLisp function reference, as constructed /// using the `(function ...)` special form. GDLisp function /// references compile to instances of private helper classes, similar /// to lambda expressions. /// /// If the function has scope [`FnScope::Local`], then it is already a /// local variable, and the name of that local variable will be /// returned without constructing an unnecessary second helper class. /// If the function has scope [`FnScope::SpecialLocal`], then the /// resulting helper class will have a single constructor argument: /// the special local function object. Otherwise, the constructor /// function will have no constructor arguments. pub fn compile_function_ref(compiler: &mut Compiler, pipeline: &mut Pipeline, builder: &mut StmtBuilder, table: &mut SymbolTable, func: FnCall, minimalist: bool, pos: SourceOffset) -> Result { if let FnScope::Local(name) = func.scope { // If the function is already bound to a local variable, we can // happily reuse that variable. This is most likely to come up if // we take a function ref of an flet function with a nontrivial // closure. Ok(StExpr { expr: Expr::new(ExprF::Var(name), pos), side_effects: SideEffects::None }) } else { let arglist = simple_arg_names(func.specs.runtime_arity()); // Normally, function references are either to local variables (in // which case, we have a reference already) or functions (in which // case, there is no closure so a simple top-level class will do). // However, if the referent is from a nontrivial SCC of a labels // block, then we have to paradoxically close around it, since it // *is* local but doesn't satisfy the funcref interface. let gd_src_closure_vars = if let FnScope::SpecialLocal(name) = func.scope { vec!(name) } else { vec!() }; let object = func.object.clone().into_inner_scope(&OuterStaticRef::InnerStatic, compiler.preload_resolver(), pipeline).into_expr(pos); let body = Stmt::new( StmtF::ReturnStmt( Expr::call(object, &func.function, arglist.all_args_iter().map(|x| Expr::var(x, pos)).collect(), pos) ), pos, ); // Generate the class and the constructor call. let class_name = RegisteredNameGenerator::new_global_var(table).generate_with("_FunctionRefBlock"); let class = generate_lambda_class(class_name.clone(), func.specs, arglist, &gd_src_closure_vars, vec!(body), minimalist, pos); builder.add_helper(Decl::new(DeclF::ClassDecl(class), pos)); Ok(make_constructor_call(class_name, gd_src_closure_vars, vec!(), pos)) } } /// Simple helper function to generate basic function arguments with a /// regular naming scheme. This function makes no effort to avoid /// conflicts with other variables and should only be used in scopes /// where such conflicts are impossible. /// /// The names generated by this function begin with "arg" and are /// followed by a numerical index from 0 up to (exclusive) `count`. /// /// # Examples /// /// ``` /// # use gdlisp::compile::special_form::lambda::simple_arg_names; /// # use gdlisp::gdscript::arglist::ArgList; /// assert_eq!(simple_arg_names(0), ArgList::required(vec!())); /// assert_eq!(simple_arg_names(4), ArgList::required(vec!("arg0".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string()))); /// ``` pub fn simple_arg_names(count: usize) -> ArgList { let arg_names = (0..count).map(|i| format!("arg{}", i)).collect(); ArgList::required(arg_names) } ================================================ FILE: src/compile/special_form/lambda_class.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::ir; use crate::ir::expr::LambdaClass; use crate::compile::error::{GDError, GDErrorF}; use crate::compile::factory; use crate::compile::CompilerFrame; use crate::compile::Compiler; use crate::compile::stateful::{StExpr, NeedsResult}; use crate::compile::body::builder::{StmtBuilder, HasDecls}; use crate::compile::body::class_initializer::ClassBuilder; use crate::compile::body::class_scope::DirectClassScope; use crate::compile::symbol_table::{SymbolTable, ClassTablePair}; use crate::compile::symbol_table::inner::InnerSymbolTable; use crate::compile::symbol_table::local_var::LocalVar; use crate::compile::symbol_table::function_call::OuterStaticRef; use crate::compile::names::registered::RegisteredNameGenerator; use crate::compile::names::generator::NameGenerator; use crate::gdscript::decl::{self, Decl, DeclF, VarDecl}; use crate::gdscript::inner_class::{self, NeedsOuterClassRef}; use crate::pipeline::source::SourceOffset; use super::lambda; use super::closure::ClosureData; pub fn compile_lambda_class(frame: &mut CompilerFrame, class: &LambdaClass, pos: SourceOffset) -> Result { // In the perfect world, we would do all of our operations *on* // frame rather than destructuring that variable here. But, the way // this function is currently written, we need access to both // frame.table and lambda_table throughout most of the computation. // Perhaps there's a way to refactor it, but it won't be easy. let CompilerFrame { compiler, pipeline, table, builder, class_scope: original_class_scope } = frame; let LambdaClass { extends, args: constructor_args, constructor, decls } = class; // Note: We construct a new class scope here, since we're now inside // of a newly-declared class. We use the original_class_scope to // compile the constructor arguments, since super calls to the scope // enclosing the class declaration are still valid in that syntactic // position. let mut class_scope = DirectClassScope::new(); // Validate the extends declaration (must be a global variable) let extends = Compiler::resolve_extends(table, extends, pos)?; // New GD name let gd_class_name = RegisteredNameGenerator::new_global_var(table).generate_with("_AnonymousClass"); let closure = { let mut closure = ClosureData::from(class); // No need to close around global variables, as they're available // everywhere. closure.purge_globals(table); closure }; // Generate an outer class ref if we need access to the scope from // within the lambda class. let mut outer_ref_name = String::new(); let needs_outer_ref = closure.closure_fns.needs_outer_class_ref(table); if needs_outer_ref { outer_ref_name = compiler.name_generator().generate_with(inner_class::OUTER_REFERENCE_NAME); } let mut lambda_table = SymbolTable::with_synthetics_from(table); // Bind self into the lambda table. lambda_table.set_var(String::from("self"), LocalVar::self_var()); // Bind all of the closure variables, closure functions, and global // variables inside. let forbidden_names = get_all_instance_scoped_vars(decls); lambda::locally_bind_vars(compiler, table, &mut lambda_table, &closure.closure_vars, &forbidden_names, pos)?; lambda::locally_bind_fns(compiler, *pipeline, table, &mut lambda_table, &closure.closure_fns, pos, &OuterStaticRef::InnerInstanceVar(&outer_ref_name))?; lambda::copy_global_vars(table, &mut lambda_table); // Convert the closures to GDScript names. let gd_closure_vars = closure.to_gd_closure_vars(&lambda_table); let gd_src_closure_vars = closure.to_gd_closure_vars(table); let mut lambda_table = InnerSymbolTable::new(lambda_table, table); // Build the constructor for the lambda class. let default_constructor: ir::decl::ConstructorDecl; let constructor = match constructor { None => { default_constructor = ir::decl::ConstructorDecl::empty(pos); &default_constructor } Some(c) => { c } }; let (constructor, constructor_helpers) = compile_lambda_class_constructor(&mut compiler.frame(pipeline, *builder, &mut lambda_table, &mut class_scope), constructor, &gd_closure_vars, pos)?; // Build the class body for the lambda class. let mut class_init_builder = ClassBuilder::new(); #[allow(clippy::vec_init_then_push)] // For style consistency let class_body = { let mut class_body = vec!(); class_body.push(Decl::new(DeclF::InitFnDecl(constructor), pos)); for helper in constructor_helpers { class_body.push(Decl::new(DeclF::FnDecl(decl::Static::NonStatic, helper), pos)); } for name in gd_closure_vars.iter() { class_body.push(Decl::new(DeclF::VarDecl(VarDecl::simple(name.clone())), pos)); } for d in decls { if d.is_static() { // Static methods / constants are not allowed on lambda classes return Err(GDError::new(GDErrorF::StaticOnLambdaClass(d.name().into_owned()), d.pos)); } // Nothing static is allowed in lambda classes (static methods // or constants). The ClassTablePair simply gets a dummy static // symbol table that will never be used, since we just checked // in the above code that the declaration is non-static. let mut dummy_table = SymbolTable::new(); let tables = ClassTablePair { instance_table: &mut lambda_table, static_table: &mut dummy_table }; class_body.push(compiler.compile_class_inner_decl(pipeline, &mut class_init_builder, tables, &mut class_scope, d)?); } class_body }; drop(lambda_table); let mut class = decl::ClassDecl { name: gd_class_name.clone(), extends: extends, body: class_body, }; if needs_outer_ref { inner_class::add_outer_class_ref_named(&mut class, compiler.preload_resolver(), *pipeline, outer_ref_name, pos); } class_init_builder.declare_proxies_from_scope(class_scope); let class_init = class_init_builder.build_into(*builder); class_init.apply(&mut class, pos)?; builder.add_helper(Decl::new(DeclF::ClassDecl(class), pos)); let constructor_args = constructor_args.iter().map(|expr| compiler.frame(pipeline, *builder, table, *original_class_scope).compile_expr(expr, NeedsResult::Yes)).collect::, _>>()?; let expr = lambda::make_constructor_call(gd_class_name, gd_src_closure_vars, constructor_args, pos); Ok(expr) } fn get_all_instance_scoped_vars(decls: &[ir::decl::ClassInnerDecl]) -> Vec<&str> { // TODO This is NOT a complete solution. See Issue #82 // (https://github.com/Mercerenies/gdlisp/issues/82) for the // problems with this implementation. let mut result: Vec<&str> = vec![]; for decl in decls { match &decl.value { ir::decl::ClassInnerDeclF::ClassVarDecl(var) => { result.push(&var.name); } ir::decl::ClassInnerDeclF::ClassConstDecl(cdecl) => { result.push(&cdecl.name); } ir::decl::ClassInnerDeclF::ClassSignalDecl(_) => {} ir::decl::ClassInnerDeclF::ClassFnDecl(_) => {} } } result } fn compile_lambda_class_constructor(frame: &mut CompilerFrame, constructor: &ir::decl::ConstructorDecl, gd_closure_vars: &[String], pos: SourceOffset) -> Result<(decl::InitFnDecl, Vec), GDError> { let (mut constructor, constructor_helpers) = factory::declare_constructor(frame, constructor)?; constructor.args.prepend_required(gd_closure_vars.iter().cloned()); for name in gd_closure_vars.iter().rev() { constructor.body.insert(0, super::assign_to_compiler(name.to_string(), name.to_string(), pos)); } Ok((constructor, constructor_helpers)) } ================================================ FILE: src/compile/special_form/lambda_vararg/builder.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::{self, Stmt}; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::op; use crate::pipeline::source::SourceOffset; use std::iter; /// A `LambdaVarargBuilder` builds up what will eventually be a /// `Vec`. In the course of doing so, we assume that a variable /// with the name given by `args_variable` (in the constructor) is in /// scope, and we accumulate values from that variable into several /// others, eventually culminating in a final function call which uses /// the accumulated arguments. #[derive(Debug, Clone)] pub struct LambdaVarargBuilder { args_variable: String, stmts: Vec, args: Vec, pos: SourceOffset, } impl LambdaVarargBuilder { /// Construct a new builder with the given arguments variable, an /// empty list of accumulated arguments, and the given source /// position. The source position is used for GDLisp compiler error /// messages. pub fn new(args_variable: String, pos: SourceOffset) -> LambdaVarargBuilder { LambdaVarargBuilder::with_existing_args(args_variable, iter::empty(), pos) } /// Construct a new builder with the given arguments variable, an /// inherited list of accumulated arguments, and the given source /// position. The source position is used for GDLisp compiler error /// messages. pub fn with_existing_args(args_variable: String, args: impl Iterator, pos: SourceOffset) -> LambdaVarargBuilder { LambdaVarargBuilder { args_variable: args_variable, stmts: Vec::new(), args: args.collect(), pos: pos, } } /// Runs an internal block with a builder inherited from `self`, /// returning the statements produced by the inner builder. /// /// The new builder will be created with the same arguments variable /// and `SourceOffset`, and it will be created with the current /// accumulated argument list of the outer builder. Changes to the /// inner builder's argument list will not propagate to the outer. /// The given `block` shall be called with the inner builder, and /// then `inner_builder.build()` will be returned. pub fn with_inner_builder(&self, block: impl FnOnce(&mut LambdaVarargBuilder)) -> Vec { let mut inner_builder = LambdaVarargBuilder::with_existing_args( self.args_variable.to_owned(), self.args.iter().cloned(), self.pos, ); block(&mut inner_builder); inner_builder.build() } /// Declares a variable (whose initial value is [`Expr::null`]), and /// adds it to the accumulated arguments list. pub fn declare_argument_var(&mut self, name: String) { self.stmts.push(Stmt::var_decl(name.clone(), Expr::null(self.pos), self.pos)); self.args.push(Expr::new(ExprF::Var(name), self.pos)); } /// Pushes a call to the GDScript built-in function `push_error`, /// with the given error message. pub fn push_error(&mut self, message: &str) { self.stmts.push(Stmt::expr( Expr::simple_call("push_error", vec!(Expr::str_lit(message, self.pos)), self.pos), )); } /// This method generates code to take the first element (i.e. the /// `car`) of the arguments variable and assign it to the variable /// indicated by `variable_name`. Then the arguments variable is /// reassigned to its own `cdr`. Note that this will fail if the /// arguments variable contains `nil`, so it is often better to run /// this inside of a /// [`if_args_is_empty`](LambdaVarargBuilder::if_args_is_empty) /// block, to be sure. pub fn pop_argument(&mut self, variable_name: &str) { self.stmts.push(Stmt::simple_assign( Expr::var(variable_name, self.pos), Expr::var(&self.args_variable, self.pos).attribute("car", self.pos), self.pos, )); self.stmts.push(Stmt::simple_assign( Expr::var(&self.args_variable, self.pos), Expr::var(&self.args_variable, self.pos).attribute("cdr", self.pos), self.pos, )); } /// Assigns an arbitrary value to the given variable. pub fn assign_to_var(&mut self, variable_name: &str, value: Expr) { self.stmts.push(Stmt::simple_assign(Expr::var(variable_name, self.pos), value, self.pos)); } /// Takes the arguments variable, indicating all of the arguments /// that have not been processed yet, and adds it, as a single /// scalar unit, to the accumulated arguments list. /// /// The `function` argument indicates a transformative function to /// be applied to the arguments variable before using it as an /// accumulated argument. If the transformative function is not /// necessary, then [`LambdaVarargBuilder::pop_rest_of_arguments`] /// can be used instead. pub fn pop_rest_of_arguments_with(&mut self, function: F) where F : FnOnce(Expr) -> Expr { let args_var = Expr::new(ExprF::Var(self.args_variable.to_owned()), self.pos); self.args.push(function(args_var)); } /// Takes the arguments variable, indicating all of the arguments /// that have not been processed yet, and adds it, as a single /// scalar unit, to the accumulated arguments list. /// /// Equivalent to `self.pop_rest_of_arguments_with(|x| x)`. pub fn pop_rest_of_arguments(&mut self) { self.pop_rest_of_arguments_with(|x| x); } /// Generates code to call the function given by `function_name`, /// passing all of the accumulated arguments in order. The code is /// generated as part of a `return` statement, so generally speaking /// no more code should be executed after this point. pub fn call_function_with_arguments(&mut self, function_name: &str) { let all_args = self.args.clone(); self.stmts.push(Stmt::return_stmt(Expr::simple_call(function_name, all_args, self.pos), self.pos)); } /// Generates code for an `if` statement, where the first branch is /// followed if the arguments variable has the value `nil` and the /// second is followed otherwise. /// /// The two branches are created by passing `empty_block` and /// `nonempty_block`, respectively, to /// [`LambdaVarargBuilder::with_inner_builder`]. pub fn if_args_is_empty(&mut self, empty_block: F1, nonempty_block: F2) where F1 : FnOnce(&mut LambdaVarargBuilder), F2 : FnOnce(&mut LambdaVarargBuilder) { let empty_case: Vec = self.with_inner_builder(empty_block); let nonempty_case: Vec = self.with_inner_builder(nonempty_block); self.stmts.push(stmt::if_else( Expr::binary(Expr::var(&self.args_variable, self.pos), op::BinaryOp::Eq, Expr::null(self.pos), self.pos), empty_case, nonempty_case, self.pos, )); } /// Consumes the builder and returns the sequence of statements /// generated. pub fn build(self) -> Vec { self.stmts } } ================================================ FILE: src/compile/special_form/lambda_vararg/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod builder; use builder::LambdaVarargBuilder; use crate::ir::arglist::vararg::VarArg; use crate::compile::symbol_table::function_call::FnSpecs; use crate::gdscript::decl::{self, Decl, DeclF, VarDecl}; use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::gdscript::library; use crate::gdscript::class_extends::ClassExtends; use crate::gdscript::arglist::ArgList; use crate::pipeline::source::SourceOffset; use std::convert::TryInto; const ARGS_VAR: &str = "args"; pub fn generate_lambda_vararg(specs: FnSpecs, pos: SourceOffset) -> decl::FnDecl { let mut builder = LambdaVarargBuilder::new(ARGS_VAR.to_owned(), pos); let required: Vec<_> = (0..specs.required).map(|i| format!("required_{}", i)).collect(); let optional: Vec<_> = (0..specs.optional).map(|i| format!("optional_{}", i)).collect(); for req in &required { builder.declare_argument_var(req.to_owned()); builder.if_args_is_empty(|builder| { builder.push_error("Not enough arguments"); }, |builder| { builder.pop_argument(req); }); } for opt in &optional { builder.declare_argument_var(opt.to_owned()); builder.if_args_is_empty(|builder| { builder.assign_to_var(opt, Expr::null(pos)); }, |builder| { builder.pop_argument(opt); }); } match specs.rest { Some(VarArg::RestArg) => { builder.pop_rest_of_arguments(); builder.call_function_with_arguments("call_func"); } Some(VarArg::ArrArg) => { builder.pop_rest_of_arguments_with(|args| { Expr::call(Some(Expr::var(library::GDLISP_NAME, pos)), "list_to_array", vec!(args), pos) }); builder.call_function_with_arguments("call_func"); } None => { builder.if_args_is_empty(|builder| { builder.call_function_with_arguments("call_func"); }, |builder| { builder.push_error("Too many arguments"); }); } } decl::FnDecl { name: String::from("call_funcv"), args: ArgList::required(vec!(String::from(ARGS_VAR))), body: builder.build(), } } pub fn generate_lambda_class(class_name: String, specs: FnSpecs, args: ArgList, closed_vars: &[String], lambda_body: Vec, minimalist_run: bool, pos: SourceOffset) -> decl::ClassDecl { let func_name = String::from("call_func"); let func = decl::FnDecl { name: func_name, args: args, body: lambda_body, }; let funcv = generate_lambda_vararg(specs, pos); let mut constructor_body = Vec::new(); for name in closed_vars.iter() { constructor_body.push(super::assign_to_compiler(name.to_string(), name.to_string(), pos)); } let r: i32 = specs.required.try_into().unwrap(); let o: i32 = specs.optional.try_into().unwrap(); let x: i32 = VarArg::arg_to_const(specs.rest); constructor_body.push(super::assign_expr_to_compiler(String::from("__gdlisp_required"), Expr::from_value(r, pos))); constructor_body.push(super::assign_expr_to_compiler(String::from("__gdlisp_optional"), Expr::from_value(o, pos))); constructor_body.push(super::assign_expr_to_compiler(String::from("__gdlisp_rest"), Expr::from_value(x, pos))); let constructor = decl::FnDecl { name: String::from(library::CONSTRUCTOR_NAME), args: ArgList::required(closed_vars.iter().map(|x| x.to_owned()).collect()), body: constructor_body, }; let mut class_body = vec!(); for var in closed_vars { class_body.push(Decl::new(DeclF::VarDecl(VarDecl::simple(var.to_owned())), pos)); } class_body.append(&mut vec!( Decl::new(DeclF::FnDecl(decl::Static::NonStatic, constructor), pos), Decl::new(DeclF::FnDecl(decl::Static::NonStatic, func), pos), Decl::new(DeclF::FnDecl(decl::Static::NonStatic, funcv), pos), )); decl::ClassDecl { name: class_name, extends: gdlisp_function_class(minimalist_run), body: class_body, } } fn gdlisp_function_class(minimalist_run: bool) -> ClassExtends { if minimalist_run { ClassExtends::SimpleIdentifier(String::from("Function")) } else { ClassExtends::SimpleIdentifier(String::from("GDLisp")).attribute(String::from("Function")) } } #[cfg(test)] mod tests { use super::*; use crate::ir::arglist::vararg::VarArg; fn compile_vararg(specs: FnSpecs) -> String { let result = generate_lambda_vararg(specs, SourceOffset::default()); Decl::new(DeclF::FnDecl(decl::Static::NonStatic, result), SourceOffset::default()).to_gd(0) } #[test] fn test_lambda_vararg() { assert_eq!(compile_vararg(FnSpecs::new(0, 0, None)), "func call_funcv(args):\n if args == null:\n return call_func()\n else:\n push_error(\"Too many arguments\")\n"); assert_eq!(compile_vararg(FnSpecs::new(0, 0, Some(VarArg::RestArg))), "func call_funcv(args):\n return call_func(args)\n"); assert_eq!(compile_vararg(FnSpecs::new(0, 0, Some(VarArg::ArrArg))), "func call_funcv(args):\n return call_func(GDLisp.list_to_array(args))\n"); assert_eq!(compile_vararg(FnSpecs::new(1, 0, Some(VarArg::RestArg))), "func call_funcv(args):\n var required_0 = null\n if args == null:\n push_error(\"Not enough arguments\")\n else:\n required_0 = args.car\n args = args.cdr\n return call_func(required_0, args)\n"); assert_eq!(compile_vararg(FnSpecs::new(0, 1, Some(VarArg::RestArg))), "func call_funcv(args):\n var optional_0 = null\n if args == null:\n optional_0 = null\n else:\n optional_0 = args.car\n args = args.cdr\n return call_func(optional_0, args)\n"); } } ================================================ FILE: src/compile/special_form/let_block.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::compile::frame::CompilerFrame; use crate::compile::stateful::{StExpr, NeedsResult}; use crate::compile::factory; use crate::compile::names; use crate::compile::names::registered::RegisteredNameGenerator; use crate::compile::body::builder::StmtBuilder; use crate::compile::error::GDError; use crate::compile::symbol_table::HasSymbolTable; use crate::compile::symbol_table::local_var::LocalVar; use crate::gdscript::library; use crate::ir; use crate::ir::access_type::AccessType; use crate::ir::expr::LocalVarClause; use crate::pipeline::source::SourceOffset; type IRExpr = ir::expr::Expr; pub fn compile_let(frame: &mut CompilerFrame, clauses: &[LocalVarClause], body: &IRExpr, needs_result: NeedsResult, _pos: SourceOffset) -> Result { let closure_vars = body.get_locals(); let var_names = clauses.iter().map::, _>(|clause| { let LocalVarClause { name: ast_name, value: expr } = clause; let ast_name = ast_name.to_owned(); let result_value = frame.compile_expr(expr, NeedsResult::Yes)?.expr; let result_value = if closure_vars.get(&ast_name).unwrap_or(&AccessType::None).requires_cell() { library::cell::construct_cell(result_value) } else { result_value }; let gd_name = factory::declare_var(&mut RegisteredNameGenerator::new_local_var(frame.table), frame.builder, &names::lisp_to_gd(&ast_name), Some(result_value), clause.value.pos); Ok((ast_name, gd_name)) }).collect::, _>>()?; frame.with_local_vars(&mut var_names.into_iter().map(|x| (x.0.clone(), LocalVar::local(x.1, *closure_vars.get(&x.0).unwrap_or(&AccessType::None)))), |frame| { frame.compile_expr(body, needs_result) }) } ================================================ FILE: src/compile/special_form/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod lambda; pub mod flet; pub mod lambda_class; pub mod lambda_vararg; pub mod closure; pub mod let_block; use crate::ir; use crate::ir::access_type::AccessType; use crate::compile::{Compiler, StExpr, NeedsResult}; use crate::compile::body::builder::StmtBuilder; use crate::compile::symbol_table::HasSymbolTable; use crate::compile::symbol_table::local_var::LocalVar; use crate::compile::stmt_wrapper; use crate::compile::error::GDError; use crate::compile::stateful::SideEffects; use crate::compile::names; use crate::compile::names::registered::RegisteredNameGenerator; use crate::compile::factory; use crate::compile::frame::CompilerFrame; use crate::compile::names::generator::NameGenerator; use crate::gdscript::stmt::{self, Stmt, StmtF}; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::op; use crate::pipeline::source::SourceOffset; use crate::util; type IRExpr = ir::expr::Expr; pub fn compile_cond_stmt(frame: &mut CompilerFrame, clauses: &[(IRExpr, Option)], needs_result: NeedsResult, pos: SourceOffset) -> Result { let (destination, result) = needs_result.into_destination(&mut RegisteredNameGenerator::new_local_var(frame.table), frame.builder, "_cond", pos); let init: Vec = util::option_to_vec(destination.wrap_to_stmt(Compiler::nil_expr(pos))); let body = clauses.iter().rev().fold(Ok(init), |acc: Result<_, GDError>, curr| { let acc = acc?; let (cond, body) = curr; match body { None => { // Outer local builder (for constructing any values needed in // the conditional). frame.with_local_builder(|frame| { let cond = frame.compile_expr(cond, NeedsResult::Yes)?.expr; let var_name = factory::declare_var(&mut RegisteredNameGenerator::new_local_var(frame.table), frame.builder, "_cond", Some(cond), pos); let var_expr = StExpr { expr: Expr::new(ExprF::Var(var_name.clone()), pos), side_effects: SideEffects::None }; // Inner local builder (for the contents of the "true" // block). let if_branch = frame.with_local_builder_ok(|frame| { destination.wrap_to_builder(frame.builder, var_expr); }); frame.builder.append(stmt::if_else(Expr::new(ExprF::Var(var_name), pos), if_branch, acc, pos)); Ok(()) }) } Some(body) => { // Outer local builder (for constructing any values needed in // the conditional). frame.with_local_builder(|frame| { let cond = frame.compile_expr(cond, NeedsResult::Yes)?.expr; // Inner local builder (for the contents of the "true" // block). let if_branch = frame.with_local_builder(|frame| { frame.compile_stmt(destination.as_ref(), body) })?; frame.builder.append(stmt::if_else(cond, if_branch, acc, pos)); Ok(()) }) } } })?; frame.builder.append_all(&mut body.into_iter()); Ok(StExpr { expr: result, side_effects: SideEffects::None }) } pub fn compile_while_stmt(frame: &mut CompilerFrame, cond: &IRExpr, body: &IRExpr, _needs_result: NeedsResult, pos: SourceOffset) -> Result { // If the condition fits in a single GDScript expression, then we'll // just compile straight to a GDScript while loop. If not, then we // need to compile to "while True:" and have a break statement when // we check our conditional. So, to figure out whether the condition // fits in a single expression, we'll compile it with a temporary // builder and then ask that builder whether or not it received any // statements. let (mut cond_expr, cond_body) = frame.with_local_builder_result(|frame| { frame.compile_expr(cond, NeedsResult::Yes).map(|x| x.expr) })?; let body = frame.with_local_builder(|frame| { if !cond_body.is_empty() { // Compound while form frame.builder.append_all(&mut cond_body.into_iter()); let inner_cond_expr = cond_expr.clone().unary(op::UnaryOp::Not, pos); frame.builder.append(stmt::if_then(inner_cond_expr, vec!(Stmt::new(StmtF::BreakStmt, cond.pos)), cond.pos)); cond_expr = Expr::from_value(true, pos); } frame.compile_stmt(&stmt_wrapper::Vacuous, body)?; Ok(()) })?; frame.builder.append(Stmt::new(StmtF::WhileLoop(stmt::WhileLoop { condition: cond_expr, body: body }), pos)); Ok(Compiler::nil_expr(pos)) } pub fn compile_for_stmt(frame: &mut CompilerFrame, name: &str, iter: &IRExpr, body: &IRExpr, _needs_result: NeedsResult, pos: SourceOffset) -> Result { let closure_vars = body.get_locals(); let citer = frame.compile_expr(iter, NeedsResult::Yes)?.expr; let var_name = RegisteredNameGenerator::new_fn(frame.table).generate_with(&names::lisp_to_gd(name)); let body = frame.with_local_builder(|frame| { let local_var = LocalVar::local(var_name.to_owned(), *closure_vars.get(name).unwrap_or(&AccessType::None)); frame.with_local_var(name.to_owned(), local_var, |frame| { frame.compile_stmt(&stmt_wrapper::Vacuous, body).map(|_| ()) }) })?; frame.builder.append(Stmt::new(StmtF::ForLoop(stmt::ForLoop { iter_var: var_name, collection: citer, body: body }), pos)); Ok(Compiler::nil_expr(pos)) } /// A [`Stmt`] which assigns the value of the local variable /// `local_var` to the variable `inst_var` on the Godot `self` object. pub fn assign_to_compiler(inst_var: String, local_var: String, pos: SourceOffset) -> Stmt { assign_expr_to_compiler(inst_var, Expr::new(ExprF::Var(local_var), pos)) } /// A [`Stmt`] which assigns `expr` to the variable `inst_var` on the /// Godot `self` object. pub fn assign_expr_to_compiler(inst_var: String, expr: Expr) -> Stmt { let pos = expr.pos; let self_target = Expr::self_var(pos).attribute(inst_var, pos); Stmt::simple_assign(self_target, expr, pos) } ================================================ FILE: src/compile/stateful.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Types for expressing whether or not an [`Expr`] has side effects. use super::factory; use super::stmt_wrapper::{self, StmtWrapper}; use super::names::generator::NameGenerator; use super::body::builder::StmtBuilder; use crate::gdscript::expr::{Expr, ExprF}; use crate::ir::access_type::AccessType; use crate::pipeline::source::SourceOffset; /// An `StExpr` is an expression coupled with a declaration of that /// expression's side effects. /// /// Note that such a declaration is context-sensitive, so there is no /// general-purpose function for converting an `Expr` into an /// `StExpr`. For example, simple variable access `foobar` could be /// read-only, assuming the variable is a local variable, but it could /// also be read-write, if the variable is an instance variable which /// has a `setget` modifier. Likewise, a function call is generally /// read-write, but if we can prove that the call is free of side /// effects, we can reduce this. #[derive(Debug, Clone)] pub struct StExpr { /// The expression being wrapped. pub expr: Expr, /// A declaration of the side effects of the wrapped expression, as /// a [`SideEffects`] value. pub side_effects: SideEffects, } /// A declaration of side effects. /// /// `SideEffects` has an [`Ord`] instance, where more invasive effects /// are considered greater than less invasive ones. This makes it /// possible to use [`std::cmp::max`] to take the more invasive of two /// side effects. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum SideEffects { /// The expression has no side effects. An expression with this /// declaration modifies nothing and reads no mutable state. It /// should be possible to move the expression, for inlining /// purposes, to anywhere else within the same scope, or anywhere /// else which contains the appropriate constant values necessary /// for evaluation. An expression with `SideEffects::None` can /// safely be evaluated several times, or have several identical /// evaluations reduced to one. The optimizer has a great deal of /// control over these expressions. None, /// The expression does not modify any state, but it may read from /// mutable state. An expression with `SideEffects::ReadsState` may /// be evaluated multiple times, or may have multiple evaluations /// reduced to one, provided that no [`SideEffects::ModifiesState`] /// calls happen in between the evaluations. Such expressions may /// also be reordered relative to other `SideEffects::ReadsState` or /// `SideEffects::None` expressions. ReadsState, /// The expression may read or write to ambient state. It is not /// safe to reorder multiple `SideEffects::ModifiesState` /// expressions relative to one another, and evaluating such a /// statement multiple times is different than evaluating it only /// once. The optimizer is very limited in this case. ModifiesState, } /// A Boolean-isomorphic type indicating whether a result is required. /// /// An argument of type `NeedsResult` is used in functions such as /// [`CompilerFrame::compile_expr`](super::frame::CompilerFrame::compile_expr) /// to indicate whether or not the result is going to be used for /// anything. There are some expressions that can compile to a more /// efficient form if we know the result is unneeded. For instance, /// `if` expressions in general need to create a local variable to /// store the result in, but if we know the result is going to be /// discarded, then we can safely skip that step. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum NeedsResult { No, Yes } impl From for bool { fn from(s: NeedsResult) -> bool { s == NeedsResult::Yes } } impl From for NeedsResult { fn from(b: bool) -> NeedsResult { if b { NeedsResult::Yes } else { NeedsResult::No } } } impl NeedsResult { /// Returns `Yes` iff either `self` or `other` is `Yes`. pub fn or(self, other: NeedsResult) -> NeedsResult { NeedsResult::from(self == NeedsResult::Yes || other == NeedsResult::Yes) } /// Returns `Yes` iff both `self` and `other` are `Yes`. pub fn and(self, other: NeedsResult) -> NeedsResult { NeedsResult::from(self == NeedsResult::Yes && other == NeedsResult::Yes) } /// Construct an appropriate [`StmtWrapper`] that can store the /// result of an expression, if needed. /// /// If `self` is `Yes`, then this function declares a variable (into /// `builder`) with a local, generated name (using `prefix` as the /// prefix) and then returns a `StmtWrapper` which assigns a value /// to that variable. The `Expr`, in this case, is the variable /// name. /// /// If `self` is `No`, then this function returns a vacuous /// `StmtWrapper` and [`Expr::null()`] as the expression. /// /// In general, the returned `StmtWrapper` should be supplied with /// the Godot expression which is, semantically, the result of the /// IR expression, and the returned `Expr` can be used to reference /// it later. If `self` is `No`, then the returned `Expr` is nil, as /// we chose not to store the result anywhere. pub fn into_destination(self, gen: &mut impl NameGenerator, builder: &mut StmtBuilder, prefix: &str, pos: SourceOffset) -> (Box, Expr) { if self.into() { let var_name = factory::declare_var(gen, builder, prefix, None, pos); let destination = Box::new(stmt_wrapper::assign_to_var(var_name.clone(), pos)) as Box; (destination, Expr::new(ExprF::Var(var_name), pos)) } else { let destination = Box::new(stmt_wrapper::Vacuous) as Box; (destination, Expr::null(pos)) } } } impl SideEffects { /// Whether or not `self` is *at least* `SideEffects::ReadsState`. /// /// # Examples /// /// ``` /// # use gdlisp::compile::stateful::SideEffects; /// assert!(!SideEffects::None.reads_state()); /// assert!(SideEffects::ReadsState.reads_state()); /// assert!(SideEffects::ModifiesState.reads_state()); /// ``` pub fn reads_state(&self) -> bool { *self >= SideEffects::ReadsState } /// Whether or not `self` is *at least* /// `SideEffects::ModifiesState`. /// /// # Examples /// /// ``` /// # use gdlisp::compile::stateful::SideEffects; /// assert!(!SideEffects::None.modifies_state()); /// assert!(!SideEffects::ReadsState.modifies_state()); /// assert!(SideEffects::ModifiesState.modifies_state()); /// ``` pub fn modifies_state(&self) -> bool { *self >= SideEffects::ModifiesState } } /// If we're writing to a variable, then the side effect is obviously /// ModifiesState and this From instance should not be used. If we're /// reading from a variable, then we can convert the AccessType of /// that variable into a SideEffects by determining whether or not the /// variable is ever modified (x.is_written_to) to figure out if we're /// querying mutable state or simply accessing a constant value. impl From for SideEffects { fn from(x: AccessType) -> SideEffects { if x.is_written_to() { SideEffects::ReadsState } else { SideEffects::None } } } ================================================ FILE: src/compile/stmt_wrapper.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Trait and implementations for conveniently wrapping [`Expr`] into //! [`Stmt`]. //! //! This module defines the [`StmtWrapper`] trait, as well as several //! implementations of it by structs. use crate::gdscript::stmt::{Stmt, StmtF}; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::op; use crate::compile::body::builder::StmtBuilder; use crate::pipeline::source::SourceOffset; use super::StExpr; /// A `StmtWrapper` provides a mechanism for taking an arbitrary /// [`Expr`] and wrapping it into a [`Stmt`]. It can be thought of as /// a `Stmt` with a hole in it, which we'll later fill with an /// expression. pub trait StmtWrapper { /// Wraps the expression in a statement. fn wrap_expr(&self, expr: Expr) -> Stmt; /// Normally, a wrapper, as the name implies, wraps an expression in /// a statement. Sometimes, however, the expression has no side /// effects, and the work itself has already been done elsewhere. In /// this case, `is_vacuous` specifies whether we actually *need* the /// result or not. If `is_vacuous` is true and the expression has no /// side effects, it will be elided completely. /// /// The default `is_vacuous` always returns false. It can be /// overridden in implementors. fn is_vacuous(&self) -> bool { false } /// Wraps `expr` and places it in `builder`. /// /// If `self` is vacuous and `expr` does not modify state, this /// method does nothing. The reasoning for this is as follows. If /// `expr.side_effects` does not modify state, then the expression, /// as a value, is only being used for its result, not for its side /// effects. If `self.is_vacuous()` is true, then we've declared /// that this statement wrapper does not care about the result. So /// if we have an expression for which the only thing that matters /// is the result and a statement wrapper that's ready to ignore /// that result, then we effectively have no need to ever evaluate /// the expression in the first place. fn wrap_to_builder(&self, builder: &mut StmtBuilder, expr: StExpr) { if let Some(stmt) = self.wrap_to_stmt(expr) { builder.append(stmt); } } /// Wraps `expr` and returns either zero or one statements, using /// the same reasoning at [`StmtWrapper.wrap_to_builder`]. If `self` /// is vacuous and the expression is stateless, then the vector will /// be empty. Otherwise, a single statement will be produced via /// [`StmtWrapper::wrap_expr`]. fn wrap_to_stmt(&self, expr: StExpr) -> Option { let StExpr { expr, side_effects } = expr; if side_effects.modifies_state() || !self.is_vacuous() { Some(self.wrap_expr(expr)) } else { None } } } /// A [`StmtWrapper`] which wraps the expression in a /// [`StmtF::ReturnStmt`]. pub struct Return; /// A [`StmtWrapper`] which wraps the expression in a [`StmtF::Expr`]. /// This is a vacuous statement wrapper, as per /// [`StmtWrapper::is_vacuous`]. pub struct Vacuous; /// A [`StmtWrapper`] which wraps the expression in a /// [`StmtF::Assign`], where the left-hand side is given by the /// `AssignToExpr` value. pub struct AssignToExpr(pub Expr); impl StmtWrapper for Return { fn wrap_expr(&self, expr: Expr) -> Stmt { let pos = expr.pos; Stmt::new(StmtF::ReturnStmt(expr), pos) } } impl StmtWrapper for Vacuous { fn wrap_expr(&self, expr: Expr) -> Stmt { let pos = expr.pos; Stmt::new(StmtF::Expr(expr), pos) } fn is_vacuous(&self) -> bool { true } } /// An [`AssignToExpr`] which assigns to an [`ExprF::Var`] with the /// given name. pub fn assign_to_var(s: String, pos: SourceOffset) -> AssignToExpr { AssignToExpr(Expr::new(ExprF::Var(s), pos)) } impl StmtWrapper for AssignToExpr { fn wrap_expr(&self, expr: Expr) -> Stmt { let pos = expr.pos; let lhs = Box::new(self.0.clone()); let rhs = Box::new(expr); Stmt::new(StmtF::Assign(lhs, op::AssignOp::Eq, rhs), pos) } } ================================================ FILE: src/compile/symbol_table/call_magic/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Function call magic, for deterministic inlining behavior. //! //! Function calls, by default, compile to function calls in GDScript, //! naturally. However, there are many situations where the function //! on the GDScript side is simply a trivial wrapper around some //! operation that should be inlined. This is such a trivial inline //! step that's so ubiquitously useful that we do it here as a rule. //! If a built-in function is called directly (i.e. not through a //! funcref) then it can trigger a special [`CallMagic`] which //! effectively inlines it. //! //! For a good example, look at the `+` GDLisp function. In general, //! it compiles to `GDLisp.plus`, which iterates over its arguments, //! adds them, and returns the result. But, of course, `(+ a b)` //! shouldn't require a for loop, so any time we call `+` with an //! arity known at compile time (i.e. without invoking funcrefs or the //! like), we can compile directly to the `+` operator in GDScript, //! which is much more efficient. //! //! Note that this is *not* general-purpose inlining, which will be //! implemented later as a general pass over the IR. This is for the //! very specific case of certain GDScript functions written in //! `GDLisp.lisp` which I know how to inline effectively by hand. pub mod table; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::literal::Literal; use crate::gdscript::stmt::Stmt; use crate::gdscript::op; use crate::gdscript::library; use crate::gdscript::expr_wrapper; use crate::compile::Compiler; use crate::compile::factory; use crate::compile::error::GDError; use crate::compile::body::builder::StmtBuilder; use crate::compile::stateful::StExpr; use crate::compile::stmt_wrapper::{self, StmtWrapper}; use crate::compile::args::{self, Expecting}; use crate::compile::names::registered::RegisteredNameGenerator; use crate::compile::constant::CONSTANT_GDSCRIPT_FUNCTIONS; use crate::ir::arglist::vararg::VarArg; use crate::util; use crate::pipeline::source::SourceOffset; use super::function_call::FnCall; use super::SymbolTable; use serde::{Serialize, Deserialize}; /// A `CallMagic` can meaningfully compile a given function call /// expression `call` into some GDScript [`Expr`]. /// /// Note that, although we talk about call magic applying to certain /// designated builtin calls, strictly speaking every function call /// written in GDLisp invokes call magic. This trait is *always* used /// to compile function calls. In most cases, for user-defined /// functions or for builtins without special behavior, the /// [`CallMagic::DefaultCall`] option is used, which performs the /// basic function call compilation routine via /// [`compile_default_call`]. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CallMagic { /// A default [`CallMagic`] for any functions without special /// behavior. The `CallMagic` implementation for `DefaultCall` /// simply delegates to [`compile_default_call`]. DefaultCall, /// [Call magic](CallMagic) for the `-` builtin. MinusOperation, /// [Call magic](CallMagic) for the (fractional) division operator. DivOperation, /// [Call magic](CallMagic) for the integer division operator. /// /// **Note:** This call magic is currently not used in /// `GDLisp.lisp`. IntDivOperation, /// [Call magic](CallMagic) for the mathematical modulo operator. ModOperation, /// [Call magic](CallMagic) for the `min` function. MinFunction, /// [Call magic](CallMagic) for the `max` function. MaxFunction, /// [`CallMagic`] for the inequality `/=` operator. `NEqOperation` /// only provides special behavior if two or fewer arguments are /// given. If more than two arguments are given, `NEqOperation` will /// fall back to `fallback`. NEqOperation(Box), /// The [call magic](CallMagic) for the Boolean negation operation. BooleanNotOperation, /// [`CallMagic`] for the builtin `list` function. This call magic /// compiles calls to literal `cons` cell constructors. ListOperation, /// [`CallMagic`] for the builtin `array` function. This call magic /// compiles calls to a literal GDScript array. ArrayOperation, /// [`CallMagic`] for the builtin `dict` function. This call magic /// compiles calls to a literal GDScript dictionary. If given an odd /// number of arguments, the last argument will be silently dropped. DictOperation, /// [`CallMagic`] for the builtin `vector` function, which compiles /// `vector` calls to literal `Vector2` or `Vector3` constructions /// in GDScript. VectorOperation, /// [`CallMagic`] for array subscript (via `elt`). This magic /// compiles directly to the `foo[bar]` subscript notation in /// GDScript. ArraySubscript, /// [`CallMagic`] for array subscript assignment (via `set-elt`). This /// magic compiles directly to the `foo[bar] = baz` subscript notation /// in GDScript. ArraySubscriptAssign, /// [Call magic] for the `member?` builtin function. This magic /// compiles to the GDScript `in` operator. ElementOf, /// [Call magic] for instance type checks in Godot. /// /// Note that the GDLisp built-in `instance?` does *not* compile to /// this magic, as that function is overloaded internally to work on /// primitive type constants as well as actual instances. This magic /// is used on the internal system function `sys/instance-direct?`. InstanceOf, /// [Call magic] for the `$` syntax used to invoke `get_node` in /// GDScript. /// /// This call magic compiles expressions of the form `(sys/get-node /// a b)` into GDScript `$x` syntax wherever it makes sense to do /// so, or `(a:get-node b)` in any other context. GetNodeSyntax, /// `CompileToBinOp` is a [`CallMagic`] which compiles function /// calls to sequences of binary operator application. /// /// If no arguments are provided, then this magic compiles to `zero` /// unconditionally. If one argument is provided, it is passed /// through untouched. If two or more arguments are provided, they /// are combined using `bin` in order, with associativity given by /// `assoc`. /// /// This call magic is used to implement the GDLisp builtins `+` and /// `*`. CompileToBinOp(Literal, op::BinaryOp, Assoc), /// `CompileToTransCmp` is a [`CallMagic`] which compiles function /// calls to transitive sequences of binary comparison applications. /// /// Conceptually, `CompileToTransCmp` can be thought of as /// translating a call like `(< a b c d)` into `a < b and b < c and /// c < d`. However, the translation is more subtle than that, as /// any of the names which are evaluated twice (i.e. all except the /// first and last) have to be checked for side effects. If the /// arguments might exhibit side effects, then they will need to be /// assigned to local variables to avoid evaluating the calls twice /// in the expression. /// /// If zero arguments are provided, then an error is returned. If /// one argument is provided, the result is vacuously true, as there /// are no comparisons to be performed. /// /// This call magic is used for most builtin comparison operators, /// such as `=` and `<`. It is notably *not* used for `/=`, which is /// handled by [`CallMagic::NEqOperation`] due to its unique (non-transitive) /// behavior. CompileToTransCmp(op::BinaryOp), /// `CompileToVarargCall` is a [`CallMagic`] which compiles a /// function call to a literal function call (to a GDScript global /// function) with the same set of arguments. /// /// This is used for variable-argument built-in functions like /// `print`. Since we can't (meaningfully) make a GDLisp function /// reference object for the built-in `print` function, GDLisp /// provides its own `print` function that wraps the built-in one, /// and we provide call magic to compile that directly to the /// original `print` in all situations except indirect ones. CompileToVarargCall(String), /// [`CallMagic`] for the inequality `NodePath` constructor. /// `NodePathConstructor` will compile to the literal `@"..."` /// GDScript syntax if given a literal string as argument. If given /// a variable or anything else, it will fall back to `fallback`. NodePathConstructor(Box), } /// Associativity of an operator. #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] pub enum Assoc { /// A left associative operator, as `(a + b) + c`. Left, /// A right associative operator, as `a + (b + c)`. Right, } // For most of these (but *not* all of them), we need Vec, not // Vec. The latter gives more information than we usually // need. So this helper just strips off the excess. fn strip_st(x: Vec) -> Vec { x.into_iter().map(|x| x.expr).collect() } /// The "default" mechanism for compiling a function call, in the /// absence of any nontrivial magic. This function compiles `call` and /// its arguments `args` into a standard GDScript function call, /// taking into consideration any optional-argument or rest-argument /// padding which needs to be done to make the call correct on the /// GDScript call. This is called by [`CallMagic::compile`] in the /// [`CallMagic::DefaultCall`] case to perform its work. pub fn compile_default_call(call: FnCall, mut args: Vec, pos: SourceOffset) -> Result { let FnCall { scope: _, object, function, specs, is_macro: _ } = call; // First, check arity Expecting::from(specs).validate(&function, pos, &args)?; // Get the "rest" arguments let rest = if args.len() < (specs.required + specs.optional) as usize { vec!() } else { args.split_off((specs.required + specs.optional) as usize) }; // Extend with nil while args.len() < (specs.required + specs.optional) as usize { args.push(Expr::null(pos)); } // Bind the "rest" arguments match specs.rest { None => { // We already checked arity, so if this assertion fails it's a // bug in the compiler. assert!(rest.is_empty()); } Some(VarArg::RestArg) => { args.push(library::construct_list(rest, pos)); } Some(VarArg::ArrArg) => { args.push(Expr::new(ExprF::ArrayLit(rest), pos)); } } // Then compile the call. let object: Option = object.into_expr(pos); Ok(Expr::new(ExprF::Call(object.map(Box::new), function, args), pos)) } impl CallMagic { // TODO Currently, this uses the GD name in error messages, which is // super wonky, especially for stdlib calls. Store the Lisp name and // use it for this. pub fn is_default(&self) -> bool { matches!(self, CallMagic::DefaultCall) } /// Given a [`FnCall`] instance `call` and argument list `args`, /// compile the call into a GDScript [`Expr`]. `compiler` provides a /// fresh name generator and compilation state, `builder` provides /// the enclosing block body as a [`StmtBuilder`], and `table` /// provides the enclosing scope information. pub fn compile(&self, call: FnCall, _compiler: &mut Compiler, builder: &mut StmtBuilder, table: &mut SymbolTable, mut args: Vec, // TODO Get this declared immutable here and mutable on inner scopes only pos: SourceOffset) -> Result { match self { CallMagic::DefaultCall => { let args = strip_st(args); compile_default_call(call, args, pos) } CallMagic::CompileToBinOp(zero, op, assoc) => { let args = strip_st(args); if args.is_empty() { let expr = Expr::from_value(zero.clone(), pos); Ok(expr) } else { Ok(match assoc { Assoc::Left => { util::fold1(args.into_iter(), |x, y| Expr::new(ExprF::Binary(Box::new(x), *op, Box::new(y)), pos)) } Assoc::Right => { util::fold1(args.into_iter().rev(), |x, y| Expr::new(ExprF::Binary(Box::new(y), *op, Box::new(x)), pos)) } }.unwrap()) } } CallMagic::CompileToTransCmp(op) => { Expecting::at_least(1).validate(&call.function, pos, &args)?; match args.len() { 0 => { unreachable!() } 1 => { // Dump to the builder as a simple statement if it's stateful. stmt_wrapper::Vacuous.wrap_to_builder(builder, args[0].clone()); Ok(Expr::from_value(true, pos)) } 2 => { let a = args.remove(0).expr; let b = args.remove(0).expr; Ok(Expr::new(ExprF::Binary(Box::new(a), *op, Box::new(b)), pos)) } _ => { // We need to use several of the arguments twice, so any // arguments (such as function calls) which are // potentially stateful need to be stored in temporaries. // Note that simply accessing variables, even variables // which may change, is fine, since we're doing it twice // in a row, and nothing happens in between. let args = args.into_iter().map(|x| { let StExpr { expr, side_effects } = x; if side_effects.modifies_state() { let var_name = factory::declare_var(&mut RegisteredNameGenerator::new_local_var(table), builder, "_cmp", Some(expr), pos); Expr::new(ExprF::Var(var_name), pos) } else { expr } }); let comparisons = util::each_pair(args).map(|(x, y)| { Expr::new(ExprF::Binary(Box::new(x), *op, Box::new(y)), pos) }); Ok( util::fold1(comparisons, |x, y| Expr::new(ExprF::Binary(Box::new(x), op::BinaryOp::And, Box::new(y)), pos)).unwrap() ) } } } CallMagic::MinusOperation => { let args = strip_st(args); Expecting::at_least(1).validate(&call.function, pos, &args)?; match args.len() { 0 => { unreachable!() } 1 => { let arg = args::one(args); Ok(arg.unary(op::UnaryOp::Negate, pos)) } _ => { Ok( util::fold1(args.into_iter(), |x, y| x.binary(op::BinaryOp::Sub, y, pos)).unwrap() ) } } } CallMagic::DivOperation => { let mut args = strip_st(args); Expecting::at_least(1).validate(&call.function, pos, &args)?; match args.len() { 0 => { unreachable!() } 1 => { let one = Expr::from_value(1, pos); let arg = args::one(args); Ok(one.binary(op::BinaryOp::Div, arg, pos)) } _ => { let first = args.remove(0); let result = args.into_iter().fold(first, |x, y| { x.binary(op::BinaryOp::Div, y, pos) }); Ok(result) } } } CallMagic::IntDivOperation => { let mut args = strip_st(args); Expecting::at_least(1).validate(&call.function, pos, &args)?; match args.len() { 0 => { unreachable!() } 1 => { let one = Expr::from_value(1, pos); let arg = args::one(args); Ok(one.binary(op::BinaryOp::Div, expr_wrapper::int(arg), pos)) } _ => { let first = args.remove(0); let result = args.into_iter().fold(first, |x, y| { x.binary(op::BinaryOp::Div, expr_wrapper::int(y), pos) }); Ok(result) } } } CallMagic::ModOperation => { let args = strip_st(args); Expecting::exactly(2).validate(&call.function, pos, &args)?; let (x, y) = args::two(args); Ok(Expr::new(ExprF::Binary(Box::new(x), op::BinaryOp::Mod, Box::new(y)), pos)) } CallMagic::MinFunction => { let args = strip_st(args); if args.is_empty() { let expr = Expr::var("INF", pos); Ok(expr) } else { Ok( util::fold1(args.into_iter(), |x, y| Expr::simple_call("min", vec!(x, y), pos)).unwrap(), ) } } CallMagic::MaxFunction => { let args = strip_st(args); if args.is_empty() { let expr = Expr::var("INF", pos).unary(op::UnaryOp::Negate, pos); Ok(expr) } else { Ok( util::fold1(args.into_iter(), |x, y| Expr::simple_call("max", vec!(x, y), pos)).unwrap(), ) } } CallMagic::NEqOperation(fallback) => { // We only optimize for the 0, 1, and 2 argument cases. Any more // arguments than that and the resulting expression would just be // long and annoying, and it's simply easier to call the built-in // anyway. Expecting::at_least(1).validate(&call.function, pos, &args)?; match args.len() { 0 => { unreachable!() } 1 => { // Dump to the builder as a simple statement if it's stateful. stmt_wrapper::Vacuous.wrap_to_builder(builder, args[0].clone()); Ok(Expr::from_value(true, pos)) } 2 => { let (lhs, rhs) = args::two(strip_st(args)); Ok(lhs.binary(op::BinaryOp::NE, rhs, pos)) } _ => { fallback.compile(call, _compiler, builder, table, args, pos) } } } CallMagic::BooleanNotOperation => { let args = strip_st(args); Expecting::exactly(1).validate(&call.function, pos, &args)?; let arg = args::one(args); Ok(arg.unary(op::UnaryOp::Not, pos)) } CallMagic::ListOperation => { let args = strip_st(args); Ok(library::construct_list(args, pos)) } CallMagic::ArrayOperation => { let args = strip_st(args); Ok(Expr::new(ExprF::ArrayLit(args), pos)) } CallMagic::DictOperation => { let args = strip_st(args); let contents: Vec<_> = util::each_non_overlapping_pair(args.into_iter()).collect(); Ok(Expr::new(ExprF::DictionaryLit(contents), pos)) } CallMagic::VectorOperation => { let args = strip_st(args); Expecting::between(2, 3).validate(&call.function, pos, &args)?; match args.len() { 2 => { let (x, y) = args::two(args); Ok(Expr::call(None, "Vector2", vec!(x, y), pos)) } 3 => { let (x, y, z) = args::three(args); Ok(Expr::call(None, "Vector3", vec!(x, y, z), pos)) } _ => { unreachable!() } } } CallMagic::ArraySubscript => { let args = strip_st(args); Expecting::exactly(2).validate(&call.function, pos, &args)?; let (arr, n) = args::two(args); Ok(arr.subscript(n, pos)) } CallMagic::ArraySubscriptAssign => { let args = strip_st(args); Expecting::exactly(3).validate(&call.function, pos, &args)?; let (x, arr, n) = args::three(args); let assign_target = arr.subscript(n, pos); builder.append(Stmt::simple_assign(assign_target.clone(), x, pos)); Ok(assign_target) } CallMagic::ElementOf => { let args = strip_st(args); Expecting::exactly(2).validate(&call.function, pos, &args)?; let (value, arr) = args::two(args); Ok(value.binary(op::BinaryOp::In, arr, pos)) } CallMagic::InstanceOf => { let args = strip_st(args); Expecting::exactly(2).validate(&call.function, pos, &args)?; let (value, type_) = args::two(args); Ok(value.binary(op::BinaryOp::Is, type_, pos)) } CallMagic::GetNodeSyntax => { let args = strip_st(args); Expecting::exactly(2).validate(&call.function, pos, &args)?; let (value, path) = args::two(args); if let ExprF::Literal(Literal::String(s)) = &path.value { if value.value == ExprF::Var(String::from("self")) { // We can use the $x syntax on the GDScript side return Ok(Expr::from_value(Literal::NodeLiteral(s.to_owned()), pos)); } } // Otherwise, just compile to self.get_node. Ok( Expr::call( Some(value), "get_node", vec!(path), pos, ), ) } CallMagic::CompileToVarargCall(name) => { let args = strip_st(args); Expecting::from(call.specs).validate(&call.function, pos, &args)?; Ok(Expr::simple_call(name, args, pos)) } CallMagic::NodePathConstructor(fallback) => { if args.len() == 1 { if let ExprF::Literal(Literal::String(s)) = &args[0].expr.value { return Ok( Expr::new(ExprF::Literal(Literal::NodePathLiteral(s.clone())), pos), ); } } fallback.compile(call, _compiler, builder, table, args, pos) } } } /// Returns whether the function indicated by this call magic object /// can be called in a `const` context, given sufficiently constant /// arguments. This function also includes the argument count, for /// magics that depend on the number of arguments. pub fn can_be_called_as_const(&self, arg_count: usize) -> bool { match self { CallMagic::DefaultCall => false, // In the abstract, we cannot perform a function call in const context CallMagic::MinusOperation => true, CallMagic::DivOperation => true, CallMagic::IntDivOperation => true, CallMagic::ModOperation => true, CallMagic::MinFunction => true, CallMagic::MaxFunction => true, CallMagic::NEqOperation(inner_magic) => (arg_count <= 2) || inner_magic.can_be_called_as_const(arg_count), CallMagic::BooleanNotOperation => true, CallMagic::ListOperation => false, CallMagic::ArrayOperation => true, CallMagic::DictOperation => true, CallMagic::VectorOperation => true, CallMagic::ArraySubscript => true, CallMagic::ArraySubscriptAssign => false, CallMagic::ElementOf => true, CallMagic::InstanceOf => false, // Weirdly enough, GDScript seems to not consider `is` a const operator CallMagic::GetNodeSyntax => false, // Either requires `self` or a `get_node` call; either way, non-const CallMagic::CompileToBinOp(_, _, _) => true, // TODO Some operators might cause trouble CallMagic::CompileToTransCmp(_) => true, // TODO Some operators might cause trouble CallMagic::CompileToVarargCall(name) => CONSTANT_GDSCRIPT_FUNCTIONS.contains(&**name), CallMagic::NodePathConstructor(_) => true, // Either a literal string or `NodePath`, both of which are const } } } ================================================ FILE: src/compile/symbol_table/call_magic/table.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! The [`MagicTable`] structure for tables of [call magic](super). //! //! The compiler maintains a table of known call magic that the user's //! source code is allowed to link up to during compilation. //! Currently, this table is read-only and there are no facilities in //! GDLisp to change it. //! //! Note that this functionality is used for bootstrapping and there //! is *never* a legitimate reason for an end user to ever need to //! interface with this directly. As such, everything here should be //! regarded as an implementation detail (like anything else in the //! `sys/*` namespace). use super::CallMagic; use std::collections::HashMap; /// A `MagicTable` is a table where the keys are strings and the /// values are [`CallMagic`](super::CallMagic) instances. /// /// This type can be thought of, in spirit, as `HashMap`. #[derive(Clone, Debug, Default)] pub struct MagicTable { values: HashMap, } impl MagicTable { /// An empty `MagicTable`. Equivalent to `MagicTable::default()`. pub fn new() -> MagicTable { MagicTable::default() } /// Gets the call magic associated to the given name. pub fn get(&self, name: &str) -> Option<&CallMagic> { self.values.get(name) } /// Assigns call magic to the given name, replacing any previous /// call magic affiliated with that name. pub fn set(&mut self, name: String, value: CallMagic) { self.values.insert(name, value); } /// Removes the call magic associated to the given name, if it /// exists. pub fn del(&mut self, name: &str) { self.values.remove(name); } /// Converts `self` into a hash map containing the same information. pub fn into_hashmap(self) -> HashMap { self.values } } impl From> for MagicTable { fn from(values: HashMap) -> MagicTable { MagicTable { values } } } ================================================ FILE: src/compile/symbol_table/function_call.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! [`FnCall`] and various helper structures for storing information //! about functions. use crate::gdscript::expr::Expr; use crate::gdscript::inner_class; use crate::ir::arglist::vararg::VarArg; use crate::compile::Compiler; use crate::compile::error::GDError; use crate::compile::body::builder::StmtBuilder; use crate::compile::stateful::StExpr; use crate::compile::preload_resolver::PreloadResolver; use crate::compile::args::Expecting; use crate::compile::constant::CONSTANT_GDSCRIPT_FUNCTIONS; use crate::pipeline::can_load::CanLoad; use crate::pipeline::source::SourceOffset; use super::call_magic::{CallMagic}; use super::local_var::VarName; use super::SymbolTable; use serde::{Serialize, Deserialize}; /// All of the relevant information needed to make a call to a /// function is stored in `FnCall`. /// [`CallMagic`](super::call_magic::CallMagic) requires an `FnCall` to /// identify the information about a function. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FnCall { /// The function's scope. pub scope: FnScope, /// The expression for the object on which the function is being /// called. pub object: FnName, /// The name of the function, as a GDScript identifier. pub function: String, /// The shape of the function. pub specs: FnSpecs, /// Whether or not the function being called is in fact a macro. /// This can affect the way in which a function is imported in /// scope. pub is_macro: bool, } /// The type of scope in which a function declaration appears. This /// affects certain compiled behaviors of the function, such as /// whether it needs to be included in closures and how it gets /// imported into new scopes. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FnScope { /// A superglobal function is available in all scopes, such as /// built-in GDScript functions like `abs()` or `min()`. /// Superglobals never need to be closed around or qualified, even /// if imported. Superglobal, /// A global function is defined at the global scope and will be /// compiled to a globally-scoped function. When imported, globals /// will have their name qualified. Global functions do not require /// closures, as they do not close around any data. Global, /// A semiglobal function is still local in scope from the GDLisp /// perspective, but it doesn't require any variable closures, so it /// will be promoted to a global function on the GDScript side, for /// efficiency reasons. SemiGlobal, /// A local function is a closure which exists as a local variable /// on the GDScript side, very similar to a lambda but with /// different name resolution rules. The string parameter is the /// name of the (GDScript) local variable which contains the /// function. The variable should refer to an object with /// `call_func` and `call_funcv` methods. If the variable has /// nonstandard method names, then [`FnScope::SpecialLocal`] should /// be used instead, as the object is not suitable as a funcref in /// this case. Local(String), /// A special local function is like a local function in that it /// needs a closure. But a special local function is potentially /// constructed with several other functions like it, so it will /// still need an explicit closure if referred to via a funcref. /// This is the worst case scenario, as we can make no assumptions /// about the scoping of this function. As with [`FnScope::Local`], /// the argument to `FnScope::SpecialLocal` should be the name of a /// local variable on which the function is defined. No assumptions /// are made about the name of the function on this object, only /// that it exists. SpecialLocal(String), } /// Like [`VarName`](super::local_var::VarName), this will eventually /// translate into an [`Expr`] (or possibly a lack thereof) and /// consists of all of the expressions which denote valid function /// "name" translations. /// /// `FnName` should be thought of as a more restricted form of `Expr`. /// The ultimate goal of this type is to eventually be converted to an /// `Expr` via [`From::from`]. #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum FnName { /// A static function local to the file and defined at the /// top-level. FileConstant, /// A superglobal name, such as built-in GDScript functions. Superglobal, /// A file-level function defined in another file and imported. ImportedConstant(Box), /// A local function referenced using a local variable. OnLocalVar(Box), /// A local function referenced by the current local scope. OnLocalScope, } /// A specification of the parameters a function takes. `FnSpecs` is /// similar to [`ArgList`](crate::ir::arglist::ordinary::ArgList) /// except that the latter specifies names for its arguments, whereas /// this structure simply designates the shape of the function. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct FnSpecs { /// The number of required parameters to the function. If the /// function is provided with fewer than this many arguments, an /// error is issued. pub required: usize, /// The number of optional parameters to the function. After /// required arguments are bound, the next arguments are bound to /// optional parameters until the argument list is exhausted or /// optional parameters have all been bound. pub optional: usize, /// The "rest" argument. After required and optional arguments have /// been handled, if there are excess arguments, they are bound to /// the "rest" parameter. If there is no "rest" parameter and there /// are excess arguments, then an error is issued. pub rest: Option, } /// When referencing enclosing static data from an inner class scope, /// there are two possible types of inner class scopes we could be /// within. /// /// If we're in a static inner scope and need to reference a static /// outer scope, then the inner scope will need to load the current /// file, using [`FnName::inner_static_load`]. On the other hand, if /// we're in a non-static inner scope and need to reference a static /// outer scope, then we're expected to do a single `load` call and /// store the result in an instance variable. /// /// This enum encapsulates these two behaviors. An outer static /// reference is either referred to by a live static load or by an /// instance variable with a given name. #[derive(Debug, Clone, PartialEq, Eq)] pub enum OuterStaticRef<'a> { /// An outer static reference from an inner static scope uses a live /// `load` call on-site. InnerStatic, /// An outer static reference from an inner non-static scope /// references an instance variable on the current (inner) class /// with the given name. InnerInstanceVar(&'a str), } impl FnCall { /// A top-level macro. A macro always has `object` of /// [`FnName::FileConstant`] and `is_macro` of true. pub fn file_macro(specs: FnSpecs, scope: FnScope, function: String) -> FnCall { FnCall { specs, scope, object: FnName::FileConstant, function, is_macro: true } } /// A top-level function, with [`FnName::FileConstant`]. pub fn file_constant(specs: FnSpecs, scope: FnScope, function: String) -> FnCall { FnCall { specs, scope, object: FnName::FileConstant, function, is_macro: false } } /// A superglobal function, with [`FnName::Superglobal`]. pub fn superglobal(specs: FnSpecs, scope: FnScope, function: String) -> FnCall { FnCall { specs, scope, object: FnName::Superglobal, function, is_macro: false } } /// Returns whether the function indicated by this function call /// object can be called in a `const` context, given sufficiently /// constant arguments. pub fn can_be_called_as_const(&self) -> bool { match self.object { FnName::FileConstant => false, FnName::Superglobal => { // Very specific superglobals can be called. CONSTANT_GDSCRIPT_FUNCTIONS.contains(&*self.function) } FnName::ImportedConstant(_) => false, FnName::OnLocalVar(_) => false, FnName::OnLocalScope => false, } } /// As [`FnCall::into_expr_with_magic`] with [`CallMagic::DefaultCall`] as the /// call magic type. pub fn into_expr(self, compiler: &mut Compiler, builder: &mut StmtBuilder, table: &mut SymbolTable, args: Vec, pos: SourceOffset) -> Result { self.into_expr_with_magic(&CallMagic::DefaultCall, compiler, builder, table, args, pos) } /// Compile, via [`CallMagic::compile`], the function call `self` /// into an [`Expr`]. pub fn into_expr_with_magic(self, magic: &CallMagic, compiler: &mut Compiler, builder: &mut StmtBuilder, table: &mut SymbolTable, args: Vec, pos: SourceOffset) -> Result { magic.compile(self, compiler, builder, table, args, pos) } } impl FnSpecs { /// An `FnSpecs` object representing a function of no arguments. pub const EMPTY: FnSpecs = FnSpecs { required: 0, optional: 0, rest: None }; /// Convenience constructor for a `FnSpecs`. pub fn new(required: usize, optional: usize, rest: Option) -> FnSpecs { FnSpecs { required, optional, rest } } /// Returns whether `self` has a "rest" argument of any kind (list /// or array). Equivalent to `self.rest.is_some()`. pub fn has_rest(&self) -> bool { self.rest.is_some() } /// In GDLisp, a function can have a "rich" argument list (as per /// [`ArgList`](crate::ir::arglist::ordinary::ArgList)), consisting /// of required arguments, optional arguments, and a rest argument. /// However, on the GDScript side, we always compile down to a set /// number of required arguments. Given an `FnSpecs`, this method /// returns the number of arguments that will be present in that /// resulting compiled function. /// /// Required arguments in GDLisp compile one-to-one to required /// arguments in GDScript. Optional arguments in GDLisp are /// converted into required arguments and padded (at compile-time) /// with `null` as needed. Finally, if a rest argument (of either /// type) is present, it is converted into a single additional /// argument. /// /// # Examples /// /// ``` /// # use gdlisp::compile::symbol_table::function_call::FnSpecs; /// # use gdlisp::ir::arglist::vararg::VarArg; /// assert_eq!(FnSpecs::new(0, 0, None).runtime_arity(), 0); /// assert_eq!(FnSpecs::new(5, 0, None).runtime_arity(), 5); /// assert_eq!(FnSpecs::new(0, 5, None).runtime_arity(), 5); /// assert_eq!(FnSpecs::new(2, 5, None).runtime_arity(), 7); /// assert_eq!(FnSpecs::new(0, 0, Some(VarArg::RestArg)).runtime_arity(), 1); /// assert_eq!(FnSpecs::new(0, 0, Some(VarArg::ArrArg)).runtime_arity(), 1); /// assert_eq!(FnSpecs::new(2, 1, Some(VarArg::RestArg)).runtime_arity(), 4); /// assert_eq!(FnSpecs::new(1, 2, Some(VarArg::ArrArg)).runtime_arity(), 4); /// ``` pub fn runtime_arity(&self) -> usize { self.required + self.optional + usize::from(self.has_rest()) } /// The minimum number of arguments necessary to correctly call a /// function with this shape. Equivalent to `self.required`. /// /// # Examples /// /// ``` /// # use gdlisp::compile::symbol_table::function_call::FnSpecs; /// # use gdlisp::ir::arglist::vararg::VarArg; /// assert_eq!(FnSpecs::new(0, 0, None).min_arity(), 0); /// assert_eq!(FnSpecs::new(5, 0, None).min_arity(), 5); /// assert_eq!(FnSpecs::new(0, 5, None).min_arity(), 0); /// assert_eq!(FnSpecs::new(2, 5, None).min_arity(), 2); /// assert_eq!(FnSpecs::new(0, 0, Some(VarArg::RestArg)).min_arity(), 0); /// assert_eq!(FnSpecs::new(0, 0, Some(VarArg::ArrArg)).min_arity(), 0); /// assert_eq!(FnSpecs::new(2, 1, Some(VarArg::RestArg)).min_arity(), 2); /// assert_eq!(FnSpecs::new(1, 2, Some(VarArg::ArrArg)).min_arity(), 1); /// ``` pub fn min_arity(&self) -> usize { self.required } /// The maximum number of arguments that can be correctly supplied /// to a function with this shape. /// /// If the function takes a rest argument, then this returns /// [`usize::MAX`]. /// /// # Examples /// /// ``` /// # use gdlisp::compile::symbol_table::function_call::FnSpecs; /// # use gdlisp::ir::arglist::vararg::VarArg; /// assert_eq!(FnSpecs::new(0, 0, None).max_arity(), 0); /// assert_eq!(FnSpecs::new(5, 0, None).max_arity(), 5); /// assert_eq!(FnSpecs::new(0, 5, None).max_arity(), 5); /// assert_eq!(FnSpecs::new(2, 5, None).max_arity(), 7); /// assert_eq!(FnSpecs::new(0, 0, Some(VarArg::RestArg)).max_arity(), usize::MAX); /// assert_eq!(FnSpecs::new(0, 0, Some(VarArg::ArrArg)).max_arity(), usize::MAX); /// assert_eq!(FnSpecs::new(2, 1, Some(VarArg::RestArg)).max_arity(), usize::MAX); /// assert_eq!(FnSpecs::new(1, 2, Some(VarArg::ArrArg)).max_arity(), usize::MAX); /// ``` pub fn max_arity(&self) -> usize { // TODO Is usize.MAX correct here? If we put an upper limit on // function arity, use that instead. if self.has_rest() { usize::MAX } else { self.required + self.optional } } } impl FnScope { /// Whether or not the scope is local. /// /// [`FnScope::Local`] and [`FnScope::SpecialLocal`] are local /// scopes. All other scopes are considered global. pub fn is_local(&self) -> bool { self.local_name().is_some() } /// If `self` refers to a local scope (i.e. /// [`is_local`](FnScope::is_local) returns true), then this method /// returns the name of the GDScript local variable which contains /// the enclosing object for the function. If `self` refers to a /// global scope, this method returns [`None`]. pub fn local_name(&self) -> Option<&str> { match self { FnScope::Local(name) | FnScope::SpecialLocal(name) => Some(name), FnScope::Superglobal | FnScope::Global | FnScope::SemiGlobal => None, } } } impl FnName { /// A value imported from another scope. pub fn imported_constant(orig_name: VarName) -> FnName { FnName::ImportedConstant(Box::new(orig_name)) } /// A call made on a local variable with the given name. pub fn on_local_var(local_name: VarName) -> FnName { FnName::OnLocalVar(Box::new(local_name)) } /// Converts the `FnName` into an appropriate value to be called /// from another module. /// /// If a name `foo` is available at top-level scope `A.gd` and some /// file `B.gd` imports `A.gd` and calls the top-level preload /// constant `AConst`, then calling `foo.as_imported("AConst")` will /// convert the name to how it should be referenced from `B.gd`. pub fn into_imported(self, import_name: String) -> FnName { self.into_imported_var(VarName::FileConstant(import_name)) } pub fn into_imported_var(self, import: VarName) -> FnName { match self { FnName::FileConstant => { FnName::imported_constant(import) } FnName::Superglobal => { FnName::Superglobal } FnName::ImportedConstant(v) => { FnName::imported_constant(v.into_imported_var(import)) } FnName::OnLocalVar(v) => { // This case probably shouldn't happen, but oh well. Delegate to VarName. FnName::on_local_var(v.into_imported_var(import)) } FnName::OnLocalScope => { // This case definitely shouldn't happen. Leave it alone I guess. FnName::OnLocalScope } } } /// Generate a `load` expression for the current file (as per /// `loader`) using `resolver` as the current preload resolver. See /// [`FnName::update_for_inner_scope`] for details on why this is /// necessary. Note that this function is usually called *through* /// that one and should seldom be called directly. /// /// # Panics /// /// If the current filename cannot be detected from `loader`, or if /// `resolver` fails to resolve the name given by `loader`, then /// this function will panic. pub fn inner_static_load(resolver: &(impl PreloadResolver + ?Sized), loader: &impl CanLoad) -> FnName { let fname = inner_class::get_current_filename(loader, resolver) .expect("Cannot identify currently-loading filename"); FnName::on_local_var(VarName::CurrentFile(fname)) } /// In Godot, inner classes do not retain a reference to the /// enclosing scope, not even to be able to call static functions in /// an enclosing scope. GDLisp makes extensive use of static /// file-level functions, so this poses an issue for any nontrivial /// inner class. `inner_static_load` provides a viable workaround /// for this. /// /// If we are in a static context in an inner class and we need to /// refer to an enclosing static function, we need to `load` the /// current file again. In most cases, this `load` will be a cache /// hit and will simply see an existing resource (though it's not /// impossible for a cache miss to occur, and in this case the /// `load` will still work as intended, albeit a bit slower). /// `inner_static_load` constructs an `FnName` representing a `load` /// expression for the appropriate file. `resolver` shall be the /// current preload resolver, and `loader`, naturally, provides the /// current filename. /// /// If we're in a non-static inner scope and need to refer to an /// enclosing static scope, then for efficiency reasons we don't /// want to load the outer script every time. Instead, we simply /// require that an instance variable (whose name is given by /// `outer_ref_name`) on the class be loaded (via `load`) to point /// to the current file at construction time. /// /// See [Issue #30](https://github.com/Mercerenies/gdlisp/issues/30) /// for a further discussion. pub fn update_for_inner_scope(&mut self, binding_type: &OuterStaticRef<'_>, resolver: &(impl PreloadResolver + ?Sized), loader: &impl CanLoad) { if self.needs_inner_scope_reference() { match binding_type { OuterStaticRef::InnerStatic => { *self = FnName::inner_static_load(resolver, loader); } OuterStaticRef::InnerInstanceVar(outer_ref_name) => { *self = FnName::OnLocalVar(Box::new(VarName::outer_class_ref(outer_ref_name))); } } } } /** * Returns true if this function name type requires an outer class * reference in order to be called. Functions require an outer class * reference if they are either (1) a file-level constant, or (2) * already an outer class reference. The latter case occurs if we're * in a nested lambda inside of a lambda scope that already has an * outer class reference. */ pub fn needs_inner_scope_reference(&self) -> bool { match self { FnName::FileConstant => { true } FnName::OnLocalVar(var_name) => { matches!(&**var_name, VarName::OuterClassRef(_)) } _ => { false } } } /// As [`update_for_inner_scope`](FnName::update_for_inner_scope), /// but takes ownership of `self` and returns the modified `FnName`. pub fn into_inner_scope(mut self, binding_type: &OuterStaticRef<'_>, resolver: &(impl PreloadResolver + ?Sized), loader: &impl CanLoad) -> Self { self.update_for_inner_scope(binding_type, resolver, loader); self } /// **Note:** An `Option` here does NOT denote failure to convert. /// `FnName` can be converted to an `Option`, in the sense /// that "there is no expression here" is a completely valid result /// of conversion and indicates a function call which is not /// subscripted on a name. pub fn into_expr(self, pos: SourceOffset) -> Option { match self { FnName::FileConstant => None, FnName::Superglobal => None, FnName::ImportedConstant(var_name) => Some(var_name.into_expr(pos)), FnName::OnLocalVar(var_name) => Some(var_name.into_expr(pos)), FnName::OnLocalScope => None, } } } impl<'a> OuterStaticRef<'a> { /// If `static_binding` is true, then this method returns /// [`OuterStaticRef::InnerStatic`]. Otherwise, this method returns /// [`OuterStaticRef::InnerInstanceVar`] with `outer_ref_name` as /// the reference name. This is useful in situations where we /// already have an outer reference name and simply need to know /// whether we can use it from the given scope. pub fn from_ref_type(static_binding: bool, outer_ref_name: &'a str) -> Self { if static_binding { OuterStaticRef::InnerStatic } else { OuterStaticRef::InnerInstanceVar(outer_ref_name) } } /// Converts `None` to [`OuterStaticRef::InnerStatic`] and `Some` to /// [`OuterStaticRef::InnerInstanceVar`]. pub fn from_option_ref(outer_ref_name: Option<&'a str>) -> Self { match outer_ref_name { None => OuterStaticRef::InnerStatic, Some(ref_name) => OuterStaticRef::InnerInstanceVar(ref_name), } } } impl From for FnName { fn from(var_name: VarName) -> FnName { FnName::imported_constant(var_name) } } impl From for Expecting { fn from(specs: FnSpecs) -> Expecting { Expecting::new(specs.min_arity(), specs.max_arity()) } } ================================================ FILE: src/compile/symbol_table/inner.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`InnerSymbolTable`] structure, which behaves mostly //! like a [`SymbolTable`] but returns global synthetic variables when //! it goes out of scope, as if by //! [`SymbolTable::dump_synthetics_to`]. use super::SymbolTable; use std::ops::{Deref, DerefMut}; /// An `InnerSymbolTable` is a thin wrapper around a [`SymbolTable`]. /// However, `InnerSymbolTable` keeps a (mutable) reference to an /// enclosing symbol table, and when the `InnerSymbolTable` is /// dropped, it dumps all global synthetic variables from the current /// symbol table into the enclosing table, as if by /// [`SymbolTable::dump_synthetics_to`]. #[derive(Debug)] pub struct InnerSymbolTable<'a> { inner_table: SymbolTable, outer_table: &'a mut SymbolTable, } impl<'a> InnerSymbolTable<'a> { /// Constructs a new `InnerSymbolTable` object wrapping /// `inner_table` and pointing to the enclosing table `outer_table`. pub fn new(inner_table: SymbolTable, outer_table: &'a mut SymbolTable) -> Self { InnerSymbolTable { inner_table, outer_table } } /// Constructs a new `InnerSymbolTable` with no concrete symbols and /// with synthetic symbols inherited from the enclosing table. pub fn with_synthetics_from(outer_table: &'a mut SymbolTable) -> Self { let inner_table = SymbolTable::with_synthetics_from(outer_table); Self::new(inner_table, outer_table) } /// Constructs a new `InnerSymbolTable` which is a clone of /// `outer_table` and points to it. pub fn cloned_from(outer_table: &'a mut SymbolTable) -> Self { let inner_table = outer_table.clone(); Self::new(inner_table, outer_table) } } impl<'a> Deref for InnerSymbolTable<'a> { type Target = SymbolTable; fn deref(&self) -> &SymbolTable { &self.inner_table } } impl<'a> DerefMut for InnerSymbolTable<'a> { fn deref_mut(&mut self) -> &mut SymbolTable { &mut self.inner_table } } impl<'a> Drop for InnerSymbolTable<'a> { fn drop(&mut self) { self.inner_table.dump_synthetics_to(self.outer_table); } } ================================================ FILE: src/compile/symbol_table/local_var.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! [`LocalVar`] and various helper structures for storing information //! about variables. use crate::ir::access_type::AccessType; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::literal::Literal; use crate::gdscript::library::cell::CELL_CONTENTS; use crate::gdscript::class_extends::ClassExtends; use crate::pipeline::source::SourceOffset; use crate::compile::names; use serde::{Serialize, Deserialize}; use std::borrow::ToOwned; use std::convert::TryFrom; use std::fmt; use std::error::Error; use std::ffi::OsStr; /// All of the relevant information needed to understand a variable is /// stored in `LocalVar`. Despite its name, this structure is used to /// describe all names in the variable namespace in Godot, including /// local variables, global constants, and class names. #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct LocalVar { /// The name of the variable. pub name: VarName, /// The broadest type of access that will ever be required of the /// variable. pub access_type: AccessType, /// The scope of the variable. pub scope: VarScope, /// Whether or not the variable can be assigned to. pub assignable: bool, /// In the case of global constants, the compiler often knows the /// constant's value at compile-time. We can use this information to /// provide optimizations, as well as possible warnings about /// incorrect usage of a value. If no value is known, then /// `value_hint` is simply `None`. pub value_hint: Option, } /// `VarName` will eventually translate into an [`Expr`] (via /// [`From::from`]). This type should be thought of as a spiritual /// subtype of `Expr`, only supporting a strict subset of expressions /// which are necessary for name translations. #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum VarName { /// A variable which is local to the current scope and can be seen /// unqualified. Local(String), /// Similar to `Local`, with the caveat that the variable /// `OuterClassRef` refers to is an outer class reference. These are /// treated like local variables for most purposes but will be /// handled specially when used in nested closures. OuterClassRef(String), /// A file-level constant defined in the current file. FileConstant(String), /// A superglobal name, such as built-in GDScript constants. These /// names will never be modified if imported. Superglobal(String), /// A file-level constant defined in another file and imported. ImportedConstant(Box, String), /// A file-level constant subscripted by a given constant numerical value. SubscriptedConstant(Box, i32), /// The current file, as a constant value. This is semantically /// similar to a [`VarName::FileConstant`], but it is a special /// case, as `VarName::CurrentFile` is required to compile to a /// `load(...)` call rather than a simple name. CurrentFile(String), /// A `load` call on a file other than the current file. DirectLoad(String), /// A null value. This is used as a placeholder for things in the /// value namespace that don't have runtime presence, such as symbol /// macros. Null, } /// [`VarName`] can always be converted (without loss of information) /// into an [`Expr`]. It can also sometimes be converted (via /// [`TryFrom`]) into a [`ClassExtends`](ClassExtends). This is /// the error type that can result when the latter conversion fails. #[derive(Clone, Debug, PartialEq, Eq)] pub enum VarNameIntoExtendsError { /// It is not permitted to have a class extend a local variable. /// Even if the class is an anonymous class defined at local scope, /// there is no good semantic way to permit this behavior. CannotExtendLocal(String), /// It is not permitted to have a class which extends a subscripted /// (i.e. `foo[bar]`) expression. CannotExtendSubscript(Box, i32), /// It is not permitted to have a class extend the currently loading /// file. This would cause a cyclic load error in Godot. CannotExtendCurrentFile(String), /// It is not permitted to have a class extend a `load` function /// call. CannotExtendLoadDirective(String), /// The null [`VarName`] is used in some places as a placeholder for /// values that don't exist at runtime (i.e. those fully handled /// during the IR macro expansion phase). Needless to say, we can't /// extend from a name that doesn't exist at runtime. CannotExtendNull, } /// The scope of a variable. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum VarScope { /// Global variables are available at least in the current file, if /// not everywhere. A global variable never needs to be closed over, /// since any closures will be lifted no further than the top of the /// current file. GlobalVar, /// Local variables are available in some scope strictly smaller /// than file-level. A local variable *does* need a closure if /// referenced in an inner lambda or anonymous class, as it may not /// be available outside the original scope of the variable. LocalVar, } /// A hint as to the value of a [`VarName`], if known. This /// information can be used in optimizations or possible compiler /// warnings. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ValueHint { /// The variable refers to a class. Class names cannot be reassigned /// and are always valid as types. ClassName, /// The variable refers to a singleton object. Singleton objects /// cannot be reassigned. ObjectName, /// The variable refers to a GDScript literal value, whose exact /// value is already known. Literal(Literal), /// The variable refers to an enumeration declaration with the given /// names as enum cases. If this value hint is given, then `:` slot /// access can be checked at compile time to ensure that the name in /// question actually exists on the enum. Enum(Vec), /// The variable is a constant whose exact value may not be known. GlobalConstant, /// The variable is the result of a superglobal declaration. /// Currently, this is only available if explicitly requested via /// `sys/declare`. Superglobal, /// A symbol macro name. If we end up using one of these at runtime /// somehow, then it should be a hard error wherever possible. SymbolMacro, } /// A `ValueHintsTable` provides a means to look up a variable and get /// a potential [`ValueHint`] for the variable. The most relevant /// implementor of this trait is [`SymbolTable`](super::SymbolTable), /// where the value hint is simply looked up in the current scope. pub trait ValueHintsTable { /// Get a hint for the value of the variable with name `name`. /// Returns `None` if no hint is available. fn get_value_hint(&self, name: &str) -> Option<&ValueHint>; } /// `VacuousValueHintsTable` is a trivial implementation of /// [`ValueHintsTable`] that never returns a hint. pub struct VacuousValueHintsTable; impl ValueHintsTable for VacuousValueHintsTable { fn get_value_hint(&self, _name: &str) -> Option<&ValueHint> { None } } impl LocalVar { /// A read-only (non-closure) local variable, as per /// [`LocalVar::local`]. pub fn read(name: String) -> LocalVar { LocalVar::local(name, AccessType::Read) } /// A read-write (non-closure) local variable, as per /// [`LocalVar::local`]. pub fn rw(name: String) -> LocalVar { LocalVar::local(name, AccessType::RW) } /// A closed read-write local variable, as per [`LocalVar::local`]. pub fn closed_rw(name: String) -> LocalVar { LocalVar::local(name, AccessType::ClosedRW) } /// A local variable with the given access type. pub fn local(name: String, access_type: AccessType) -> LocalVar { LocalVar { name: VarName::Local(name), access_type: access_type, scope: VarScope::LocalVar, assignable: true, value_hint: None, } } /// A local outer class reference variable with the given access /// type. pub fn outer_class_ref(name: String, access_type: AccessType) -> LocalVar { LocalVar { name: VarName::OuterClassRef(name), access_type: access_type, scope: VarScope::LocalVar, assignable: true, value_hint: None, } } /// A superglobal variable, available in all GDScript scopes, not /// just the current file. Superglobals are always /// [`AccessType::Read`] and are never `assignable`. pub fn superglobal(name: String) -> LocalVar { LocalVar { name: VarName::Superglobal(name), access_type: AccessType::Read, scope: VarScope::GlobalVar, assignable: false, value_hint: None, } } /// A file-level constant GDScript variable. File-level constants /// are always [`AccessType::Read`] and are never `assignable`. pub fn file_constant(name: String) -> LocalVar { LocalVar { name: VarName::FileConstant(name), access_type: AccessType::Read, scope: VarScope::GlobalVar, assignable: false, value_hint: None, } } /// A `LocalVar` representing the current file (whose name is /// `name`) via [`VarName::CurrentFile`]. The current file is always /// a non-`assignable` value with access type [`AccessType::Read`]. pub fn current_file(name: String) -> LocalVar { LocalVar { name: VarName::CurrentFile(name), access_type: AccessType::Read, scope: VarScope::GlobalVar, assignable: false, value_hint: None, } } /// A `LocalVar` referencing the special GDScript `self` variable. /// `self` is a local, non-assignable variable. We give it access /// type [`AccessType::ClosedRead`], as the broadest possible access /// type `self` can use, since it cannot be written to but it could /// potentially be closed over. pub fn self_var() -> LocalVar { // TODO Should this be a special case? // Note: Cannot assign to self LocalVar::local(String::from("self"), AccessType::ClosedRead).no_assign() } /// Sets `self.assignable` to false and returns `self`. This method /// is intended to be used in a builder style. pub fn no_assign(mut self) -> Self { self.assignable = false; self } /// Sets the `self.value_hint` value and returns `self`. This method /// is intended to be used in a builder style. pub fn with_hint(mut self, value_hint: ValueHint) -> Self { self.value_hint = Some(value_hint); self } /// The simple, unqualified name of the variable, if it exists. /// Equivalent to `self.name.simple_name()`. pub fn simple_name(&self) -> Option<&str> { self.name.simple_name() } /// Replaces the simple, unqualified name of the variable. If the /// variable has no unqualified name, then this function does /// nothing. pub fn set_simple_name(&mut self, name: String) { match &mut self.name { VarName::Local(s) => { *s = name; } VarName::OuterClassRef(s) => { *s = name; } VarName::FileConstant(s) => { *s = name; } VarName::Superglobal(s) => { *s = name; } VarName::ImportedConstant(_, _) => {} VarName::SubscriptedConstant(_, _) => {} VarName::CurrentFile(_) => {} VarName::DirectLoad(_) => {} VarName::Null => {} } } /// Returns whether or not the variable name in question compiles to /// an expression which is valid as the right-hand side of a `const` /// in GDScript. /// /// This function is permitted to be overly conservative. That is, /// in situations where the variable name may or may not be valid, /// this function will return `false`. However, if this function /// returns `true`, then it *must* be valid as a `const` expression. pub fn is_valid_const_expr(&self) -> bool { match &self.name { VarName::Local(_) => false, VarName::OuterClassRef(_) => false, VarName::FileConstant(_) | VarName::ImportedConstant(_, _) | VarName::SubscriptedConstant(_, _) => { // If it's a top-level constant, use the value hint to figure // out which constant. match self.value_hint { None => false, Some(ValueHint::ClassName) => true, Some(ValueHint::ObjectName) => false, // Implemented as a 0-ary function call by GDLisp Some(ValueHint::Literal(_)) => true, Some(ValueHint::Enum(_)) => true, Some(ValueHint::GlobalConstant) => true, Some(ValueHint::Superglobal) => true, Some(ValueHint::SymbolMacro) => false, // How did we even get into this situation? } } VarName::Superglobal(_) => true, VarName::CurrentFile(_) => false, VarName::DirectLoad(_) => false, VarName::Null => true, } } /// An `Expr` which references the value of this variable. If this /// variable requires a cell (`self.access_type.requires_cell()`), /// then this access expression contains the necessary subscripting /// to access the *contents* of the cell, not the cell itself. pub fn expr(&self, pos: SourceOffset) -> Expr { let inner: Expr = self.name.clone().into_expr(pos); if self.access_type.requires_cell() { Expr::new(ExprF::Attribute(Box::new(inner), CELL_CONTENTS.to_owned()), pos) } else { inner } } // TODO Put all of the declaration-site stuff here as well, like // .expr() for access, so we have it all in one place (i.e. the // difference between "var x = ..." and "var x = Cell.new(...)") } impl VarName { /// Helper function to produce a `load(...)` GDScript expression for /// the file named `filename`. pub fn load_expr(filename: String, pos: SourceOffset) -> Expr { Expr::call(None, "load", vec!(Expr::from_value(filename, pos)), pos) } /// `VarName` for a local variable. pub fn local(name: &str) -> VarName { VarName::Local(String::from(name)) } /// `VarName` for an outer class reference (local) variable. pub fn outer_class_ref(name: &str) -> VarName { VarName::OuterClassRef(String::from(name)) } /// `VarName` for a file-level constant. pub fn file_constant(name: &str) -> VarName { VarName::FileConstant(String::from(name)) } /// `VarName` for a superglobal variable. pub fn superglobal(name: &str) -> VarName { VarName::Superglobal(String::from(name)) } /// `VarName` for an imported constant name. pub fn imported_constant(orig_name: VarName, name: &str) -> VarName { VarName::ImportedConstant(Box::new(orig_name), String::from(name)) } /// `VarName` for the current file. pub fn current_file(filename: &str) -> VarName { VarName::CurrentFile(String::from(filename)) } /// Converts `self` to valid GDScript syntax. Equivalent to /// `self.clone().into_expr(pos).to_gd()`. pub fn to_gd(&self, pos: SourceOffset) -> String { self.clone().into_expr(pos).to_gd() } /// If `self` refers to a simple (unqualified) name, such as a local /// variable or an unimported constant defined in the current file, /// then this method returns the name. If `self` refers to a /// qualified or otherwise special name (such as a `load` on the /// current file), then this method returns `None`. pub fn simple_name(&self) -> Option<&str> { match self { VarName::Local(s) => Some(s), VarName::OuterClassRef(s) => Some(s), VarName::FileConstant(s) => Some(s), VarName::Superglobal(s) => Some(s), VarName::ImportedConstant(_, _) => None, VarName::SubscriptedConstant(_, _) => None, VarName::CurrentFile(_) => None, VarName::DirectLoad(_) => None, VarName::Null => None, } } /// Converts the `VarName` into an appropriate value to be called /// from another module. /// /// If a name `foo` is available at top-level scope `A.gd` and some /// file `B.gd` imports `A.gd` and calls the top-level preload /// constant `AConst`, then calling `foo.as_imported("AConst")` will /// convert the name to how it should be referenced from `B.gd`. pub fn into_imported(self, import_name: String) -> VarName { self.into_imported_var(VarName::FileConstant(import_name)) } pub fn into_imported_var(self, import: VarName) -> VarName { match self { VarName::Local(s) => { // To be honest, this case probably should never occur. So // we'll just pretend it's FileConstant. VarName::ImportedConstant(Box::new(import), s) } VarName::OuterClassRef(s) => { // To be honest, this case probably should never occur. So // we'll just pretend it's FileConstant. VarName::ImportedConstant(Box::new(import), s) } VarName::FileConstant(s) => { // Import file constants by qualifying the name. VarName::ImportedConstant(Box::new(import), s) } VarName::Superglobal(s) => { // Superglobals are always in scope and don't change on import. VarName::Superglobal(s) } VarName::ImportedConstant(lhs, s) => { // Import the constant transitively. let lhs = Box::new(lhs.into_imported_var(import)); VarName::ImportedConstant(lhs, s) } VarName::SubscriptedConstant(lhs, n) => { // Import the constant transitively. let lhs = Box::new(lhs.into_imported_var(import)); VarName::SubscriptedConstant(lhs, n) } VarName::DirectLoad(filename) => { // A direct load does not change when imported. It still directly loads the same file. VarName::DirectLoad(filename) } VarName::CurrentFile(_) => { // The current file imports as the name of the import itself. import } VarName::Null => { // Null is already available everywhere. VarName::Null } } } /// [`VarName`] can always be converted into an [`Expr`]. `VarName` /// is, by definition, the subset of GDScript expressions suitable /// for variable name expansion. pub fn into_expr(self, pos: SourceOffset) -> Expr { match self { VarName::Local(s) => Expr::new(ExprF::Var(s), pos), VarName::OuterClassRef(s) => Expr::new(ExprF::Var(s), pos), VarName::FileConstant(s) => Expr::new(ExprF::Var(s), pos), VarName::Superglobal(s) => Expr::new(ExprF::Var(s), pos), VarName::ImportedConstant(lhs, s) => Expr::new(ExprF::Attribute(Box::new(lhs.into_expr(pos)), s), pos), VarName::SubscriptedConstant(lhs, n) => Expr::new(ExprF::Subscript(Box::new(lhs.into_expr(pos)), Box::new(Expr::from_value(n, pos))), pos), VarName::DirectLoad(filename) => VarName::load_expr(filename, pos), VarName::CurrentFile(filename) => VarName::load_expr(filename, pos), VarName::Null => Expr::null(pos), } } } /// Classes extend from variables which have `VarName`. We can /// (attempt to) convert from `VarName` to [`ClassExtends`]. We /// cannot, however, extend from local variables using this technique. impl TryFrom for ClassExtends { type Error = VarNameIntoExtendsError; // Note: This will only succeed into ClassExtends::Qualified. It // will never produce any other alternative value for ClassExtends. // This behavior may change in the future, at which point we'll need // to change VarName::ImportedConstant to transitively handle that // (or at least err in a better way than panicking). fn try_from(var_name: VarName) -> Result { match var_name { VarName::Local(s) => { Err(VarNameIntoExtendsError::CannotExtendLocal(s)) } VarName::OuterClassRef(s) => { Err(VarNameIntoExtendsError::CannotExtendLocal(s)) } VarName::FileConstant(s) => { Ok(ClassExtends::SimpleIdentifier(s)) } VarName::Superglobal(s) => { Ok(ClassExtends::SimpleIdentifier(s)) } VarName::ImportedConstant(lhs, s) => { ClassExtends::try_from(*lhs).map(|x| x.attribute(s)) } VarName::SubscriptedConstant(lhs, n) => { Err(VarNameIntoExtendsError::CannotExtendSubscript(lhs, n)) } VarName::DirectLoad(s) => { Err(VarNameIntoExtendsError::CannotExtendLoadDirective(s)) } VarName::CurrentFile(s) => { Err(VarNameIntoExtendsError::CannotExtendCurrentFile(s)) } VarName::Null => { Err(VarNameIntoExtendsError::CannotExtendNull) } } } } impl ValueHint { /// Helper method for constructing a [`ValueHint::Enum`] from an /// iterator of unowned strings. Implicitly clones the strings and /// stores them in a `ValueHint::Enum`. pub fn enumeration<'a>(values: impl Iterator) -> ValueHint { ValueHint::Enum(values.map(names::lisp_to_gd).collect()) } /// The value hint for a resource with the given file extension. The /// file extension should *not* include the dot. If the file type /// cannot be inferred or if there isn't a value hint that /// represents the given file type, then `None` is returned. pub fn from_file_ext(file_extension: &OsStr) -> Option { let file_extension = file_extension.to_ascii_lowercase(); if file_extension == "gd" { Some(ValueHint::ClassName) } else { None } } } impl fmt::Display for VarNameIntoExtendsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { VarNameIntoExtendsError::CannotExtendLocal(s) => { write!(f, "Cannot extend local variable {}", s) } VarNameIntoExtendsError::CannotExtendSubscript(expr, n) => { let expr = expr.clone().into_expr(SourceOffset(0)).subscript(Expr::from_value(*n, SourceOffset(0)), SourceOffset(0)); write!(f, "Cannot extend reference to current file {}", expr.to_gd()) } VarNameIntoExtendsError::CannotExtendCurrentFile(s) => { write!(f, "Cannot extend reference to current file {}", s) } VarNameIntoExtendsError::CannotExtendLoadDirective(s) => { write!(f, "Cannot extend direct load of file {}", s) } VarNameIntoExtendsError::CannotExtendNull => { write!(f, "Cannot extend null value") } } } } impl Error for VarNameIntoExtendsError {} #[cfg(test)] mod tests { use super::*; // TODO More fn qualified(mut name: Vec<&'static str>) -> ClassExtends { assert!(!name.is_empty(), "Empty identifier not allowed"); let first = ClassExtends::SimpleIdentifier(name.remove(0).to_owned()); name.into_iter().fold(first, |acc, name| acc.attribute(name)) } #[test] fn extends_constant() { assert_eq!( ClassExtends::try_from(VarName::file_constant("Abc")), Ok(qualified(vec!("Abc"))), ); } #[test] fn extends_superglobal() { assert_eq!( ClassExtends::try_from(VarName::superglobal("Node")), Ok(qualified(vec!("Node"))), ); } #[test] fn extends_imported() { assert_eq!( ClassExtends::try_from(VarName::imported_constant(VarName::file_constant("Foo"), "MyClass")), Ok(qualified(vec!("Foo", "MyClass"))), ); } #[test] fn cannot_extend_local() { assert_eq!( ClassExtends::try_from(VarName::local("abc")), Err(VarNameIntoExtendsError::CannotExtendLocal(String::from("abc"))), ); } #[test] fn cannot_extend_subscripted() { assert_eq!( ClassExtends::try_from(VarName::SubscriptedConstant(Box::new(VarName::file_constant("abc")), 10)), Err(VarNameIntoExtendsError::CannotExtendSubscript(Box::new(VarName::file_constant("abc")), 10)), ); } #[test] fn cannot_extend_current_file() { assert_eq!( ClassExtends::try_from(VarName::CurrentFile(String::from("Filename.gd"))), Err(VarNameIntoExtendsError::CannotExtendCurrentFile(String::from("Filename.gd"))), ); } #[test] fn cannot_extend_null_value() { assert_eq!( ClassExtends::try_from(VarName::Null), Err(VarNameIntoExtendsError::CannotExtendNull), ); } #[test] fn from_file_ext_test() { assert_eq!(ValueHint::from_file_ext(OsStr::new("GD")), Some(ValueHint::ClassName)); assert_eq!(ValueHint::from_file_ext(OsStr::new("gd")), Some(ValueHint::ClassName)); assert_eq!(ValueHint::from_file_ext(OsStr::new("tscn")), None); assert_eq!(ValueHint::from_file_ext(OsStr::new("png")), None); assert_eq!(ValueHint::from_file_ext(OsStr::new("abc")), None); } } ================================================ FILE: src/compile/symbol_table/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Symbol tables store names and keep track of what variable or //! function they reference. //! //! See the [`SymbolTable`] structure for more. For anything in the //! variable namespace, we use [`local_var::LocalVar`] to keep track //! of the value. For anything in the function namespace, we use //! [`function_call::FnCall`] to keep track of its value, and we use //! [`call_magic::CallMagic`] for special call semantics on functions. pub mod call_magic; pub mod function_call; pub mod inner; pub mod local_var; use crate::ir::identifier::{Id, Namespace}; use function_call::FnCall; use call_magic::{CallMagic}; use local_var::{LocalVar, ValueHint, ValueHintsTable}; use serde::{Serialize, Deserialize}; use std::collections::HashMap; use std::collections::HashSet; use std::borrow::Borrow; /// GDLisp has two separate namespaces when it comes to name /// resolution: the variable namespace and the function namespace. /// Types, such as classes and primitive types, fall into the variable /// namespace. `SymbolTable` encompasses a table of names available in /// the current scope, in either namespace. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SymbolTable { /// A mapping from GDLisp names to [`LocalVar`] instances, which can /// be local variables, file-level constants, GDScript constants, or /// several other types of values. locals: HashMap, /// A mapping from GDScript names to GDLisp names. The values in /// this map shall always be valid keys in `locals`. reverse_locals: HashMap, /// A mapping of GDScript variables that were /// synthetically-generated and do not have a GDLisp name /// corresponding to them. The Boolean indicates whether the /// variable is local to the current function or not. Locals will be /// cleared by [`SymbolTable::clear_synthetic_locals`]. synthetic_locals: HashMap, /// A mapping from GDLisp names to [`FnCall`] and corresponding /// [`CallMagic`] instances. functions: HashMap, /// A mapping from GDScript names to GDLisp names. The values in /// this map shall always be valid keys in `functions`. reverse_functions: HashMap, /// A set of GDScript functions that were synthetically-generated /// and do not have a GDLisp name corresponding to them. synthetic_functions: HashSet, } /// When we move into a class scope, we need to keep two symbol /// tables: one for use in static contexts and one for use in instance /// (non-static) contexts. This struct encapsulates that concept. pub struct ClassTablePair<'a, 'b> { /// The table for use in static context. pub static_table: &'a mut SymbolTable, /// The table for use in instance (non-static) context. pub instance_table: &'b mut SymbolTable, } impl SymbolTable { /// A new, empty symbol table. Equivalent to /// `SymbolTable::default()`. pub fn new() -> SymbolTable { SymbolTable::default() } /// A new, empty symbol table which inherits its synthetic variables /// from the given symbol table. Only global synthetic variables and /// functions are inherited; local synthetic variables from the /// prior table are discarded. pub fn with_synthetics_from(table: &SymbolTable) -> SymbolTable { let mut new_table = SymbolTable::new(); table.dump_synthetics_to(&mut new_table); new_table } /// Copies the global synthetic variables and functions from `self` /// back into the given table. Local synthetic names are ignored. pub fn dump_synthetics_to(&self, destination_table: &mut SymbolTable) { // Variables for (var, is_local) in &self.synthetic_locals { if !is_local { destination_table.add_synthetic_var(var.to_owned(), false); } } // Functions (cannot be local, so assume all are global) for fun in &self.synthetic_functions { destination_table.add_synthetic_fn(fun.to_owned()); } } /// Gets the variable with the given GDLisp name, or `None` if no such /// variable exists in the table. pub fn get_var(&self, name: &str) -> Option<&LocalVar> { self.locals.get(name) } /// Gets the variable with the given local GDScript name. Variables /// are indexed by both names, so this access is as efficient as /// [`SymbolTable::get_var`]. pub fn get_var_by_gd_name(&self, gd_name: &str) -> Option<&LocalVar> { self.reverse_locals.get(gd_name).and_then(|name| self.get_var(name)) } /// Gets the function with the given local GDScript name. Functions /// are indexed by both names, so this access is as efficient as /// [`SymbolTable::get_fn`]. pub fn get_fn_by_gd_name(&self, gd_name: &str) -> Option<(&FnCall, &CallMagic)> { self.reverse_functions.get(gd_name).and_then(|name| self.get_fn(name)) } /// Sets the variable with the given GDLisp name, returning the old /// value if one existed. pub fn set_var(&mut self, name: String, value: LocalVar) -> Option { let new_gd_name = value.simple_name().map(|x| x.to_owned()); let old_value = self.locals.insert(name.clone(), value); if let Some(old_value) = &old_value { if let Some(old_gd_name) = old_value.simple_name() { self.reverse_locals.remove(old_gd_name); } } if let Some(new_gd_name) = new_gd_name { self.reverse_locals.insert(new_gd_name, name); } old_value } /// Removes the variable with the given GDLisp name. If no such /// variable exists, then nothing is changed. pub fn del_var(&mut self, name: &str) { let value = self.locals.remove(name); if let Some(value) = value { if let Some(gd_name) = value.simple_name() { self.reverse_locals.remove(gd_name); } } } /// Adds a synthetic GDScript variable to the symbol table. /// Synthetic GDScript variables do not appear in the symbol table's /// maps but will respond to [`SymbolTable::has_var_with_gd_name`]. pub fn add_synthetic_var(&mut self, name: String, is_local: bool) { self.synthetic_locals.insert(name, is_local); } /// Removes a synthetic GDScript variable. See /// [`SymbolTable::add_synthetic_var`]. pub fn remove_synthetic_var(&mut self, name: &str) { self.synthetic_locals.remove(name); } /// Removes all synthetic variables marked with `is_local`. pub fn clear_synthetic_locals(&mut self) { self.synthetic_locals.retain(|_, is_local| !*is_local); } /// Gets the function call and call magic associated with the given /// GDLisp function name. pub fn get_fn(&self, name: &str) -> Option<(&FnCall, &CallMagic)> { self.functions.get(name).map(|(call, magic)| { (call, magic) }) } /// Sets the function call and call magic associated with the given /// function name. pub fn set_fn(&mut self, name: String, value: FnCall, magic: CallMagic) { let new_gd_name = value.function.clone(); let old_function = self.functions.insert(name.clone(), (value, magic)); if let Some((old_function, _)) = old_function { self.reverse_functions.remove(&old_function.function); } self.reverse_functions.insert(new_gd_name, name); } /// Deletes any function call info and call magic associated with /// the given function name. pub fn del_fn(&mut self, name: &str) { let old_function = self.functions.remove(name); if let Some((old_function, _)) = old_function { self.reverse_locals.remove(&old_function.function); } } /// Adds a synthetic GDScript function to the symbol table. /// Synthetic GDScript functions do not appear in the symbol table's /// maps but will respond to [`SymbolTable::has_fn_with_gd_name`]. pub fn add_synthetic_fn(&mut self, name: String) { self.synthetic_functions.insert(name); } /// Removes a synthetic GDScript function. See /// [`SymbolTable::add_synthetic_fn`]. pub fn remove_synthetic_fn(&mut self, name: &str) { self.synthetic_functions.remove(name); } /// An iterator over the variable namespace of this table. pub fn vars(&self) -> impl Iterator { return self.locals.iter().map(|x| (x.0.borrow(), x.1)); } /// An iterator over the function namespace of this table. pub fn fns(&self) -> impl Iterator { self.functions.iter().map(|(name, value)| { (name.borrow(), &value.0, &value.1) }) } /// An iterator over the function namespace of this table, providing /// mutable access to the function call information and the call /// magic. pub fn fns_mut(&mut self) -> impl Iterator { self.functions.iter_mut().map(|(name, value)| { (name.borrow(), &mut value.0, &mut value.1) }) } /// An iterator over all of the names in the symbol table, in both /// namespaces. The order in which this iterator outputs names is /// not specified. pub fn keys(&self) -> impl Iterator { let vars = self.vars().map(|(name, _)| (Namespace::Value, name)); let fns = self.fns().map(|(name, _, _)| (Namespace::Function, name)); vars.chain(fns) } /// An iterator over all of the names in the symbol table, taking /// ownership of the respective identifiers, in both namespaces. The /// order in which this iterator outputs names is not specified. pub fn into_keys(self) -> impl Iterator { let vars = self.locals.into_iter().map(|(name, _)| Id::new(Namespace::Value, name)); let fns = self.functions.into_iter().map(|(name, _)| Id::new(Namespace::Function, name)); vars.chain(fns) } /// Iterates over both namespaces of `other` and sets the variable /// and function names in `self` to the values from `other`. If /// values already existed in `self` for some name, then they are /// overwritten. pub fn assign_from(&mut self, other: &SymbolTable) { for (name, value) in other.vars() { self.set_var(name.to_owned(), value.to_owned()); } for (name, call, magic) in other.fns() { self.set_fn(name.to_owned(), call.to_owned(), magic.to_owned()); } } /// Returns true if the symbol table has a variable with the given /// GDScript name. pub fn has_var_with_gd_name(&self, name: &str) -> bool { self.reverse_locals.contains_key(name) || self.synthetic_locals.contains_key(name) } /// Returns true if the symbol table has a function with the given /// GDScript name. pub fn has_fn_with_gd_name(&self, name: &str) -> bool { self.reverse_functions.contains_key(name) || self.synthetic_functions.contains(name) } } /// Trait for objects which have a symbol table. Currently, there is /// only one implementor of this trait: [`SymbolTable`] itself. pub trait HasSymbolTable { /// Borrows the symbol table from `self`. fn get_symbol_table(&self) -> &SymbolTable; /// Borrows the symbol table mutably from `self`. fn get_symbol_table_mut(&mut self) -> &mut SymbolTable; /// Binds the local variable, calls `block` with `self` as argument, /// then binds the variable back to its previous value. This is the /// correct semantic behavior for introducing a local variable into /// some inner scope in GDScript. fn with_local_var(&mut self, name: String, value: LocalVar, block: F) -> B where F : FnOnce(&mut Self) -> B { let previous = self.get_symbol_table_mut().set_var(name.clone(), value); let result = block(self); if let Some(previous) = previous { self.get_symbol_table_mut().set_var(name, previous); } else { self.get_symbol_table_mut().del_var(&name); }; result } /// Recursive convenience helper for calling /// [`HasSymbolTable::with_local_var`] on several variable names in /// sequence. fn with_local_vars(&mut self, vars: &mut dyn Iterator, block: impl FnOnce(&mut Self) -> B) -> B { if let Some((name, value)) = vars.next() { self.with_local_var(name, value, |curr| { curr.with_local_vars(vars, block) }) } else { block(self) } } /// Binds the local function, calls `block` with `self` as argument, /// then binds the function back to its previous value. This is the /// correct semantic behavior for introducing a local function into /// some inner scope in GDScript. fn with_local_fn(&mut self, name: String, value: FnCall, block: impl FnOnce(&mut Self) -> B) -> B { let previous = self.get_symbol_table_mut().get_fn(&name).map(|(p, m)| (p.clone(), m.clone())); self.get_symbol_table_mut().set_fn(name.clone(), value, CallMagic::DefaultCall); let result = block(self); if let Some((p, m)) = previous { self.get_symbol_table_mut().set_fn(name, p, m); } else { self.get_symbol_table_mut().del_fn(&name); }; result } /// Recursive convenience helper for calling /// [`HasSymbolTable::with_local_fn`] on several function names in /// sequence. fn with_local_fns(&mut self, vars: &mut dyn Iterator, block: impl FnOnce(&mut Self) -> B) -> B { if let Some((name, value)) = vars.next() { self.with_local_fn(name, value, |curr| { curr.with_local_fns(vars, block) }) } else { block(self) } } } impl HasSymbolTable for SymbolTable { fn get_symbol_table(&self) -> &SymbolTable { self } fn get_symbol_table_mut(&mut self) -> &mut SymbolTable { self } } // TODO This is a mess. Can we please store the value hints some way // that doesn't require a reverse lookup on GDScript names, because // that reverse lookup is just going to be awkward no matter what. impl ValueHintsTable for SymbolTable { fn get_value_hint(&self, name: &str) -> Option<&ValueHint> { self.get_var_by_gd_name(name).and_then(|var| var.value_hint.as_ref()) } } impl<'a> ClassTablePair<'a, 'a> { /// Returns either `self.static_table` or `self.instance_table`, /// depending on the type of scope we're in. pub fn into_table(self, is_static: bool) -> &'a mut SymbolTable { if is_static { self.static_table } else { self.instance_table } } } // TODO Test magic here as well. #[cfg(test)] mod tests { use super::*; use local_var::VarName; use function_call::{FnSpecs, FnScope}; fn from_var_name(e: &VarName) -> &str { e.simple_name().expect("Unexpected nontrivial variable name") } #[test] fn test_vars() { let mut table = SymbolTable::new(); assert_eq!(table.get_var("foo"), None); assert_eq!(table.get_var_by_gd_name("foo"), None); assert_eq!(table.get_var_by_gd_name("bar"), None); assert_eq!(table.get_var_by_gd_name("baz"), None); assert_eq!(table.set_var("foo".to_owned(), LocalVar::read("bar".to_owned())), None); assert_eq!(table.get_var("foo"), Some(&LocalVar::read("bar".to_owned()))); assert_eq!(table.get_var_by_gd_name("foo"), None); assert_eq!(table.get_var_by_gd_name("bar"), Some(&LocalVar::read("bar".to_owned()))); assert_eq!(table.get_var_by_gd_name("baz"), None); assert_eq!(table.set_var("foo".to_owned(), LocalVar::read("baz".to_owned())), Some(LocalVar::read("bar".to_owned()))); assert_eq!(table.get_var_by_gd_name("foo"), None); assert_eq!(table.get_var_by_gd_name("bar"), None); assert_eq!(table.get_var_by_gd_name("baz"), Some(&LocalVar::read("baz".to_owned()))); table.del_var("foo"); assert_eq!(table.get_var("foo"), None); assert_eq!(table.get_var_by_gd_name("foo"), None); assert_eq!(table.get_var_by_gd_name("bar"), None); assert_eq!(table.get_var_by_gd_name("baz"), None); } #[test] fn test_iter_vars() { let mut table = SymbolTable::new(); table.set_var("foo".to_owned(), LocalVar::read("bar".to_owned())); table.set_var("foo1".to_owned(), LocalVar::rw("baz".to_owned())); table.set_var("foo2".to_owned(), LocalVar::read("abcdef".to_owned())); let mut vec: Vec<_> = table.vars().map(|x| (x.0, from_var_name(&x.1.name))).collect(); vec.sort_unstable(); assert_eq!(vec, vec!(("foo", "bar"), ("foo1", "baz"), ("foo2", "abcdef"))); } fn sample_fn() -> FnCall { FnCall::file_constant(FnSpecs::new(1, 0, None), FnScope::Global, "foobar".to_owned()) } #[test] fn test_fns() { let mut table = SymbolTable::new(); assert_eq!(table.get_fn("foo").map(|x| x.0), None); table.set_fn("foo".to_owned(), sample_fn(), CallMagic::DefaultCall); assert_eq!(table.get_fn("foo").map(|x| x.0), Some(&sample_fn())); assert_eq!(table.get_fn_by_gd_name("foo").map(|x| x.0), None); assert_eq!(table.get_fn_by_gd_name("foobar").map(|x| x.0), Some(&sample_fn())); table.set_fn("foo".to_owned(), sample_fn(), CallMagic::DefaultCall); table.del_fn("foo"); assert_eq!(table.get_fn("foo").map(|x| x.0), None); assert_eq!(table.get_fn_by_gd_name("foo").map(|x| x.0), None); assert_eq!(table.get_fn_by_gd_name("foobar").map(|x| x.0), None); } #[test] fn test_iter_fns() { let mut table = SymbolTable::new(); table.set_fn("foo".to_owned(), sample_fn(), CallMagic::DefaultCall); table.set_fn("foo1".to_owned(), sample_fn(), CallMagic::DefaultCall); table.set_fn("foo2".to_owned(), sample_fn(), CallMagic::DefaultCall); let mut vec: Vec<_> = table.fns().map(|(x, y, _)| (x, y)).collect(); vec.sort_unstable_by(|a, b| a.0.cmp(b.0)); let tmp = sample_fn(); assert_eq!(vec, vec!(("foo", &tmp), ("foo1", &tmp), ("foo2", &tmp))); } #[test] fn test_synthetic_vars() { let mut table = SymbolTable::new(); assert_eq!(table.has_var_with_gd_name("foo"), false); table.add_synthetic_var(String::from("foo"), false); assert_eq!(table.has_var_with_gd_name("foo"), true); table.remove_synthetic_var("foo"); assert_eq!(table.has_var_with_gd_name("foo"), false); } #[test] fn test_synthetic_fns() { let mut table = SymbolTable::new(); assert_eq!(table.has_fn_with_gd_name("foo"), false); table.add_synthetic_fn(String::from("foo")); assert_eq!(table.has_fn_with_gd_name("foo"), true); table.remove_synthetic_fn("foo"); assert_eq!(table.has_fn_with_gd_name("foo"), false); } } ================================================ FILE: src/gdscript/arglist.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDScript argument lists. //! //! The type [`ArgList`] defines a list of arguments as GDScript sees //! them. //! //! See [`crate::ir::arglist`] for the companion module used during IR //! analysis. use super::expr::Expr; use std::convert::AsRef; use std::mem::swap; /// A list of arguments in a function declaration. /// /// Currently, GDLisp only compiles to required GDScript arguments and /// does not provide support for optional arguments on the GDScript /// side. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArgList { /// The required arguments. required_args: Vec, /// The optional arguments. optional_args: Vec<(String, Expr)>, } impl ArgList { /// An empty argument list. pub fn empty() -> ArgList { ArgList { required_args: Vec::new(), optional_args: Vec::new() } } /// A list of required arguments. pub fn required(args: Vec) -> ArgList { ArgList { required_args: args, optional_args: Vec::new() } } /// Appends the given values to the current argument list as /// optional arguments. Takes ownership but returns `self`. /// /// This method is intended to be used in a builder style, like so. /// /// ``` /// # use gdlisp::gdscript::arglist::ArgList; /// # use gdlisp::gdscript::expr::Expr; /// # use gdlisp::pipeline::source::SourceOffset; /// # let required_vec = vec!(String::from("a"), String::from("b")); /// # let optional_vec = vec!((String::from("c"), Expr::null(SourceOffset(0)))); /// let args = ArgList::required(required_vec).optional(optional_vec); /// # let all_arg_names: Vec<&str> = args.all_args_iter().collect(); /// # assert_eq!(all_arg_names, vec!("a", "b", "c")); /// ``` pub fn optional(mut self, args: impl IntoIterator) -> Self { self.optional_args.extend(args); self } /// Whether the argument list is empty. pub fn is_empty(&self) -> bool { self.required_args.is_empty() && self.optional_args.is_empty() } /// Insert required arguments at the beginning of this argument /// list. pub fn prepend_required(&mut self, required: impl Iterator) { let mut new_vec: Vec<_> = required.collect(); swap(&mut new_vec, &mut self.required_args); self.required_args.extend(new_vec); } /// Converts all required arguments into optional arguments, where /// the default value is the given value. /// /// The default value will be cloned once for each argument. pub fn optionalize_all(&mut self, default_value: &Expr) { let required_args = self.required_args.drain(..).map(|arg| (arg, default_value.to_owned())); self.optional_args.splice(0..0, required_args); } /// All of the arguments, required and optional alike, as an /// iterator. pub fn all_args_iter(&self) -> impl Iterator { self.required_args.iter().map(String::as_ref) .chain(self.optional_args.iter().map(|x| x.0.as_ref())) } /// Returns the list of arguments in GDScript form, i.e. as a /// comma-separated list of arguments. pub fn to_gd(&self) -> String { let required = self.required_args.iter().map(String::from); let optional = self.optional_args.iter().map(|(name, default)| { format!("{} = {}", name, default.to_gd()) }); let mut all_args = Vec::new(); all_args.extend(required); all_args.extend(optional); all_args.join(", ") } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; fn null() -> Expr { Expr::null(SourceOffset(0)) } #[test] fn empty_arglist_to_gd_test() { assert_eq!(ArgList::empty().to_gd(), ""); } #[test] fn required_arglist_to_gd_test() { assert_eq!(ArgList::required(vec!()).to_gd(), ""); assert_eq!(ArgList::required(vec!(String::from("foo"))).to_gd(), "foo"); assert_eq!(ArgList::required(vec!(String::from("foo"), String::from("bar"))).to_gd(), "foo, bar"); } #[test] fn optional_arglist_to_gd_test() { assert_eq!(ArgList::empty().optional(vec!((String::from("foo"), null()))).to_gd(), "foo = null"); assert_eq!(ArgList::empty().optional(vec!((String::from("foo"), null()), (String::from("bar"), null()))).to_gd(), "foo = null, bar = null"); } #[test] fn full_arglist_to_gd_test() { assert_eq!(ArgList::required(vec!(String::from("foo"), String::from("bar"))) .optional(vec!((String::from("baz"), null()))) .to_gd(), "foo, bar, baz = null"); } #[test] fn optional_arglist_custom_expr_to_gd_test() { let expr = Expr::from_value(10, SourceOffset(0)); assert_eq!(ArgList::empty().optional(vec!((String::from("foo"), expr))).to_gd(), "foo = 10"); } #[test] fn prepend_required_test() { let mut args = ArgList::required(vec!(String::from("c"), String::from("d"))); args.prepend_required(vec!(String::from("a"), String::from("b")).into_iter()); assert_eq!(args.to_gd(), "a, b, c, d"); } #[test] fn all_args_iter_test() { let args = ArgList::required(vec!(String::from("foo"), String::from("bar"))) .optional(vec!((String::from("baz"), null()))); let names: Vec<_> = args.all_args_iter().collect(); assert_eq!(names, vec!("foo", "bar", "baz")); } } ================================================ FILE: src/gdscript/class_extends.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`ClassExtends`] type, for qualified names valid in //! an `extends` clause. use super::literal::Literal; /// A descriptor of what class is being extended. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ClassExtends { /// A qualified name, separating the final identifier from the /// left-hand side by a dot. Qualified(Box, String), /// A simple, unquoted identifier. SimpleIdentifier(String), /// A string literal, referencing a known file name. StringLit(String), } impl ClassExtends { /// Qualifies the given `extends` name with an additional /// identifier. pub fn attribute(self, attr: impl Into) -> ClassExtends { ClassExtends::Qualified(Box::new(self), attr.into()) } /// Convert `self` to a string suitable as the tail end of a /// `extends` clause in GDScript. pub fn to_gd(&self) -> String { match self { ClassExtends::Qualified(lhs, rhs) => format!("{}.{}", lhs.to_gd(), rhs), ClassExtends::SimpleIdentifier(string) => string.to_owned(), ClassExtends::StringLit(string) => Literal::String(string.to_owned()).to_gd(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_simple_identifier() { let cls = ClassExtends::SimpleIdentifier(String::from("foobar")); assert_eq!(cls.to_gd(), "foobar"); } #[test] fn test_string_literal() { let cls = ClassExtends::StringLit(String::from("abc")); assert_eq!(cls.to_gd(), "\"abc\""); } #[test] fn test_string_literal_escaped() { let cls = ClassExtends::StringLit(String::from(r#"abc"\def"#)); assert_eq!(cls.to_gd(), r#""abc\"\\def""#); } #[test] fn test_qualified_identifier() { let cls = ClassExtends::SimpleIdentifier(String::from("foobar")).attribute("baz").attribute("xyz"); assert_eq!(cls.to_gd(), "foobar.baz.xyz"); } #[test] fn test_qualified_path() { let cls = ClassExtends::StringLit(String::from("foobar")).attribute("baz").attribute("xyz"); assert_eq!(cls.to_gd(), "\"foobar\".baz.xyz"); } } ================================================ FILE: src/gdscript/decl.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDScript declarations. //! //! This module defines a [datatype](Decl) for representing //! declarations in the GDScript language, as well as //! [`Decl::write_gd`] for writing declarations as GDScript syntax to //! a [`fmt::Write`] instance. use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::gdscript::indent; use crate::gdscript::library; use crate::gdscript::arglist::ArgList; use crate::pipeline::source::{SourceOffset, Sourced}; use super::spacing::SpacedDeclPrinter; use super::class_extends::ClassExtends; use std::fmt::{self, Write}; /// The type of GDScript declarations. #[derive(Clone, Debug, PartialEq, Eq)] pub enum DeclF { VarDecl(VarDecl), ConstDecl(String, Expr), ClassDecl(ClassDecl), InitFnDecl(InitFnDecl), FnDecl(Static, FnDecl), EnumDecl(EnumDecl), SignalDecl(String, ArgList), PassDecl, } /// GDScript declaration with its source offset. See [`Sourced`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Decl { pub value: DeclF, pub pos: SourceOffset, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ClassDecl { pub name: String, pub extends: ClassExtends, pub body: Vec, } /// The top-level class is a special kind of class, similar to a /// [`ClassDecl`]. /// /// The top-level class, however, is not required to have a name and /// will print using subtly different syntax. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TopLevelClass { pub name: Option, // The top-level class is not required to have a name. pub extends: ClassExtends, pub body: Vec, } /// A variable declaration in a GDScript class. This also includes /// variable declarations at the top-level (i.e. in the implied "main" /// class of the file) but does *not* include constant declarations. /// For the latter, [`ConstDecl`](DeclF::ConstDecl) should be used. #[derive(Clone, Debug, PartialEq, Eq)] pub struct VarDecl { pub export: Option, pub onready: Onready, pub name: String, pub value: Option, pub setget: Setget, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FnDecl { pub name: String, pub args: ArgList, pub body: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct InitFnDecl { pub args: ArgList, pub super_call: Vec, pub body: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct EnumDecl { pub name: Option, pub clauses: Vec<(String, Option)>, } /// A GDScript export declaration attached to a variable. /// /// Not all expressions are valid in an export declaration, so the /// user should take care to use this type only with valid /// expressions. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Export { pub args: Vec, } /// A Boolean-isomorphic type which indicates whether or not a /// function is static. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Static { NonStatic, IsStatic, } /// A Boolean-isomorphic type which indicates whether or not a /// variable has the `onready` modifier. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Onready { No, Yes, } /// A setter declaration and a getter declaration. Either part may be /// omitted. #[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] pub struct Setget { pub setter: Option, pub getter: Option, } fn empty_class_body(pos: SourceOffset) -> Decl { Decl::new( DeclF::InitFnDecl(InitFnDecl { args: ArgList::empty(), super_call: vec!(), body: vec!(), }), pos ) } impl Decl { /// A new `Decl` with its source offset. pub fn new(value: DeclF, pos: SourceOffset) -> Decl { Decl { value, pos } } /// The name of the identifier referenced by this declaration, if /// one exists. Some declarations, such as enums, may not always /// have names. pub fn name(&self) -> Option<&str> { match &self.value { DeclF::VarDecl(vdecl) => Some(&vdecl.name), DeclF::ConstDecl(n, _) => Some(n), DeclF::ClassDecl(cdecl) => Some(&cdecl.name), DeclF::InitFnDecl(_) => Some(library::CONSTRUCTOR_NAME), DeclF::FnDecl(_, fdecl) => Some(&fdecl.name), DeclF::EnumDecl(edecl) => edecl.name.as_ref().map(|x| x.as_ref()), DeclF::SignalDecl(n, _) => Some(n), DeclF::PassDecl => None, } } /// Write the declaration, as GDScript code, to the [`fmt::Write`] /// instance `w`. /// /// We are assumed to be at the indentation level `ind`, so that all /// lines in the result will be indented to that level. /// /// The writer `w` should currently be either empty or immediately /// after a newline. The declaration will always end by printing a /// newline, making it suitable for writing a subsequent declaration /// immediately after. pub fn write_gd(&self, w: &mut W, ind: u32) -> Result<(), fmt::Error> { indent(w, ind)?; match &self.value { DeclF::VarDecl(VarDecl { export, onready, name, value, setget }) => { if let Some(export) = export { if export.args.is_empty() { write!(w, "export ")?; } else { write!(w, "export({}) ", export.args.iter().map(|x| x.to_gd()).collect::>().join(", "))?; } } if bool::from(*onready) { write!(w, "onready ")?; } write!(w, "var {}", name)?; if let Some(value) = value { write!(w, " = {}", value.to_gd())?; } let setget_str = setget.to_gd(); if !setget_str.is_empty() { write!(w, " {}", setget_str)?; } writeln!(w) } DeclF::ConstDecl(name, value) => { writeln!(w, "const {} = {}", name, value.to_gd()) } DeclF::ClassDecl(ClassDecl { name, extends, body }) => { write!(w, "class {} extends {}:\n\n", name, extends.to_gd())?; if body.is_empty() { empty_class_body(self.pos).write_gd(w, ind + 4) } else { let printer = SpacedDeclPrinter::new().with_spacing(1).with_indentation(ind + 4); printer.write_gd(w, body.iter()) } } DeclF::FnDecl(stat, FnDecl { name, args, body }) => { if *stat == Static::IsStatic { write!(w, "static ")?; } writeln!(w, "func {}({}):", name, args.to_gd())?; Stmt::write_gd_stmts(body, w, ind + 4) } DeclF::InitFnDecl(InitFnDecl { args, super_call, body }) => { let super_args: Vec<_> = super_call.iter().map(Expr::to_gd).collect(); if super_args.is_empty() { writeln!(w, "func _init({}):", args.to_gd())?; } else { writeln!(w, "func _init({}).({}):", args.to_gd(), super_args.join(", "))?; } Stmt::write_gd_stmts(body, w, ind + 4) } DeclF::SignalDecl(name, args) => { if args.is_empty() { writeln!(w, "signal {}", name) } else { writeln!(w, "signal {}({})", name, args.to_gd()) } } DeclF::EnumDecl(EnumDecl { name, clauses }) => { write!(w, "enum ")?; if let Some(name) = name { write!(w, "{} ", name)?; } writeln!(w, "{{")?; for (const_name, const_value) in clauses { indent(w, ind + 4)?; write!(w, "{}", const_name)?; if let Some(const_value) = const_value { write!(w, " = {}", const_value.to_gd())?; } writeln!(w, ",")?; } indent(w, ind)?; writeln!(w, "}}") } DeclF::PassDecl => { writeln!(w, "pass") } } } /// Write the declaration to a string, using [`Decl::write_gd`]. /// /// # Panics /// /// This function panics if there is a write error to the string. If /// you wish to handle that case yourself, use [`Stmt::write_gd`] /// explicitly. pub fn to_gd(&self, ind: u32) -> String { let mut string = String::new(); self.write_gd(&mut string, ind).expect("Could not write to string in Decl::to_gd"); string } } impl Sourced for Decl { type Item = DeclF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &DeclF { &self.value } } impl TopLevelClass { /// Convert `self` to a string suitable for writing to a `.gd` file. pub fn to_gd(&self) -> String { let mut string = String::new(); if let Some(name) = &self.name { writeln!(string, "class_name {}", name).expect("Could not write to string in TopLevelClass::to_gd"); } writeln!(string, "extends {}", self.extends.to_gd()).expect("Could not write to string in TopLevelClass::to_gd"); write!(string, "\n\n").expect("Could not write to string in TopLevelClass::to_gd"); if self.body.is_empty() { empty_class_body(SourceOffset(0)).write_gd(&mut string, 0) .expect("Could not write to string in TopLevelClass::to_gd"); } else { let printer = SpacedDeclPrinter::new(); printer.write_gd(&mut string, self.body.iter()) .expect("Could not write to string in TopLevelClass::to_gd"); } string } } impl VarDecl { /// A new variable declaration with the given name and optional /// initializer. The resulting variable will have no `export` /// declaration, will not be declared `onready`, and will have no /// `setget` modifiers. pub fn new(name: String, value: Option) -> VarDecl { VarDecl { export: None, onready: Onready::No, name: name, value: value, setget: Setget::default(), } } /// A variable declaration without an initializer. Equivalent to /// `VarDecl::new(name, None)`. pub fn simple(name: String) -> VarDecl { VarDecl::new(name, None) } /// Adds or removes an export clause from the variable declaration. pub fn with_export(mut self, export: Option) -> VarDecl { self.export = export; self } /// Adds the `onready` flag to the variable declaration. pub fn onready(mut self) -> VarDecl { self.onready = Onready::Yes; self } /// Changes the `setget` modifier to the given value. pub fn setget(mut self, setget: Setget) -> VarDecl { self.setget = setget; self } } impl Setget { /// Convert `self` to a string suitable for suffixing onto a /// variable declaration. If all fields are `None`, this returns the /// empty string. pub fn to_gd(&self) -> String { match (&self.setter, &self.getter) { (None, None) => String::from(""), (Some(s), None) => format!("setget {}", s), (None, Some(g)) => format!("setget ,{}", g), // The leading comma is an intentional part of the GDScript syntax (Some(s), Some(g)) => format!("setget {}, {}", s, g), } } } impl From for bool { fn from(s: Static) -> bool { s == Static::IsStatic } } impl From for bool { fn from(r: Onready) -> bool { r == Onready::Yes } } impl From for Static { fn from(b: bool) -> Static { if b { Static::IsStatic } else { Static::NonStatic } } } impl From for Onready { fn from(b: bool) -> Onready { if b { Onready::Yes } else { Onready::No } } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::expr::{Expr, ExprF}; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn d(decl: DeclF) -> Decl { Decl::new(decl, SourceOffset::default()) } // Helper function to match the "old" VarDecl interface, just for // convenience purposes in testing. fn var(export: Option, onready: Onready, name: String, value: Option, setget: Setget) -> VarDecl { VarDecl { export, onready, name, value, setget } } #[test] fn var_and_const() { let expr = e(ExprF::from(10)); assert_eq!(d(DeclF::VarDecl(var(None, Onready::No, String::from("foo"), None, Setget::default()))).to_gd(0), "var foo\n"); assert_eq!(d(DeclF::VarDecl(var(None, Onready::No, String::from("foo"), Some(expr.clone()), Setget::default()))).to_gd(0), "var foo = 10\n"); assert_eq!(d(DeclF::VarDecl(var(None, Onready::Yes, String::from("foo"), None, Setget::default()))).to_gd(0), "onready var foo\n"); assert_eq!(d(DeclF::VarDecl(var(None, Onready::Yes, String::from("foo"), Some(expr.clone()), Setget::default()))).to_gd(0), "onready var foo = 10\n"); assert_eq!(d(DeclF::ConstDecl(String::from("FOO"), expr.clone())).to_gd(0), "const FOO = 10\n"); } #[test] fn exported_var() { let expr = e(ExprF::from(10)); let export1 = Export { args: vec!(Expr::var("int", SourceOffset::default())) }; assert_eq!(d(DeclF::VarDecl(var(Some(export1), Onready::No, String::from("foo"), None, Setget::default()))).to_gd(0), "export(int) var foo\n"); let export2 = Export { args: vec!() }; assert_eq!(d(DeclF::VarDecl(var(Some(export2), Onready::No, String::from("foo"), None, Setget::default()))).to_gd(0), "export var foo\n"); let export3 = Export { args: vec!() }; assert_eq!(d(DeclF::VarDecl(var(Some(export3), Onready::Yes, String::from("foo"), None, Setget::default()))).to_gd(0), "export onready var foo\n"); let export4 = Export { args: vec!(Expr::var("int", SourceOffset::default()), Expr::from_value(1, SourceOffset::default()), Expr::from_value(10, SourceOffset::default())) }; assert_eq!(d(DeclF::VarDecl(var(Some(export4), Onready::No, String::from("foo"), Some(expr.clone()), Setget::default()))).to_gd(0), "export(int, 1, 10) var foo = 10\n"); } #[test] fn setget_var() { let setget1 = Setget { setter: Some(String::from("setter_name")), getter: None, }; assert_eq!(d(DeclF::VarDecl(var(None, Onready::No, String::from("foo"), None, setget1))).to_gd(0), "var foo setget setter_name\n"); let setget2 = Setget { setter: None, getter: Some(String::from("getter_name")), }; assert_eq!(d(DeclF::VarDecl(var(None, Onready::No, String::from("foo"), None, setget2))).to_gd(0), "var foo setget ,getter_name\n"); let setget3 = Setget { setter: Some(String::from("setter_name")), getter: Some(String::from("getter_name")), }; assert_eq!(d(DeclF::VarDecl(var(None, Onready::No, String::from("foo"), None, setget3))).to_gd(0), "var foo setget setter_name, getter_name\n"); } #[test] fn signal() { assert_eq!(d(DeclF::SignalDecl(String::from("signal_name"), ArgList::empty())).to_gd(0), "signal signal_name\n"); assert_eq!(d(DeclF::SignalDecl(String::from("signal_name"), ArgList::required(vec!(String::from("a"), String::from("b"))))).to_gd(0), "signal signal_name(a, b)\n"); } #[test] fn functions() { let decl1 = d(DeclF::FnDecl(Static::NonStatic, FnDecl { name: String::from("foobar"), args: ArgList::required(vec!()), body: vec!() })); assert_eq!(decl1.to_gd(0), "func foobar():\n pass\n"); let decl2 = d(DeclF::FnDecl(Static::IsStatic, FnDecl { name: String::from("foobar"), args: ArgList::required(vec!()), body: vec!() })); assert_eq!(decl2.to_gd(0), "static func foobar():\n pass\n"); let decl3 = d(DeclF::FnDecl(Static::NonStatic, FnDecl { name: String::from("foobar"), args: ArgList::required(vec!(String::from("arg1"))), body: vec!() })); assert_eq!(decl3.to_gd(0), "func foobar(arg1):\n pass\n"); let decl4 = d(DeclF::FnDecl(Static::NonStatic, FnDecl { name: String::from("foobar"), args: ArgList::required(vec!(String::from("arg1"), String::from("arg2"))), body: vec!() })); assert_eq!(decl4.to_gd(0), "func foobar(arg1, arg2):\n pass\n"); let decl5 = d(DeclF::FnDecl(Static::NonStatic, FnDecl { name: String::from("foobar"), args: ArgList::required(vec!(String::from("arg1"), String::from("arg2"))), body: vec!(Stmt::expr(e(ExprF::Var(String::from("function_body"))))) })); assert_eq!(decl5.to_gd(0), "func foobar(arg1, arg2):\n function_body\n"); } #[test] fn init_functions() { let decl1 = d(DeclF::InitFnDecl(InitFnDecl { args: ArgList::required(vec!()), super_call: vec!(), body: vec!() })); assert_eq!(decl1.to_gd(0), "func _init():\n pass\n"); let decl2 = d(DeclF::InitFnDecl(InitFnDecl { args: ArgList::required(vec!(String::from("arg1"))), super_call: vec!(e(ExprF::Var(String::from("arg1"))), e(ExprF::from(8))), body: vec!() })); assert_eq!(decl2.to_gd(0), "func _init(arg1).(arg1, 8):\n pass\n"); let decl3 = d(DeclF::InitFnDecl(InitFnDecl { args: ArgList::required(vec!(String::from("arg1"), String::from("arg2"))), super_call: vec!(e(ExprF::Var(String::from("arg1"))), e(ExprF::from(8))), body: vec!() })); assert_eq!(decl3.to_gd(0), "func _init(arg1, arg2).(arg1, 8):\n pass\n"); let decl4 = d(DeclF::InitFnDecl(InitFnDecl { args: ArgList::required(vec!(String::from("arg1"), String::from("arg2"))), super_call: vec!(e(ExprF::Var(String::from("arg1"))), e(ExprF::from(8))), body: vec!(Stmt::expr(e(ExprF::Var(String::from("function_body"))))) })); assert_eq!(decl4.to_gd(0), "func _init(arg1, arg2).(arg1, 8):\n function_body\n"); } #[test] fn classes() { let sample_function = d(DeclF::FnDecl(Static::NonStatic, FnDecl { name: String::from("sample"), args: ArgList::required(vec!()), body: vec!() })); let decl1 = d(DeclF::ClassDecl(ClassDecl { name: String::from("MyClass"), extends: ClassExtends::SimpleIdentifier(String::from("ParentClass")), body: vec!(), })); assert_eq!(decl1.to_gd(0), "class MyClass extends ParentClass:\n\n func _init():\n pass\n"); let decl2 = d(DeclF::ClassDecl(ClassDecl { name: String::from("MyClass"), extends: ClassExtends::SimpleIdentifier(String::from("ParentClass")), body: vec!( d(DeclF::VarDecl(var(None, Onready::No, String::from("variable"), None, Setget::default()))), ), })); assert_eq!(decl2.to_gd(0), r#"class MyClass extends ParentClass: var variable "#); let decl3 = d(DeclF::ClassDecl(ClassDecl { name: String::from("MyClass"), extends: ClassExtends::SimpleIdentifier(String::from("ParentClass")), body: vec!( d(DeclF::VarDecl(var(None, Onready::No, String::from("variable"), None, Setget::default()))), sample_function.clone(), ), })); assert_eq!(decl3.to_gd(0), r#"class MyClass extends ParentClass: var variable func sample(): pass "#); let decl4 = d(DeclF::ClassDecl(ClassDecl { name: String::from("MyClass"), extends: ClassExtends::SimpleIdentifier(String::from("ParentClass")), body: vec!( d(DeclF::VarDecl(var(None, Onready::Yes, String::from("variable"), None, Setget::default()))), sample_function.clone(), ), })); assert_eq!(decl4.to_gd(0), r#"class MyClass extends ParentClass: onready var variable func sample(): pass "#); } #[test] fn enums() { let decl1 = d(DeclF::EnumDecl(EnumDecl { name: None, clauses: vec!(), })); assert_eq!(decl1.to_gd(0), "enum {\n}\n"); let decl2 = d(DeclF::EnumDecl(EnumDecl { name: None, clauses: vec!((String::from("Value1"), None), (String::from("Value2"), None)), })); assert_eq!(decl2.to_gd(0), "enum {\n Value1,\n Value2,\n}\n"); let decl3 = d(DeclF::EnumDecl(EnumDecl { name: None, clauses: vec!((String::from("Value1"), Some(e(ExprF::from(99)))), (String::from("Value2"), None)), })); assert_eq!(decl3.to_gd(0), "enum {\n Value1 = 99,\n Value2,\n}\n"); let decl4 = d(DeclF::EnumDecl(EnumDecl { name: Some(String::from("EnumName")), clauses: vec!((String::from("Value1"), Some(e(ExprF::from(99)))), (String::from("Value2"), None)), })); assert_eq!(decl4.to_gd(0), "enum EnumName {\n Value1 = 99,\n Value2,\n}\n"); } #[test] fn decl_names() { let decl1 = d(DeclF::VarDecl(var(None, Onready::No, String::from("abc"), None, Setget::default()))); assert_eq!(decl1.name(), Some("abc")); let decl2 = d(DeclF::ConstDecl(String::from("MY_CONST"), e(ExprF::from(10)))); assert_eq!(decl2.name(), Some("MY_CONST")); let decl3 = d(DeclF::ClassDecl(ClassDecl { name: String::from("MyClass"), extends: ClassExtends::SimpleIdentifier(String::from("Node")), body: vec!() })); assert_eq!(decl3.name(), Some("MyClass")); let decl4 = d(DeclF::InitFnDecl(InitFnDecl { args: ArgList::empty(), super_call: vec!(), body: vec!() })); assert_eq!(decl4.name(), Some("_init")); let decl5 = d(DeclF::FnDecl(Static::NonStatic, FnDecl { name: String::from("foobar"), args: ArgList::empty(), body: vec!() })); assert_eq!(decl5.name(), Some("foobar")); let decl6 = d(DeclF::EnumDecl(EnumDecl { name: Some(String::from("MyEnum")), clauses: vec!() })); assert_eq!(decl6.name(), Some("MyEnum")); let decl7 = d(DeclF::EnumDecl(EnumDecl { name: None, clauses: vec!() })); assert_eq!(decl7.name(), None); let decl8 = d(DeclF::SignalDecl(String::from("signal_emitted"), ArgList::empty())); assert_eq!(decl8.name(), Some("signal_emitted")); let decl9 = d(DeclF::PassDecl); assert_eq!(decl9.name(), None); } } ================================================ FILE: src/gdscript/expr.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDScript expressions. //! //! This module defines a [datatype](Expr) for representing //! expressions in the GDScript language, as well as [`Expr::to_gd`] //! for converting to GDScript syntax. //! //! Note: For names (such as strings), we expect that they've already //! been sanitized for GDScript output. That should've happened //! earlier in the compilation process. This precondition is not //! checked anywhere in this module. use crate::gdscript::op::{self, UnaryOp, BinaryOp, OperatorHasInfo}; use crate::gdscript::literal::Literal; use crate::pipeline::source::{SourceOffset, Sourced}; use std::fmt::Write; pub const PRECEDENCE_LOWEST: i32 = -99; pub const PRECEDENCE_SUBSCRIPT: i32 = 21; pub const PRECEDENCE_ATTRIBUTE: i32 = 20; pub const PRECEDENCE_CALL: i32 = 19; /// The type of GDScript expressions. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ExprF { Var(String), Literal(Literal), /// Subscript access, i.e. `foo[bar]`. Subscript(Box, Box), /// Attribute access, i.e. `foo.bar`. Attribute(Box, String), /// A function call, possibly qualified by a value name. If the /// first argument is `None`, then this is akin to a call of the /// form `bar(...)`. If the first argument is `Some(foo)`, then this /// is akin to `foo.bar(...)`. Call(Option>, String, Vec), /// A super call, i.e. `.bar(...)`. SuperCall(String, Vec), Unary(UnaryOp, Box), Binary(Box, BinaryOp, Box), /// A use of the ternary-if operator `foo if bar else baz`. TernaryIf(TernaryIf), ArrayLit(Vec), DictionaryLit(Vec<(Expr, Expr)>), } /// GDScript expression with its source offset. See [`Sourced`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Expr { pub value: ExprF, pub pos: SourceOffset, } /// The type used by [`ExprF::TernaryIf`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TernaryIf { pub true_case: Box, pub cond: Box, pub false_case: Box, } fn maybe_parens(cond: bool, inner: String) -> String { if cond { format!("({})", inner) } else { inner } } impl Expr { /// A new `Expr` with the given [`SourceOffset`]. pub fn new(value: ExprF, pos: SourceOffset) -> Expr { Expr { value, pos } } /// The literal expression `null`. pub fn null(pos: SourceOffset) -> Expr { Expr::new(ExprF::Literal(Literal::Null), pos) } /// The expression referring to the special "self" variable. pub fn self_var(pos: SourceOffset) -> Expr { Expr::new(ExprF::Var(String::from("self")), pos) } /// A literal string. pub fn str_lit(a: &str, pos: SourceOffset) -> Expr { Expr::new(ExprF::from(a.to_owned()), pos) } /// An [`ExprF::Var`], referenced by name. The name will be cloned /// into the resulting value. pub fn var(a: &str, pos: SourceOffset) -> Expr { Expr::new(ExprF::Var(a.to_owned()), pos) } /// An [`ExprF::Attribute`] on `self`, referencing the name given by /// `attr`. pub fn attribute(self, attr: impl Into, pos: SourceOffset) -> Expr { Expr::new(ExprF::Attribute(Box::new(self), attr.into()), pos) } /// An [`ExprF::Subscript`] on `self`, subscripted by `rhs`. pub fn subscript(self, rhs: Expr, pos: SourceOffset) -> Expr { Expr::new(ExprF::Subscript(Box::new(self), Box::new(rhs)), pos) } /// A unary operator application. pub fn unary(self, op: UnaryOp, pos: SourceOffset) -> Expr { Expr::new(ExprF::Unary(op, Box::new(self)), pos) } /// Binary operator application. pub fn binary(self, op: BinaryOp, rhs: Expr, pos: SourceOffset) -> Expr { Expr::new(ExprF::Binary(Box::new(self), op, Box::new(rhs)), pos) } /// A function call expression. pub fn call(lhs: Option, name: &str, args: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::Call(lhs.map(Box::new), name.to_owned(), args), pos) } /// A function call expression, with no receiver object. Equivalent /// to `Expr::call(None, name, args)`. pub fn simple_call(name: &str, args: Vec, pos: SourceOffset) -> Expr { Expr::call(None, name, args, pos) } /// A super-method call expression. pub fn super_call(name: &str, args: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::SuperCall(name.to_owned(), args), pos) } /// A GDScript `yield` call. /// /// `yield` takes either zero or two arguments, so this function can /// produce either form of `yield` (by passing either `None` or /// `Some(a, b)`). pub fn yield_expr(args: Option<(Expr, Expr)>, pos: SourceOffset) -> Expr { let args = match args { None => vec!(), Some((x, y)) => vec!(x, y), }; Expr::simple_call("yield", args, pos) } /// A GDScript `assert` call. /// /// `assert` takes either one or two arguments. pub fn assert_expr(cond: Expr, message: Option, pos: SourceOffset) -> Expr { let args = match message { None => vec!(cond), Some(message) => vec!(cond, message), }; Expr::simple_call("assert", args, pos) } /// A GDScript `preload` call. pub fn preload_expr(arg: String, pos: SourceOffset) -> Expr { Expr::simple_call("preload", vec!(Expr::from_value(arg, pos)), pos) } /// Uses a [`From`] instance of [`ExprF`] to construct an `Expr`. pub fn from_value(value: T, pos: SourceOffset) -> Expr where ExprF : From { Expr::new(ExprF::from(value), pos) } /// Convert to a GDScript string, assuming the ambient precedence is /// a specific value. /// /// Generally, callers will want to invoke [`Expr::to_gd`] and let the /// expression manage its own precedence. pub fn to_gd_prec(&self, prec: i32) -> String { match &self.value { ExprF::Var(s) => s.clone(), ExprF::Literal(lit) => lit.to_gd(), ExprF::Subscript(lhs, index) => format!("{}[{}]", lhs.to_gd_prec(PRECEDENCE_SUBSCRIPT), index.to_gd_prec(PRECEDENCE_LOWEST)), ExprF::Attribute(lhs, name) => format!("{}.{}", lhs.to_gd_prec(PRECEDENCE_ATTRIBUTE), name), ExprF::Call(class, name, args) => { let prefix = if let Some(class) = class { format!("{}.", class.to_gd_prec(PRECEDENCE_CALL)) } else { String::from("") }; let arglist = args.iter().map(|arg| arg.to_gd_prec(PRECEDENCE_LOWEST)).collect::>().join(", "); format!("{}{}({})", prefix, name, arglist) }, ExprF::SuperCall(name, args) => { let arglist = args.iter().map(|arg| arg.to_gd_prec(PRECEDENCE_LOWEST)).collect::>().join(", "); format!(".{}({})", name, arglist) }, ExprF::Unary(op, arg) => { let info = op.op_info(); let arg = arg.to_gd_prec(info.precedence); let inner = if info.padding == op::Padding::NotRequired { format!("{}{}", info.name, arg) } else { format!("{} {}", info.name, arg) }; maybe_parens(prec > info.precedence, inner) }, ExprF::Binary(lhs, op, rhs) => { // Implicit assumption that all operators are left-associative. let info = op.op_info(); let lhs = lhs.to_gd_prec(info.precedence); let rhs = rhs.to_gd_prec(info.precedence + 1); let inner = if info.padding == op::Padding::NotRequired { format!("{}{}{}", lhs, info.name, rhs) } else { format!("{} {} {}", lhs, info.name, rhs) }; maybe_parens(prec > info.precedence, inner) }, ExprF::TernaryIf(TernaryIf { true_case, cond, false_case }) => { // Ternary is right-associative. let info = op::TernaryOp.op_info(); let lhs = true_case.to_gd_prec(info.precedence + 1); let cond = cond.to_gd_prec(PRECEDENCE_LOWEST); let rhs = false_case.to_gd_prec(info.precedence); let inner = format!("{} if {} else {}", lhs, cond, rhs); maybe_parens(prec > info.precedence, inner) }, ExprF::ArrayLit(vec) => { let mut first = true; let mut result = String::from("["); for x in vec { if !first { result.push_str(", "); } result.push_str(&x.to_gd_prec(PRECEDENCE_LOWEST)); first = false; } result.push(']'); result }, ExprF::DictionaryLit(vec) => { let mut first = true; let mut result = String::from("{"); for (k, v) in vec { if !first { result.push_str(", "); } write!(result, "{}: {}", k.to_gd_prec(PRECEDENCE_LOWEST), v.to_gd_prec(PRECEDENCE_LOWEST)) .expect("Failed to write to local string"); first = false; } result.push('}'); result }, } } /// Convert a GDScript expression to a string. The result will /// contain valid GDScript syntax. pub fn to_gd(&self) -> String { self.to_gd_prec(PRECEDENCE_LOWEST) } } impl Sourced for Expr { type Item = ExprF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &ExprF { &self.value } } impl From for ExprF where Literal : From { fn from(x: T) -> ExprF { ExprF::Literal(Literal::from(x)) } } impl From for ExprF { fn from(x: TernaryIf) -> ExprF { ExprF::TernaryIf(x) } } #[cfg(test)] mod tests { use super::*; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } #[test] fn basic_expr_types() { let var = e(ExprF::Var(String::from("foobar"))); let n = e(ExprF::from(99)); let name = String::from("attr"); let arg1 = e(ExprF::from(1)); let arg2 = e(ExprF::from(2)); let lhs = e(ExprF::Binary(Box::new(arg1), BinaryOp::Add, Box::new(arg2))); let attr = e(ExprF::Attribute(Box::new(var.clone()), name.clone())); let subs = e(ExprF::Subscript(Box::new(var.clone()), Box::new(n.clone()))); assert_eq!(var.to_gd(), "foobar"); assert_eq!(n.to_gd(), "99"); assert_eq!(e(ExprF::Subscript(Box::new(var.clone()), Box::new(n.clone()))).to_gd(), "foobar[99]"); assert_eq!(attr.to_gd(), "foobar.attr"); assert_eq!(subs.to_gd(), "foobar[99]"); assert_eq!(e(ExprF::Attribute(Box::new(attr.clone()), name.clone())).to_gd(), "foobar.attr.attr"); assert_eq!(e(ExprF::Attribute(Box::new(subs.clone()), name.clone())).to_gd(), "foobar[99].attr"); assert_eq!(e(ExprF::Subscript(Box::new(attr.clone()), Box::new(n.clone()))).to_gd(), "foobar.attr[99]"); assert_eq!(e(ExprF::Subscript(Box::new(subs.clone()), Box::new(n.clone()))).to_gd(), "foobar[99][99]"); assert_eq!(e(ExprF::Subscript(Box::new(lhs.clone()), Box::new(n.clone()))).to_gd(), "(1 + 2)[99]"); assert_eq!(e(ExprF::Attribute(Box::new(lhs.clone()), name.clone())).to_gd(), "(1 + 2).attr"); } #[test] fn call_exprs() { let lhs = e(ExprF::Var(String::from("lhs"))); let name = String::from("func"); let arg1 = e(ExprF::from(1)); let arg2 = e(ExprF::from(2)); let arg3 = e(ExprF::from(3)); let lhs_p = e(ExprF::Binary(Box::new(arg1.clone()), BinaryOp::Add, Box::new(arg2.clone()))); assert_eq!(e(ExprF::Call(None, name.clone(), vec!())).to_gd(), "func()"); assert_eq!(e(ExprF::Call(None, name.clone(), vec!(arg1.clone()))).to_gd(), "func(1)"); assert_eq!(e(ExprF::Call(None, name.clone(), vec!(arg1.clone(), arg2.clone()))).to_gd(), "func(1, 2)"); assert_eq!(e(ExprF::Call(None, name.clone(), vec!(arg1.clone(), arg2.clone(), arg3.clone()))).to_gd(), "func(1, 2, 3)"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs.clone())), name.clone(), vec!())).to_gd(), "lhs.func()"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs.clone())), name.clone(), vec!(arg1.clone()))).to_gd(), "lhs.func(1)"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs.clone())), name.clone(), vec!(arg1.clone(), arg2.clone()))).to_gd(), "lhs.func(1, 2)"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs.clone())), name.clone(), vec!(arg1.clone(), arg2.clone(), arg3.clone()))).to_gd(), "lhs.func(1, 2, 3)"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs_p.clone())), name.clone(), vec!())).to_gd(), "(1 + 2).func()"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs_p.clone())), name.clone(), vec!(arg1.clone()))).to_gd(), "(1 + 2).func(1)"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs_p.clone())), name.clone(), vec!(arg1.clone(), arg2.clone()))).to_gd(), "(1 + 2).func(1, 2)"); assert_eq!(e(ExprF::Call(Some(Box::new(lhs_p.clone())), name.clone(), vec!(arg1.clone(), arg2.clone(), arg3.clone()))).to_gd(), "(1 + 2).func(1, 2, 3)"); assert_eq!(e(ExprF::SuperCall(name.clone(), vec!())).to_gd(), ".func()"); } fn unary(op: UnaryOp, expr: &Expr) -> Expr { e(ExprF::Unary(op, Box::new(expr.clone()))) } fn binary(a: &Expr, op: BinaryOp, b: &Expr) -> Expr { e(ExprF::Binary(Box::new(a.clone()), op, Box::new(b.clone()))) } #[test] fn unary_ops() { let operand = e(ExprF::from(3)); assert_eq!(unary(UnaryOp::BitNot, &operand).to_gd(), "~3"); assert_eq!(unary(UnaryOp::Negate, &operand).to_gd(), "-3"); assert_eq!(unary(UnaryOp::Not, &operand).to_gd(), "!3"); } #[test] fn binary_ops() { let a = e(ExprF::Var(String::from("a"))); let b = e(ExprF::Var(String::from("b"))); assert_eq!(binary(&a, BinaryOp::Times, &b).to_gd(), "a * b"); assert_eq!(binary(&a, BinaryOp::Div, &b).to_gd(), "a / b"); assert_eq!(binary(&a, BinaryOp::Mod, &b).to_gd(), "a % b"); assert_eq!(binary(&a, BinaryOp::Add, &b).to_gd(), "a + b"); assert_eq!(binary(&a, BinaryOp::Sub, &b).to_gd(), "a - b"); assert_eq!(binary(&a, BinaryOp::LShift, &b).to_gd(), "a << b"); assert_eq!(binary(&a, BinaryOp::RShift, &b).to_gd(), "a >> b"); assert_eq!(binary(&a, BinaryOp::BitAnd, &b).to_gd(), "a & b"); assert_eq!(binary(&a, BinaryOp::BitXor, &b).to_gd(), "a ^ b"); assert_eq!(binary(&a, BinaryOp::BitOr, &b).to_gd(), "a | b"); assert_eq!(binary(&a, BinaryOp::LT, &b).to_gd(), "a < b"); assert_eq!(binary(&a, BinaryOp::GT, &b).to_gd(), "a > b"); assert_eq!(binary(&a, BinaryOp::Eq, &b).to_gd(), "a == b"); assert_eq!(binary(&a, BinaryOp::LE, &b).to_gd(), "a <= b"); assert_eq!(binary(&a, BinaryOp::GE, &b).to_gd(), "a >= b"); assert_eq!(binary(&a, BinaryOp::NE, &b).to_gd(), "a != b"); assert_eq!(binary(&a, BinaryOp::Is, &b).to_gd(), "a is b"); assert_eq!(binary(&a, BinaryOp::In, &b).to_gd(), "a in b"); assert_eq!(binary(&a, BinaryOp::And, &b).to_gd(), "a && b"); assert_eq!(binary(&a, BinaryOp::Or, &b).to_gd(), "a || b"); assert_eq!(binary(&a, BinaryOp::Cast, &b).to_gd(), "a as b"); } #[test] fn ternary_op() { let a = Box::new(e(ExprF::from(1))); let b = Box::new(e(ExprF::from(2))); let c = Box::new(e(ExprF::from(3))); assert_eq!(e(ExprF::TernaryIf(TernaryIf { true_case: a, cond: b, false_case: c })).to_gd(), "1 if 2 else 3"); } #[test] fn operator_precedence() { let a = e(ExprF::Var(String::from("a"))); let b = e(ExprF::Var(String::from("b"))); let c = e(ExprF::Var(String::from("c"))); let noparens = binary(&binary(&a, BinaryOp::Times, &b), BinaryOp::Add, &c); let needparens = binary(&binary(&a, BinaryOp::Add, &b), BinaryOp::Times, &c); assert_eq!(noparens.to_gd(), "a * b + c"); assert_eq!(needparens.to_gd(), "(a + b) * c"); } #[test] fn ternary_if_precedence_1() { let a = Box::new(e(ExprF::from(1))); let b = Box::new(e(ExprF::from(2))); let c = Box::new(e(ExprF::from(3))); let d = Box::new(e(ExprF::from(4))); let f = Box::new(e(ExprF::from(5))); let left_assoc = e(ExprF::from(TernaryIf { true_case: Box::new(e(ExprF::from(TernaryIf { true_case: a.clone(), cond: b.clone(), false_case: c.clone() }))), cond: d.clone(), false_case: f.clone(), })); let right_assoc = e(ExprF::from(TernaryIf { true_case: a.clone(), cond: b.clone(), false_case: Box::new(e(ExprF::from(TernaryIf { true_case: c.clone(), cond: d.clone(), false_case: f.clone() }))), })); assert_eq!(left_assoc.to_gd(), "(1 if 2 else 3) if 4 else 5"); assert_eq!(right_assoc.to_gd(), "1 if 2 else 3 if 4 else 5"); } #[test] fn ternary_if_precedence_2() { let a = e(ExprF::Var(String::from("a"))); let b = e(ExprF::Var(String::from("b"))); let c = e(ExprF::Var(String::from("c"))); let d = e(ExprF::Var(String::from("d"))); let case1 = e(ExprF::from(TernaryIf { true_case: Box::new(binary(&a, BinaryOp::Or, &b)), cond: Box::new(c.clone()), false_case: Box::new(d.clone()), })); assert_eq!(case1.to_gd(), "a || b if c else d"); let case2 = e(ExprF::from(TernaryIf { true_case: Box::new(a.clone()), cond: Box::new(binary(&b, BinaryOp::Or, &c)), false_case: Box::new(d.clone()), })); assert_eq!(case2.to_gd(), "a if b || c else d"); let case3 = e(ExprF::from(TernaryIf { true_case: Box::new(a.clone()), cond: Box::new(b.clone()), false_case: Box::new(binary(&c, BinaryOp::Or, &d)), })); assert_eq!(case3.to_gd(), "a if b else c || d"); let case4 = e(ExprF::from(TernaryIf { true_case: Box::new(binary(&a, BinaryOp::Cast, &b)), cond: Box::new(c.clone()), false_case: Box::new(d.clone()), })); assert_eq!(case4.to_gd(), "(a as b) if c else d"); let case5 = e(ExprF::from(TernaryIf { true_case: Box::new(a.clone()), cond: Box::new(binary(&b, BinaryOp::Cast, &c)), false_case: Box::new(d.clone()), })); assert_eq!(case5.to_gd(), "a if b as c else d"); let case6 = e(ExprF::from(TernaryIf { true_case: Box::new(a.clone()), cond: Box::new(b.clone()), false_case: Box::new(binary(&c, BinaryOp::Cast, &d)), })); assert_eq!(case6.to_gd(), "a if b else (c as d)"); } #[test] fn arrays() { assert_eq!(e(ExprF::ArrayLit(vec!())).to_gd(), "[]"); assert_eq!(e(ExprF::ArrayLit(vec!(e(ExprF::from(1))))).to_gd(), "[1]"); assert_eq!(e(ExprF::ArrayLit(vec!(e(ExprF::from(1)), e(ExprF::from(2))))).to_gd(), "[1, 2]"); assert_eq!(e(ExprF::ArrayLit(vec!(e(ExprF::from(1)), e(ExprF::from(2)), e(ExprF::from(3))))).to_gd(), "[1, 2, 3]"); } #[test] fn dictionaries() { assert_eq!(e(ExprF::DictionaryLit(vec!())).to_gd(), "{}"); assert_eq!(e(ExprF::DictionaryLit(vec!((e(ExprF::from(1)), e(ExprF::from(2)))))).to_gd(), "{1: 2}"); assert_eq!(e(ExprF::DictionaryLit(vec!((e(ExprF::from(1)), e(ExprF::from(2))), (e(ExprF::from(3)), e(ExprF::from(4)))))).to_gd(), "{1: 2, 3: 4}"); assert_eq!(e(ExprF::DictionaryLit(vec!((e(ExprF::from(1)), e(ExprF::from(2))), (e(ExprF::from(3)), e(ExprF::from(4))), (e(ExprF::from(5)), e(ExprF::from(6)))))).to_gd(), "{1: 2, 3: 4, 5: 6}"); } } ================================================ FILE: src/gdscript/expr_wrapper.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Miscellaneous helpers for wrapping commonly-used operations as //! GDScript expressions. use super::expr::{Expr, ExprF}; use super::literal::Literal; /// Call the GDScript function `int` on the expression, unless the /// expression is provably already an integer. pub fn int(expr: Expr) -> Expr { if let ExprF::Literal(Literal::Int(_)) = &expr.value { expr } else { let pos = expr.pos; Expr::simple_call("int", vec!(expr), pos) } } /// Call the GDScript function `float` on the expression, unless the /// expression is provably already a floating-point value. pub fn float(expr: Expr) -> Expr { if let ExprF::Literal(Literal::Float(_)) = &expr.value { expr } else { let pos = expr.pos; Expr::simple_call("float", vec!(expr), pos) } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; #[test] fn int_test() { assert_eq!(int(Expr::var("a", SourceOffset::default())), Expr::call(None, "int", vec!(Expr::var("a", SourceOffset::default())), SourceOffset::default())); assert_eq!(int(Expr::from_value(10, SourceOffset::default())), Expr::from_value(10, SourceOffset::default())); } #[test] fn float_test() { assert_eq!(float(Expr::var("a", SourceOffset::default())), Expr::call(None, "float", vec!(Expr::var("a", SourceOffset::default())), SourceOffset::default())); } } ================================================ FILE: src/gdscript/inner_class.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helpers for construction of GDLisp inner classes. //! //! Helpers to make sure GDLisp inner classes (and any feature of //! GDLisp that compiles to inner classes, like lambdas and lambda //! classes) can access statics from the enclosing scope. //! //! There are two key pieces of functionality in this module. The //! first is [`NeedsOuterClassRef`], a trait whose sole method //! determines whether a given type of declaration needs to retain a //! reference to the enclosing class. By default, inner classes in //! GDScript cannot access the outer class, so in the cases where we //! require this behavior, we have to go to special effort to get it. //! In the cases where we *do* need this behavior, the second core //! functionality of this module, [`add_outer_class_ref_named`] adds a //! reference to the outer class to the given scope. use super::decl::{self, Decl, DeclF, VarDecl}; use crate::pipeline::can_load::CanLoad; use crate::pipeline::source::SourceOffset; use crate::compile::symbol_table::SymbolTable; use crate::compile::symbol_table::local_var::VarName; use crate::compile::preload_resolver::PreloadResolver; use crate::ir; use crate::ir::identifier::{Id, Namespace}; use crate::util::lattice::Lattice; /// By convention, this name is used as the basis for outer class /// reference names. Note that this name should not be used *directly* /// unless you first check that there are no conflicts. Normally, this /// would be passed to /// [`generate_with`](crate::compile::names::generator::NameGenerator::generate_with). pub const OUTER_REFERENCE_NAME: &str = "__gdlisp_outer_class"; /// Add a declaration to the outer class with the given name to /// `inner_class`. /// /// When the class referenced by `inner_class` is constructed, the /// variable with name `var_name` will be initialized on the instance /// to be equal to the enclosing class resource. This resource will be /// loaded using `load`. The path to load is determined by `resolver`. pub fn add_outer_class_ref_named(inner_class: &mut decl::ClassDecl, resolver: &dyn PreloadResolver, current_file: &impl CanLoad, var_name: String, pos: SourceOffset) { let current_filename = get_current_filename(current_file, resolver) .expect("Error identifying current file"); let load_expr = VarName::load_expr(current_filename, pos); let var_decl = Decl::new(DeclF::VarDecl(VarDecl::new(var_name, Some(load_expr))), pos); inner_class.body.push(var_decl); } /// Gets the filename of the currently loading object from the /// pipeline, as per [`CanLoad::current_filename`], and then resolves /// that filename using `resolver`. Returns `None` if either step /// fails. pub fn get_current_filename(current_file: &L, resolver: &R) -> Option where L : CanLoad + ?Sized, R : PreloadResolver + ?Sized { resolver.resolve_preload(¤t_file.current_filename()) } /// Trait for objects, such as declarations, which may need an outer /// class reference. /// /// Before blindly throwing unnecessary references on every inner /// class (which, in addition to being hilariously inefficient, would /// cause significant memory leaks since GDScript resources are /// reference counted), the compiler should use this trait to check /// whether an outer reference is actually warranted. pub trait NeedsOuterClassRef { /// Given the names that are in scope at the current point in the /// code, determine whether this declaration requires an outer class /// reference. fn needs_outer_class_ref(&self, table: &SymbolTable) -> bool; } fn check_dependencies_for_outer_class_ref(deps: impl Iterator, table: &SymbolTable) -> bool { for dep in deps { // If the dependency is in the function namespace and refers to // a function defined in the top-level of the current file, then // we need an outer class reference. if dep.namespace == Namespace::Function { if let Some((func, _)) = table.get_fn(&dep.name) { // TODO Abstract this check alongside the inner static update pattern into one place if func.object.needs_inner_scope_reference() { return true; } } } } false } impl NeedsOuterClassRef for ir::decl::ClassInnerDecl { fn needs_outer_class_ref(&self, table: &SymbolTable) -> bool { if self.is_static() { // Static functions never use the outer class reference, since // the reference itself is an instance variable and is // inaccessible from static scope. return false; } check_dependencies_for_outer_class_ref(self.dependencies().into_iter().map(|(k, _)| k), table) } } impl NeedsOuterClassRef for ir::decl::ConstructorDecl { fn needs_outer_class_ref(&self, table: &SymbolTable) -> bool { check_dependencies_for_outer_class_ref(self.dependencies().into_iter().map(|(k, _)| k), table) } } impl NeedsOuterClassRef for ir::decl::ClassDecl { fn needs_outer_class_ref(&self, table: &SymbolTable) -> bool { let ir::decl::ClassDecl { visibility: _, name: _, extends: _, main_class: _, constructor, decls } = self; constructor.as_ref().map_or(false, |x| x.needs_outer_class_ref(table)) || decls.iter().any(|x| x.needs_outer_class_ref(table)) } } impl NeedsOuterClassRef for ir::expr::LambdaClass { fn needs_outer_class_ref(&self, table: &SymbolTable) -> bool { let ir::expr::LambdaClass { extends: _, args: _, constructor, decls } = self; constructor.as_ref().map_or(false, |x| x.needs_outer_class_ref(table)) || decls.iter().any(|x| x.needs_outer_class_ref(table)) } } impl NeedsOuterClassRef for ir::closure_names::ClosureNames { fn needs_outer_class_ref(&self, table: &SymbolTable) -> bool { let deps = self.names().map(|x| Id::new(Namespace::Function, x.to_owned())); check_dependencies_for_outer_class_ref(deps, table) } } ================================================ FILE: src/gdscript/library/cell.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helpers for constructing GDLisp `Cell` objects and wrapping //! variables in them. //! //! The GDLisp `Cell` class is a simple class with only one member //! variable (called `contents`), and a single 1-argument constructor. //! //! Any local variable (including function parameters) may need to be //! wrapped in a cell. A variable needs to be wrapped in a cell if its //! [access type](crate::ir::access_type::AccessType) is //! [`ClosedRW`](crate::ir::access_type::AccessType::ClosedRW). That //! is, a variable needs to be wrapped in a cell if either of the //! following is true. //! //! * We write to the variable inside of a closure that is strictly //! smaller than the scope of the variable. //! //! * We write to the variable anywhere AND we read from the variable //! inside a closure that is strictly smaller than the scope of the //! variable. //! //! For details on the motivation for these rules and specifics on how //! their calculated, see the documentation for //! [`AccessType`](crate::ir::access_type::AccessType). //! //! If we decide that a cell is warranted, then the variable will be //! wrapped in a cell at construction time. For local variables, this //! means that the variable's initializer is wrapped in a call to //! `Cell.new`. For parameters, the first few lines of the function //! will be assignments of the form `argument_name = //! Cell.new(argument_name)`. In either case, all access or //! assignments to these variables will be targeted at //! `argument_name.contents`. //! //! The only exception to this is the construction of closures. //! Closures are explicitly expected to share cells with their //! enclosing scope, so when a closure is constructed, the constructor //! arguments to that closure will be passed as cells directly. use super::on_gdlisp_root; use crate::pipeline::source::SourceOffset; use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::compile::body::builder::StmtBuilder; /// The name of the `Cell` class. pub const CELL_CLASS: &str = "Cell"; /// The name of the sole field in the `Cell` class. pub const CELL_CONTENTS: &str = "contents"; /// An expression representing the GDLisp `Cell` class. pub fn cell_class(pos: SourceOffset) -> Expr { on_gdlisp_root(String::from("Cell"), pos) } /// Given a GDLisp expression, produce an expression which constructs /// a cell containing it. pub fn construct_cell(expr: Expr) -> Expr { let pos = expr.pos; Expr::call(Some(cell_class(pos)), "new", vec!(expr), pos) } /// Constructs an assignment statement, assigning the variable with /// name `arg` to its own value, but wrapped in a cell. pub fn wrap_var_in_cell(stmt_builder: &mut StmtBuilder, arg: &str, pos: SourceOffset) { let var = Expr::var(arg, pos); stmt_builder.append(Stmt::simple_assign(var.clone(), construct_cell(var), pos)); } ================================================ FILE: src/gdscript/library/class_loader.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::gdnative::NativeClasses; use super::gdnative::class::Class; use crate::ir::decl::{DeclareDecl, DeclareType, ClassInnerDecl, ClassInnerDeclF, ClassVarDecl}; use crate::ir::export::Visibility; use crate::ir::expr::{Expr, ExprF, AssignTarget}; use crate::compile::body::class_initializer::InitTime; use crate::pipeline::source::SourceOffset; use phf::{phf_map, phf_set}; /// Classes which, for one reason or another, we do not want the /// bootstrapping engine to touch. This includes some "fake" classes /// like `GlobalConstants` which do not actually exist at runtime, as /// well as very primitive things like `Object` that we handle /// manually in `GDLisp.lisp`. const CLASS_NAME_BLACKLIST: phf::Set<&'static str> = phf_set! { "GlobalConstants", "Object", }; // For some reason I have yet to fathom, the GDNative API and GDScript // itself have different names for these classes. We use the GDScript // names, but `get_class` still returns the GDNative one, so we have // to be prepared to deal with that here. const PATCHED_CLASS_NAMES: phf::Map<&'static str, &'static str> = phf_map! { "_Directory" => "Directory", "_File" => "File", "_Mutex" => "Mutex", "_Semaphore" => "Semaphore", "_Thread" => "Thread", }; pub fn get_all_non_singleton_classes(native: &NativeClasses) -> Vec<&Class> { let mut classes: Vec<_> = native.values() .filter(|x| !x.singleton && !CLASS_NAME_BLACKLIST.contains(&*x.name)) .collect(); classes.sort_unstable_by(|a, b| a.name.cmp(&b.name)); classes } pub fn get_all_singleton_classes(native: &NativeClasses) -> Vec<&Class> { let mut classes: Vec<_> = native.values() .filter(|x| x.singleton && !CLASS_NAME_BLACKLIST.contains(&*x.name)) .collect(); classes.sort_unstable_by(|a, b| a.name.cmp(&b.name)); classes } pub fn get_non_singleton_declarations(native: &NativeClasses) -> impl Iterator + '_ { get_all_non_singleton_classes(native) .into_iter() .map(|cls| type_declaration_for_class(cls, DeclareType::Superglobal)) } pub fn get_singleton_declarations(native: &NativeClasses) -> Vec { let classes = get_all_singleton_classes(native); let mut result: Vec = Vec::with_capacity(classes.len() * 2); for class in classes { result.push(type_declaration_for_class(class, DeclareType::Value)); result.push(value_declaration_for_singleton(class, DeclareType::Superglobal)); } result } pub fn get_singleton_class_var_declarations(native: &NativeClasses, pos: SourceOffset) -> impl Iterator + '_ { get_all_singleton_classes(native).into_iter().map(move |class| { let name = backing_class_name_of(class); let expr = Expr::var("NamedSyntheticType", pos) .method_call( "new", vec!(Expr::from_value(class.name.clone(), pos)), // Note: *Original* class name, not the one we made in backing_class_name_of pos, ); ClassInnerDecl::new( ClassInnerDeclF::ClassVarDecl(ClassVarDecl { export: None, name: name, value: Some(expr), init_time: InitTime::Init, }), pos, ) }) } fn type_declaration_for_class(class: &Class, declare_type: DeclareType) -> DeclareDecl { let name = backing_class_name_of(class); DeclareDecl { visibility: Visibility::Public, declare_type: declare_type, name: name.clone(), target_name: Some(name), } } fn value_declaration_for_singleton(class: &Class, declare_type: DeclareType) -> DeclareDecl { let singleton_name = class.singleton_name.to_owned(); DeclareDecl { visibility: Visibility::Public, declare_type: declare_type, name: singleton_name.clone(), target_name: Some(singleton_name), } } fn backing_class_name_of(class: &Class) -> String { // Some singletons in Godot have a class name and a distinct // singleton name. For instance, `_Engine` is the class name for // `Engine`. For these, it's fine to just use what Godot has. But // some, like `ARVRServer`, have the *same* name for the type and // object. In that case, we keep the name for the object and force // an underscore at the beginning of the class name. if let Some(translated_name) = PATCHED_CLASS_NAMES.get(&*class.name) { (*translated_name).to_owned() } else if class.singleton_name == class.name { format!("_{}", class.name) } else { class.name.to_owned() } } pub fn native_types_dictionary_literal(native: &NativeClasses, pos: SourceOffset) -> Expr { let mut class_names: Vec<&Class> = native.values().collect(); class_names.sort_unstable_by(|a, b| a.name.cmp(&b.name)); let classes = class_names.into_iter() .filter(|class| !CLASS_NAME_BLACKLIST.contains(&*class.name)) .map(|class| { let original_name = class.name.to_owned(); let gdlisp_name = backing_class_name_of(class); let value: Expr = { if class.singleton { // It's a singleton, so we need to expose our // NamedSyntheticType object. Expr::new( ExprF::FieldAccess( Box::new(Expr::var("self", pos)), gdlisp_name, ), pos, ) } else { // Not a singleton, so the name is globally available in // GDScript; use that name. Expr::var(gdlisp_name, pos) } }; (Expr::from_value(original_name, pos), value) }); build_dict(classes, pos) } fn build_dict(terms: impl DoubleEndedIterator, pos: SourceOffset) -> Expr { Expr::call(String::from("dict"), terms.flat_map(|(k, v)| [k, v]).collect(), pos) } pub fn native_types_dictionary_initializer(native: &NativeClasses, pos: SourceOffset) -> Expr { let assign_target = AssignTarget::InstanceField( pos, Box::new(Expr::var("self", pos)), String::from("__gdlisp_Global_native_types_lookup"), ); let dict_literal = native_types_dictionary_literal(native, pos); Expr::new( ExprF::Assign(assign_target, Box::new(dict_literal)), pos, ) } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; #[test] fn test_non_singleton_classes() { let native = NativeClasses::get_api_from_godot().unwrap(); let classes: HashSet<&str> = get_all_non_singleton_classes(&native) .into_iter() .map(|x| &*x.name) .collect(); assert!(classes.contains("Node")); assert!(classes.contains("Node2D")); assert!(classes.contains("Spatial")); assert!(classes.contains("Texture")); assert!(!classes.contains("Object")); assert!(!classes.contains("_Engine")); assert!(!classes.contains("GlobalConstants")); } #[test] fn test_singleton_classes() { let native = NativeClasses::get_api_from_godot().unwrap(); let classes: HashSet<&str> = get_all_singleton_classes(&native) .into_iter() .map(|x| &*x.name) .collect(); assert!(!classes.contains("Node")); assert!(!classes.contains("Node2D")); assert!(!classes.contains("Spatial")); assert!(!classes.contains("Texture")); assert!(!classes.contains("Object")); assert!(classes.contains("_Engine")); assert!(!classes.contains("GlobalConstants")); } } ================================================ FILE: src/gdscript/library/constant_loader.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::gdnative::NativeClasses; use crate::ir::decl::EnumDecl; use crate::ir::export::Visibility; use crate::ir::expr::Expr; use crate::pipeline::source::SourceOffset; use crate::util::prefix_matcher::PrefixMatcher; use phf::{phf_map, phf_set}; use std::borrow::Borrow; use std::collections::HashMap; const ENUM_GROUPINGS_BY_PREFIX: phf::Map<&'static str, &'static str> = phf_map! { "BUTTON_" => "Mouse", "CORNER_" => "Corner", "ERR_" => "Err", "HALIGN_" => "HAlign", "JOY_" => "Joy", "KEY_" => "Key", "KEY_MASK_" => "KeyMask", "MARGIN_" => "Margin", "METHOD_FLAG_" => "MethodFlag", "MIDI_MESSAGE_" => "MidiMessage", "OP_" => "Op", "PROPERTY_HINT_" => "PropertyHint", "PROPERTY_USAGE_" => "PropertyUsage", "TYPE_" => "Type", "VALIGN_" => "VAlign", }; // Constants which are known in GDScript but which do not belong to an // enum. const ENUM_BLACKLIST: phf::Set<&'static str> = phf_set! { "SPKEY" }; // Names which do not follow the usual prefixing rules. const ENUM_CUSTOM_NAMES: phf::Map<&'static str, &'static str> = phf_map! { "OK" => "Err", "FAILED" => "Err", "METHOD_FLAGS_DEFAULT" => "MethodFlag", "HORIZONTAL" => "Orientation", "VERTICAL" => "Orientation", }; lazy_static! { static ref ENUM_GROUPINGS_TABLE: PrefixMatcher<'static> = PrefixMatcher::build(ENUM_GROUPINGS_BY_PREFIX.keys()); } /// An enumeration of GDScript-side constants. This can be converted /// (via [`From::from`]) into an [`EnumDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ConstantEnum { pub name: String, pub clauses: Vec<(String, String)>, pub pos: SourceOffset, } #[derive(Clone, Debug, PartialEq, Eq)] enum EnumGrouping { Blacklist, Unidentified, // Matches the enum with the given enumeration name and item name. Matched(String, String), } pub fn get_all_constants(classes: &NativeClasses) -> impl Iterator { let global_constants = classes.get_global_constants(); global_constants.constants.keys().map(String::borrow) } pub fn get_all_constant_enums(classes: &NativeClasses, pos: SourceOffset) -> Vec { get_all_constant_enums_with_leftovers(classes, pos).0 } fn get_all_constant_enums_with_leftovers(classes: &NativeClasses, pos: SourceOffset) -> (Vec, Vec<&str>) { let table = &ENUM_GROUPINGS_TABLE; let mut enums: HashMap = HashMap::new(); let mut unidentified: Vec<&str> = Vec::new(); for constant_name in get_all_constants(classes) { match identify_enum_grouping(table, constant_name) { EnumGrouping::Blacklist => { // Skip entirely, we are aware of it and don't want it in an // enum. } EnumGrouping::Unidentified => { // Unidentified unidentified.push(constant_name); } EnumGrouping::Matched(enum_name, enum_item_name) => { let enum_name_clone = enum_name.clone(); let enum_entry: &mut ConstantEnum = enums.entry(enum_name).or_insert_with(|| { ConstantEnum { name: enum_name_clone, clauses: Vec::new(), pos: pos, } }); enum_entry.clauses.push((enum_item_name, constant_name.to_owned())); } } } // For the sake of consistency, always return the enums (and their // entries) in alphabetical order let mut enums: Vec<_> = enums.into_values().collect(); enums.sort_unstable_by(|a, b| a.name.cmp(&b.name)); for constant_enum in &mut enums { constant_enum.clauses.sort_unstable_by(|a, b| a.0.cmp(&b.0)); } (enums, unidentified) } fn identify_enum_grouping(table: &PrefixMatcher<'_>, constant_name: &str) -> EnumGrouping { if ENUM_BLACKLIST.contains(constant_name) { // Blacklist, omit entirely EnumGrouping::Blacklist } else if let Some(custom_name) = ENUM_CUSTOM_NAMES.get(constant_name) { // Match in custom names list EnumGrouping::Matched((*custom_name).to_owned(), constant_name.to_owned()) } else if let Some(prefix) = table.identify_prefix(constant_name) { // Match in prefix rules let enum_name = ENUM_GROUPINGS_BY_PREFIX[prefix]; let enum_item_name = constant_name.strip_prefix(prefix).expect("Prefix did not match"); EnumGrouping::Matched(enum_name.to_owned(), enum_item_name.to_owned()) } else { // No match EnumGrouping::Unidentified } } impl From for EnumDecl { fn from(constant_enum: ConstantEnum) -> EnumDecl { let pos = constant_enum.pos; let clauses: Vec<(String, Option)> = constant_enum.clauses.into_iter() .map(|(name, value)| (name, Some(Expr::var(value, pos)))) .collect(); EnumDecl { visibility: Visibility::Public, name: constant_enum.name, clauses: clauses, } } } #[cfg(test)] mod tests { use super::*; // We should be able to classify, in some form or another, every // single constant in GDScript. If there's one that's unknown (for // instance, because we updated Godot versions and didn't add it), // then this test will fail and will let us know. #[test] fn all_constants_classified_test() { let native_classes = NativeClasses::get_api_from_godot().unwrap(); let (_, leftover_constants) = get_all_constant_enums_with_leftovers(&native_classes, SourceOffset(0)); assert!(leftover_constants.is_empty(), "The following constants were leftover after classification: {:?}", leftover_constants); } } ================================================ FILE: src/gdscript/library/gdnative/api_type.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! The [`ApiType`] enum, indicating whether a GDScript class is in //! the core library or intended for editor tooling. use serde::{Serialize, Deserialize}; /// The type of API to which a GDScript built-in class belongs. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ApiType { /// A core class, which is always available. Core, /// A tooling class, available in Godot builds which have access to /// the editor. Tools, } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn serialize_api_type_test() { let core = serde_json::to_string(&ApiType::Core).unwrap(); assert_eq!(core, "\"core\""); let tools = serde_json::to_string(&ApiType::Tools).unwrap(); assert_eq!(tools, "\"tools\""); } #[test] fn deserialize_api_type_test() { let core: ApiType = serde_json::from_str("\"core\"").unwrap(); assert_eq!(core, ApiType::Core); let tools: ApiType = serde_json::from_str("\"tools\"").unwrap(); assert_eq!(tools, ApiType::Tools); } } ================================================ FILE: src/gdscript/library/gdnative/argument.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Argument { pub name: String, #[serde(rename = "type")] pub argument_type: String, pub has_default_value: bool, pub default_value: String, } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn serialize_roundtrip_argument_test() { let argument = Argument { name: String::from("mesh"), argument_type: String::from("Mesh"), has_default_value: false, default_value: String::from(""), }; let serialized = serde_json::to_string(&argument).unwrap(); assert_eq!(serde_json::from_str::(&serialized).unwrap(), argument); } #[test] fn deserialize_argument_test() { let argument_str = r#"{ "name": "mesh", "type": "Mesh", "has_default_value": false, "default_value": "" }"#; let argument = Argument { name: String::from("mesh"), argument_type: String::from("Mesh"), has_default_value: false, default_value: String::from(""), }; assert_eq!(serde_json::from_str::(&argument_str).unwrap(), argument); } } ================================================ FILE: src/gdscript/library/gdnative/class.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::api_type::ApiType; use super::property::Property; use super::signal::Signal; use super::method::Method; use super::gdnative_enum::Enum; use serde::{Serialize, Deserialize}; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Class { pub name: String, pub base_class: String, pub api_type: ApiType, pub singleton: bool, pub singleton_name: String, pub instanciable: bool, pub is_reference: bool, pub constants: HashMap, pub properties: Vec, pub signals: Vec, pub methods: Vec, pub enums: Vec, } #[cfg(test)] mod tests { use super::*; use serde_json; // Note: This is not the actual _Engine class from any GDScript // version. It's pared down from the real one but made smaller for // testing purposes. const SAMPLE_CLASS_JSON: &str = r#"{ "name": "_Engine", "base_class": "Object", "api_type": "core", "singleton": true, "singleton_name": "Engine", "instanciable": false, "is_reference": false, "constants": { }, "properties": [ { "name": "editor_hint", "type": "bool", "getter": "is_editor_hint", "setter": "set_editor_hint", "index": -1 }, { "name": "iterations_per_second", "type": "int", "getter": "get_iterations_per_second", "setter": "set_iterations_per_second", "index": -1 } ], "signals": [ ], "methods": [ { "name": "get_author_info", "return_type": "Dictionary", "is_editor": false, "is_noscript": false, "is_const": true, "is_reverse": false, "is_virtual": false, "has_varargs": false, "is_from_script": false, "arguments": [ ] }, { "name": "get_copyright_info", "return_type": "Array", "is_editor": false, "is_noscript": false, "is_const": true, "is_reverse": false, "is_virtual": false, "has_varargs": false, "is_from_script": false, "arguments": [ ] } ], "enums": [ ] }"#; #[test] fn deserialize_class_test() { let class: Class = serde_json::from_str(SAMPLE_CLASS_JSON).unwrap(); assert_eq!(class.name, String::from("_Engine")); assert_eq!(class.base_class, String::from("Object")); assert_eq!(class.api_type, ApiType::Core); assert_eq!(class.singleton, true); assert_eq!(class.singleton_name, String::from("Engine")); assert_eq!(class.instanciable, false); assert_eq!(class.is_reference, false); assert_eq!(class.constants.len(), 0); assert_eq!(class.properties.len(), 2); assert_eq!(class.signals.len(), 0); assert_eq!(class.methods.len(), 2); assert_eq!(class.enums.len(), 0); } } ================================================ FILE: src/gdscript/library/gdnative/gdnative_enum.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use serde::{Serialize, Deserialize}; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Enum { pub name: String, pub values: HashMap, } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn deserialize_enum_test() { let enum_str = r#"{ "name": "TrackerHand", "values": { "TRACKER_HAND_UNKNOWN": 0, "TRACKER_LEFT_HAND": 1, "TRACKER_RIGHT_HAND": 2 } }"#; let enum_val = Enum { name: String::from("TrackerHand"), values: HashMap::from([ (String::from("TRACKER_HAND_UNKNOWN"), 0), (String::from("TRACKER_LEFT_HAND"), 1), (String::from("TRACKER_RIGHT_HAND"), 2), ]), }; assert_eq!(serde_json::from_str::(&enum_str).unwrap(), enum_val); } } ================================================ FILE: src/gdscript/library/gdnative/method.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::argument::Argument; use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Method { pub name: String, pub return_type: String, pub is_editor: bool, pub is_noscript: bool, pub is_const: bool, pub is_reverse: bool, pub is_virtual: bool, pub has_varargs: bool, pub is_from_script: bool, pub arguments: Vec, } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn deserialize_method_test() { let method_str = r#"{ "name": "add_interface", "return_type": "void", "is_editor": false, "is_noscript": false, "is_const": false, "is_reverse": false, "is_virtual": false, "has_varargs": false, "is_from_script": false, "arguments": [ { "name": "interface", "type": "ARVRInterface", "has_default_value": false, "default_value": "" } ] }"#; let result: Method = serde_json::from_str(method_str).unwrap(); assert_eq!(result, Method { name: String::from("add_interface"), return_type: String::from("void"), is_editor: false, is_noscript: false, is_const: false, is_reverse: false, is_virtual: false, has_varargs: false, is_from_script: false, arguments: vec![ Argument { name: String::from("interface"), argument_type: String::from("ARVRInterface"), has_default_value: false, default_value: String::from(""), }, ], }); } } ================================================ FILE: src/gdscript/library/gdnative/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Classes for reading the output of `godot //! --gdnative-generate-json-api`. pub mod argument; pub mod api_type; pub mod class; pub mod gdnative_enum; pub mod method; pub mod property; pub mod signal; use class::Class; use crate::runner::dump_json_api; use serde_json; use std::io; use std::collections::HashMap; pub const GLOBAL_CONSTANTS_CLASS: &str = "GlobalConstants"; #[derive(Debug, Clone)] pub struct NativeClasses { mapping: HashMap, } impl NativeClasses { pub fn get_api_from_godot() -> io::Result { let tempfile = dump_json_api()?; let classes: Vec = serde_json::from_reader(tempfile)?; Ok( NativeClasses { mapping: classes.into_iter().map(|cls| (cls.name.clone(), cls)).collect(), } ) } pub fn len(&self) -> usize { self.mapping.len() } pub fn is_empty(&self) -> bool { self.mapping.is_empty() } pub fn get(&self, class_name: &str) -> Option<&Class> { self.mapping.get(class_name) } pub fn get_global_constants(&self) -> &Class { self.get(GLOBAL_CONSTANTS_CLASS).expect("Could not read global constants from GDNative API") } pub fn values(&self) -> impl Iterator { self.mapping.values() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_api_from_godot() { let classes = NativeClasses::get_api_from_godot().unwrap(); // Assert only a few basic facts about the classes, as we want to // maximize compatibility with forked or custom Godot builds that // have different classes available. // There should exist at least one class. assert!(classes.len() > 0); // There should be an Object class with the desired properties. let object = classes.get("Object").unwrap(); assert_eq!(object.name, "Object"); assert_eq!(object.base_class, ""); assert_eq!(object.api_type, api_type::ApiType::Core); assert_eq!(object.singleton, false); assert_eq!(object.singleton_name, ""); assert_eq!(object.instanciable, true); assert_eq!(object.is_reference, false); // There should be an Reference class with the desired properties. let object = classes.get("Reference").unwrap(); assert_eq!(object.name, "Reference"); assert_eq!(object.base_class, "Object"); assert_eq!(object.api_type, api_type::ApiType::Core); assert_eq!(object.singleton, false); assert_eq!(object.singleton_name, ""); assert_eq!(object.instanciable, true); assert_eq!(object.is_reference, true); // There should be an Reference class with the desired properties. let object = classes.get("Node").unwrap(); assert_eq!(object.name, "Node"); assert_eq!(object.base_class, "Object"); assert_eq!(object.api_type, api_type::ApiType::Core); assert_eq!(object.singleton, false); assert_eq!(object.singleton_name, ""); assert_eq!(object.instanciable, true); assert_eq!(object.is_reference, false); } } ================================================ FILE: src/gdscript/library/gdnative/property.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Property { pub name: String, #[serde(rename = "type")] pub property_type: String, pub getter: String, pub setter: String, pub index: i32, } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn deserialize_property_test() { let property_str = r#"{ "name": "editor_hint", "type": "bool", "getter": "is_editor_hint", "setter": "set_editor_hint", "index": -1 }"#; let result: Property = serde_json::from_str(property_str).unwrap(); assert_eq!(result, Property { name: String::from("editor_hint"), property_type: String::from("bool"), getter: String::from("is_editor_hint"), setter: String::from("set_editor_hint"), index: -1, }); } } ================================================ FILE: src/gdscript/library/gdnative/signal.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::argument::Argument; use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Signal { pub name: String, pub arguments: Vec, } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn deserialize_signal_test() { let signal_str = r#"{ "name": "mesh_updated", "arguments": [ { "name": "mesh", "type": "Mesh", "has_default_value": false, "default_value": "" } ] }"#; let result: Signal = serde_json::from_str(signal_str).unwrap(); assert_eq!(result, Signal { name: String::from("mesh_updated"), arguments: vec![ Argument { name: String::from("mesh"), argument_type: String::from("Mesh"), has_default_value: false, default_value: String::from(""), }, ], }); } } ================================================ FILE: src/gdscript/library/magic.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDLisp call magic. //! //! Call magic can be thought of as a sort of optimization and //! bootstrapping technique. In essence, a function call like `(+ a //! b)` in GDLisp should, in full generality, compile to a GDScript //! function call of the form `GDLisp.plus(GDLisp.cons(a, //! GDLisp.cons(b, null)))`. However, for obvious reasons, we would //! prefer that this simply compile to `a + b`. //! //! This is exactly what call magic accomplishes. The function `+` in //! GDLisp is marked with a special flag which indicates that it is //! eligible for call magic. There is a `GDLisp.gd` implementation of //! `+` in full generality which will get called if the programmer //! ever uses `+` as a first-class function (i.e. `(function +)` or //! `#'+`). However, if the programmer ever calls `+` directly, then //! the call magic is guaranteed to fire and replace the call with the //! correct syntax. //! //! As mentioned above, this is not *merely* an optimization. The `+` //! function in GDLisp could ostensibly be implemented in terms of //! itself. Since call magic is guaranteed behavior, this bootstrapped //! function would be guaranteed to compile to actual addition and not //! be recursive. use crate::compile::symbol_table::call_magic::{CallMagic, Assoc}; use crate::compile::symbol_table::call_magic::table::MagicTable; use crate::gdscript::op; use crate::gdscript::literal::Literal; /// Bind all GDLisp call magic to the magic table given. pub fn bind_magic(table: &mut MagicTable) { // Default magic (used by default for all user-defined functions and // for any builtins which don't request other magic) table.set(String::from("DEFAULT"), CallMagic::DefaultCall); // Addition (+) table.set(String::from("ADDITION"), CallMagic::CompileToBinOp(Literal::from(0), op::BinaryOp::Add, Assoc::Left)); // Multiplication (*) table.set(String::from("MULTIPLICATION"), CallMagic::CompileToBinOp(Literal::from(1), op::BinaryOp::Times, Assoc::Left)); // Subtraction (-) table.set(String::from("SUBTRACTION"), CallMagic::MinusOperation); // Division (/) table.set(String::from("DIVISION"), CallMagic::DivOperation); // Integer Division (div) table.set(String::from("INTEGER-DIVISION"), CallMagic::IntDivOperation); // Modulo Division (mod) table.set(String::from("MODULO"), CallMagic::ModOperation); // Min function (min) table.set(String::from("MIN-FUNCTION"), CallMagic::MinFunction); // Max function (max) table.set(String::from("MAX-FUNCTION"), CallMagic::MaxFunction); // Equality (=) table.set(String::from("EQUAL"), CallMagic::CompileToTransCmp(op::BinaryOp::Eq)); // Less Than (<) table.set(String::from("LESS-THAN"), CallMagic::CompileToTransCmp(op::BinaryOp::LT)); // Greater Than (>) table.set(String::from("GREATER-THAN"), CallMagic::CompileToTransCmp(op::BinaryOp::GT)); // Less Than or Equal (<=) table.set(String::from("LESS-THAN-OR-EQUAL"), CallMagic::CompileToTransCmp(op::BinaryOp::LE)); // Greater Than or Equal (>=) table.set(String::from("GREATER-THAN-OR-EQUAL"), CallMagic::CompileToTransCmp(op::BinaryOp::GE)); // Not Equal (/=) table.set(String::from("NOT-EQUAL"), CallMagic::NEqOperation(Box::new(CallMagic::DefaultCall))); // Boolean Not (not) table.set(String::from("BOOLEAN-NOT"), CallMagic::BooleanNotOperation); // List (list) table.set(String::from("LIST"), CallMagic::ListOperation); // Array (array) table.set(String::from("ARRAY"), CallMagic::ArrayOperation); // Dictionary (dict) table.set(String::from("DICT"), CallMagic::DictOperation); // Vector (vector) table.set(String::from("VECTOR"), CallMagic::VectorOperation); // Array Subscript (elt) table.set(String::from("ARRAY-SUBSCRIPT"), CallMagic::ArraySubscript); // Array Subscript Assignment (set-elt) table.set(String::from("ARRAY-SUBSCRIPT-ASSIGNMENT"), CallMagic::ArraySubscriptAssign); // Dictionary Subscript (dict/elt; same as elt) table.set(String::from("DICT-SUBSCRIPT"), CallMagic::ArraySubscript); // Dictionary Subscript Assignment (set-dict/elt; same as set-elt) table.set(String::from("DICT-SUBSCRIPT-ASSIGNMENT"), CallMagic::ArraySubscriptAssign); // Direct Instance Check (sys/instance_direct?) table.set(String::from("DIRECT-INSTANCE-CHECK"), CallMagic::InstanceOf); // Array Membership Check (member?) table.set(String::from("ARRAY-MEMBER-CHECK"), CallMagic::ElementOf); // Node access (sys/get-node) table.set(String::from("GET-NODE-SYNTAX"), CallMagic::GetNodeSyntax); // str function table.set(String::from("VARARG-STR"), CallMagic::CompileToVarargCall(String::from("str"))); // printerr function table.set(String::from("VARARG-PRINTERR"), CallMagic::CompileToVarargCall(String::from("printerr"))); // printraw function table.set(String::from("VARARG-PRINTRAW"), CallMagic::CompileToVarargCall(String::from("printraw"))); // print-debug function table.set(String::from("VARARG-PRINTDEBUG"), CallMagic::CompileToVarargCall(String::from("print_debug"))); // printt function table.set(String::from("VARARG-PRINTT"), CallMagic::CompileToVarargCall(String::from("printt"))); // prints function table.set(String::from("VARARG-PRINTS"), CallMagic::CompileToVarargCall(String::from("prints"))); // print function table.set(String::from("VARARG-PRINT"), CallMagic::CompileToVarargCall(String::from("print"))); // range function table.set(String::from("VARARG-RANGE"), CallMagic::CompileToVarargCall(String::from("range"))); // Color8 function table.set(String::from("VARARG-COLOR8"), CallMagic::CompileToVarargCall(String::from("Color8"))); // ColorN function table.set(String::from("VARARG-COLORN"), CallMagic::CompileToVarargCall(String::from("ColorN"))); // bytes2var function table.set(String::from("VARARG-BYTES2VAR"), CallMagic::CompileToVarargCall(String::from("bytes2var"))); // var2bytes function table.set(String::from("VARARG-VAR2BYTES"), CallMagic::CompileToVarargCall(String::from("var2bytes"))); // Rect2 function table.set(String::from("VARARG-RECT2"), CallMagic::CompileToVarargCall(String::from("Rect2"))); // Transform2D function table.set(String::from("VARARG-TRANSFORM2D"), CallMagic::CompileToVarargCall(String::from("Transform2D"))); // Plane function table.set(String::from("VARARG-PLANE"), CallMagic::CompileToVarargCall(String::from("Plane"))); // Quat function table.set(String::from("VARARG-QUAT"), CallMagic::CompileToVarargCall(String::from("Quat"))); // Basis function table.set(String::from("VARARG-BASIS"), CallMagic::CompileToVarargCall(String::from("Basis"))); // Transform function table.set(String::from("VARARG-TRANSFORM"), CallMagic::CompileToVarargCall(String::from("Transform"))); // Color function table.set(String::from("VARARG-COLOR"), CallMagic::CompileToVarargCall(String::from("Color"))); // NodePath function table.set(String::from("NODEPATH-SYNTAX"), CallMagic::NodePathConstructor(Box::new(CallMagic::DefaultCall))); } /// Produce a new [`MagicTable`] with all of the magic bound as though /// via [`bind_magic`]. pub fn standard_magic_table() -> MagicTable { let mut table = MagicTable::new(); bind_magic(&mut table); table } ================================================ FILE: src/gdscript/library/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Convenient access to the builtins in `GDLisp.gd`. //! //! The most commonly-used functions from this module are //! [`bind_builtins`], [`bind_builtin_macros`], and //! [`all_builtin_names`], though the module also provides several //! simpler helper functions for commonly-used GDLisp builtins. pub mod cell; pub mod class_loader; pub mod constant_loader; pub mod gdnative; pub mod magic; use super::expr::{Expr, ExprF}; use crate::compile::Compiler; use crate::compile::names; use crate::compile::symbol_table::SymbolTable; use crate::compile::symbol_table::local_var::VarName; use crate::ir::identifier::{Id, Namespace}; use crate::ir::macros::MacroData; use crate::runner::version::{VersionInfo, get_godot_version}; use crate::pipeline::Pipeline; use crate::pipeline::translation_unit::TranslationUnit; use crate::pipeline::stdlib_unit::StdlibUnit; use crate::pipeline::config::ProjectConfig; use crate::pipeline::source::{SourceOffset, SourcedValue}; use rmp_serde::{encode, decode}; use std::collections::{HashSet, HashMap}; use std::path::PathBuf; use std::fs::File; use std::env::current_exe; /// The name of the top-level GDLisp singleton object. pub const GDLISP_NAME: &str = "GDLisp"; /// The name of a GDScript constructor. pub const CONSTRUCTOR_NAME: &str = "_init"; /// The name of a GDScript _ready function which runs when a node is /// added to the scene tree. pub const READY_NAME: &str = "_ready"; /// The name of the default superclass, when one is not specified. pub const REFERENCE_NAME: &str = "Reference"; /// An expression which accesses the global GDLisp singleton object. pub fn gdlisp_root(pos: SourceOffset) -> Expr { Expr::var(GDLISP_NAME, pos) } /// A variable name which accesses the global GDLisp singleton object. pub fn gdlisp_root_var_name() -> VarName { VarName::Superglobal(GDLISP_NAME.to_owned()) } /// An expression which accesses a specific field on [`gdlisp_root`]. pub fn on_gdlisp_root(name: String, pos: SourceOffset) -> Expr { Expr::new(ExprF::Attribute(Box::new(gdlisp_root(pos)), name), pos) } /// Given a vector of expressions `vec`, produce an expression which /// produces a GDLisp list containing those expressions in order. pub fn construct_list(vec: Vec, pos: SourceOffset) -> Expr { vec.into_iter().rev().fold(Expr::null(pos), |rest, first| { Expr::call(Some(gdlisp_root(pos)), "cons", vec!(first, rest), pos) }) } /// An appropriate [`ProjectConfig`] for the `GDLisp.gd` source file. pub fn gdlisp_project_config(godot_version: VersionInfo) -> ProjectConfig { ProjectConfig { root_directory: PathBuf::from("."), optimizations: true, godot_version: godot_version, } } fn get_stdlib() -> StdlibUnit { let exe_path = current_exe().expect("Could not locate current executable"); let exe_dir = exe_path.parent().expect("Could not locate executable path"); let file = File::open(exe_dir.join("GDLisp.msgpack")).expect("I/O error reading GDLisp.msgpack (this file should have been built as part of the GDLisp build process; are you sure you have compiled GDLisp fully?)"); decode::from_read(file).expect("Error deserializing stdlib (this is likely a GDLisp error and should be reported as a bug)") } pub fn load_stdlib_to_file() -> StdlibUnit { let godot_version = get_godot_version().expect("I/O error loading stdlib"); let mut pipeline = Pipeline::new(gdlisp_project_config(godot_version)); let unit = load_stdlib_file(&mut pipeline); let stdlib = StdlibUnit::from(unit.clone_detached()); let mut file = File::create("GDLisp.msgpack").expect("I/O error loading stdlib (does GDLisp have write permissions for this folder?)"); encode::write(&mut file, &stdlib).expect("Error during serialization of stdlib"); stdlib } fn load_stdlib_file(pipeline: &mut Pipeline) -> &TranslationUnit { match pipeline.load_file(&PathBuf::from("GDLisp.lisp"), SourceOffset(0)) { Ok(unit) => unit, Err(err) => { // This is basically .expect() but with a much better error // message. let err = SourcedValue::from_file(&err, "GDLisp.lisp").expect("Error loading stdlib (failed to get further diagnostic information)"); panic!("Error loading stdlib: {}", err); } } } /// Ensure that the standard library `GDLisp.lisp` has been compiled. /// If it has not, then this function compiles it. As with other /// `library` functions, this will panic in case of error. After this /// function returns, a file named `GDLisp.gd` will exist in the /// project directory. pub fn ensure_stdlib_loaded() { let _ = get_stdlib(); } /// Bind all GDLisp and GDScript built-in names to the given symbol /// table. /// /// This function does *not* bind built-in macros. /// [`bind_builtin_macros`] is a separate function provided for that /// behavior. /// /// If `minimalist` is true, then the GDLisp standard library will not /// be bound. Note that this does *not* make `bind_builtins` a no-op; /// GDScript global names like top-level classes (`Reference`, `Node`, /// etc.) will be bound regardless of `minimalist`, but the GDLisp /// standard library will not be loaded. `minimalist` should generally /// be false and is intended as a means of compiling the standard /// library itself. pub fn bind_builtins(table: &mut SymbolTable, minimalist: bool) { let unit = if minimalist { None } else { Some(get_stdlib()) }; bind_builtins_unchecked(table, unit.as_ref()); } fn bind_builtins_unchecked(table: &mut SymbolTable, unit: Option<&StdlibUnit>) { // (Assumes minimalist compile iff unit is None) // TODO Do we need to bind built-in macros here? Macros should have // no runtime presence so that makes me think no, but at the same // time we do bind user-defined macros to the symbol table. if let Some(unit) = unit { for id in &unit.exports { match id.namespace { Namespace::Value => { let value = unit.table.get_var(&id.name).unwrap_or_else(|| panic!("Exported value name {} does not appear", id.name)); let mut value = value.to_owned(); value.name = value.name.into_imported(String::from("GDLisp")); table.set_var(id.name.to_owned(), value); } Namespace::Function => { let (call, magic) = unit.table.get_fn(&id.name).unwrap_or_else(|| panic!("Exported function name {} does not appear", id.name)); let call = Compiler::translate_call(String::from("GDLisp"), call.clone()); let magic = magic.clone(); table.set_fn(id.name.to_owned(), call, magic); } } } } } /// Get a collection of all of the built-in names in GDLisp and /// GDScript, in no particular order. /// /// `minimalist` is passed onto [`bind_builtins`] as-is and will be /// interpreted in the same way it is in that function. pub fn all_builtin_names(minimalist: bool) -> HashSet { // bind_builtins is the single source of truth for built-in names. // So if we want them in a HashSet, we should let bind_builtins // build a symbol table, and then we should convert that into a // HashSet. let mut table = SymbolTable::new(); bind_builtins(&mut table, minimalist); table.into_keys().collect() } /// Bind all of the built-in GDLisp macros and symbol macros to the /// macro table given. /// /// If the standard library has not been loaded, this function loads /// the standard library. As such, this function should not be called /// in the process of loading the standard library, as that will /// result in a double lock on the stdlib mutex. pub fn bind_builtin_macros(macros: &mut HashMap, pipeline: &mut Pipeline) { let unit = get_stdlib(); for name in &unit.exports { if let Some(data) = unit.macros.get(name) { macros.insert(name.to_owned(), data.to_imported()); pipeline.get_server_mut().add_reserved_macro(names::lisp_to_gd(&name.name)); } } } ================================================ FILE: src/gdscript/literal.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDScript literal values. //! //! This module represents the subset of [GDScript //! expressions](super::expr) which are literal values, i.e. which //! evaluate to themselves. This includes literal integers, strings, //! and the special value `null`. use super::is_valid_node_path; use crate::ir::literal::{Literal as IRLiteral}; use crate::sxp::string::insert_escapes; use ordered_float::OrderedFloat; use serde::{Serialize, Deserialize}; use std::convert::TryFrom; use std::ops::Deref; use std::fmt; /// The type of GDScript literal values. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Literal { Int(i32), Float(LiteralFloat), String(String), NodeLiteral(String), NodePathLiteral(String), Null, Bool(bool), } /// A literal floating-point value, with reflexive equality semantics. /// The only difference between this type and [`OrderedFloat`] is that /// the former supports serde. #[derive(Clone, Copy, Serialize, Deserialize)] pub struct LiteralFloat(pub f32); /// In most cases, we can convert an /// [`ir::literal::Literal`](IRLiteral) to a [`Literal`]. However, /// there are a handful of corner cases where `ir::literal::Literal` /// compiles to something nontrivial in GDScript. In those cases, this /// error will be returned. #[derive(Debug, Clone, Default)] pub struct IRToExprLiteralError; impl Literal { /// Convert a GDScript literal to a string. The result will contain /// valid GDScript syntax. pub fn to_gd(&self) -> String { match self { Literal::Int(n) => n.to_string(), Literal::String(s) => format!("\"{}\"", insert_escapes(s)), Literal::NodeLiteral(s) => { if is_valid_node_path(s) { format!("${}", s) } else { format!("$\"{}\"", insert_escapes(s)) } } Literal::NodePathLiteral(s) => format!("@\"{}\"", insert_escapes(s)), Literal::Null => String::from("null"), Literal::Bool(b) => if *b { String::from("true") } else { String::from("false") }, Literal::Float(f) => format!("{:e}", **f), } } } impl From for Literal { fn from(x: i32) -> Literal { Literal::Int(x) } } impl From> for Literal { fn from(x: OrderedFloat) -> Literal { Literal::Float(LiteralFloat(*x)) } } impl From for Literal { fn from(x: String) -> Literal { Literal::String(x) } } impl<'a> From<&'a str> for Literal { fn from(x: &'a str) -> Literal { Literal::String(String::from(x)) } } impl From for Literal { fn from(x: bool) -> Literal { Literal::Bool(x) } } impl TryFrom for Literal { type Error = IRToExprLiteralError; fn try_from(value: IRLiteral) -> Result { match value { IRLiteral::Nil => Ok(Literal::Null), IRLiteral::Int(n) => Ok(Literal::Int(n)), IRLiteral::Float(f) => Ok(Literal::Float(LiteralFloat(*f))), IRLiteral::String(s) => Ok(Literal::String(s)), IRLiteral::Symbol(_) => Err(IRToExprLiteralError), // Doesn't compile to a GDScript literal IRLiteral::Bool(b) => Ok(Literal::Bool(b)), } } } impl PartialEq for LiteralFloat { /// Compares the two floats for equality using [`OrderedFloat`] /// semantics, so NaN compares equal to itself. fn eq(&self, other: &Self) -> bool { OrderedFloat(**self) == OrderedFloat(**other) } } impl Eq for LiteralFloat {} impl Deref for LiteralFloat { type Target = f32; fn deref(&self) -> &f32 { &self.0 } } impl fmt::Debug for LiteralFloat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[cfg(test)] mod tests { use super::*; #[test] fn literal_test() { assert_eq!(Literal::Int(10).to_gd(), "10"); assert_eq!(Literal::Null.to_gd(), "null"); assert_eq!(Literal::Bool(false).to_gd(), "false"); assert_eq!(Literal::Bool(true).to_gd(), "true"); } #[test] fn string_test() { assert_eq!(Literal::String("foo".to_owned()).to_gd(), "\"foo\""); assert_eq!(Literal::String("foo\"bar".to_owned()).to_gd(), "\"foo\\\"bar\""); } #[test] fn node_path_test() { assert_eq!(Literal::NodeLiteral("foo".to_owned()).to_gd(), "$foo"); assert_eq!(Literal::NodeLiteral("foo\"bar".to_owned()).to_gd(), "$\"foo\\\"bar\""); assert_eq!(Literal::NodeLiteral("foo bar".to_owned()).to_gd(), "$\"foo bar\""); } #[test] fn node_path_literal_test() { assert_eq!(Literal::NodePathLiteral("foo".to_owned()).to_gd(), "@\"foo\""); assert_eq!(Literal::NodePathLiteral("foo\"bar".to_owned()).to_gd(), "@\"foo\\\"bar\""); assert_eq!(Literal::NodePathLiteral("foo bar".to_owned()).to_gd(), "@\"foo bar\""); } } ================================================ FILE: src/gdscript/metadata.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helpers for using GDScript object and script metadata. //! //! Every object in Godot has metadata fields that can be modified. //! These fields exist independently of the usual instance variables //! on an object and can even be defined on built-in class instances //! such as `GDScript` objects themselves. GDLisp uses this capability //! to define various data needed for more advanced languages //! features. //! //! This module provides helpers to read, write, and check whether //! metadata exists. It also provides naming conventions for managing //! the names of GDLisp-useful metadata, to avoid conflicts. All //! GDLisp metadata keys will always begin with the prefix `__gdlisp` //! followed by some more specific string. The specific strings used //! are implementation details and should not be relied upon in GDLisp //! code. use super::expr::Expr; use crate::pipeline::source::SourceOffset; /// The prefix applied to all GDLisp metadata. pub const PREFIX: &str = "__gdlisp"; /// The metadata field that exists (and is truthy) on all cons cells /// in GDLisp. pub const CONS_META: &str = "__gdlisp_Primitive_Cons"; /// The metadata field that exists (and is truthy) on all symbol /// values in GDLisp. pub const SYMBOL_META: &str = "__gdlisp_Primitive_Symbol"; /// The name of the global function we define in a REPL file to /// indicate the REPL code to be run. pub const REPL_FUNCTION_NAME: &str = "__gdlisp_Repl_runner"; /// The name of the dictionary on an object's metadata that lists any /// objects being kept alive for the purposes of signal dispatch. pub const SIGNAL_META: &str = "__gdlisp_signals"; /// Given a GDScript function name, prefix it appropriately for a /// symbol macro with the given name. pub fn symbol_macro(name: &str) -> String { format!("{}_SymbolMacroFunction_{}", PREFIX, name) } /// Given a GDLisp lazy val, this is the name of the metadata with /// suffix `name`. /// /// Note that, under the current implementation, the `deflazy` macro /// does *not* use the GDScript name but rather a gensym to store the /// metadata. This behavior may be changed in the future. pub fn lazy(name: &str) -> String { format!("{}_Lazy_{}", PREFIX, name) } /// A `MetadataCompiler` is used as a helper for compiling calls to /// GDScript metadata functions such as `get_meta`, `has_meta`, and /// `set_meta`. #[derive(Clone, Debug)] pub struct MetadataCompiler { object_ref: Expr, } impl MetadataCompiler { /// A new `MetadataCompiler` which will compile to metadata calls on /// the given expression. pub fn new(expr: Expr) -> MetadataCompiler { MetadataCompiler { object_ref: expr } } fn get_pos(&self) -> SourceOffset { self.object_ref.pos } /// Compiles to a `get_meta` GDScript call. pub fn get_meta(&self, meta_name: &str) -> Expr { let meta_name_expr = Expr::from_value(meta_name, self.get_pos()); Expr::call(Some(self.object_ref.clone()), "get_meta", vec!(meta_name_expr), self.get_pos()) } /// Compiles to a `set_meta` GDScript call. pub fn set_meta(&self, meta_name: &str, value: Expr) -> Expr { let meta_name_expr = Expr::from_value(meta_name, self.get_pos()); Expr::call(Some(self.object_ref.clone()), "set_meta", vec!(meta_name_expr, value), self.get_pos()) } /// Compiles to a `has_meta` GDScript call. pub fn has_meta(&self, meta_name: &str) -> Expr { let meta_name_expr = Expr::from_value(meta_name, self.get_pos()); Expr::call(Some(self.object_ref.clone()), "has_meta", vec!(meta_name_expr), self.get_pos()) } /// Compiles to a `remove_meta` GDScript call. pub fn remove_meta(&self, meta_name: &str) -> Expr { let meta_name_expr = Expr::from_value(meta_name, self.get_pos()); Expr::call(Some(self.object_ref.clone()), "remove_meta", vec!(meta_name_expr), self.get_pos()) } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::expr::ExprF; #[test] fn naming_test() { assert_eq!(lazy("ABC"), "__gdlisp_Lazy_ABC"); assert_eq!(lazy("Foobar"), "__gdlisp_Lazy_Foobar"); } #[test] fn compiler_test() { let expr = Expr::var("reference", SourceOffset(0)); let value_expr = Expr::from_value(100, SourceOffset(0)); let compiler = MetadataCompiler::new(expr); // Basic compilation assert_eq!(compiler.get_meta("foobar").to_gd(), "reference.get_meta(\"foobar\")"); assert_eq!(compiler.has_meta("foobar").to_gd(), "reference.has_meta(\"foobar\")"); assert_eq!(compiler.remove_meta("foobar").to_gd(), "reference.remove_meta(\"foobar\")"); assert_eq!(compiler.set_meta("foobar", value_expr.clone()).to_gd(), "reference.set_meta(\"foobar\", 100)"); } #[test] fn compiler_offset_test() { let expr = Expr::var("reference", SourceOffset(42)); let value_expr = Expr::from_value(100, SourceOffset(10)); let compiler = MetadataCompiler::new(expr); // Check that the generated source offsets are correct let result_expr = compiler.set_meta("foobar", value_expr); assert_eq!(result_expr.pos, SourceOffset(42)); match result_expr { Expr { value: ExprF::Call(Some(lhs), _, args), pos: _ } => { assert_eq!(lhs.pos, SourceOffset(42)); assert_eq!(args.len(), 2); assert_eq!(args[0].pos, SourceOffset(42)); assert_eq!(args[1].pos, SourceOffset(10)); } _ => { panic!("Wrong shape of result, got {:?}", result_expr); } } } } ================================================ FILE: src/gdscript/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Data structures for representing and manipulating valid GDScript //! code. pub mod arglist; pub mod class_extends; pub mod decl; pub mod expr; pub mod expr_wrapper; pub mod inner_class; pub mod library; pub mod literal; pub mod metadata; pub mod op; pub mod pattern; pub mod spacing; pub mod stmt; use regex::Regex; use std::fmt; /// Indent to the given position with spaces. /// /// # Examples /// /// ``` /// # use gdlisp::gdscript::indent; /// let mut str = String::new(); /// indent(&mut str, 4); /// assert_eq!(str, " "); /// ``` pub fn indent(w: &mut W, ind: u32) -> Result<(), fmt::Error> { let spaces = " ".repeat(ind as usize); write!(w, "{}", spaces) } /// Determines whether the string is a valid GDScript identifier, by /// the rules of the language. A GDScript identifier consists only of /// ASCII alphanumeric characters and underscore and cannot begin with /// an underscore. pub fn is_valid_identifier(s: &str) -> bool { lazy_static! { static ref RE: Regex = Regex::new(r#"\A[A-Za-z_][A-Za-z0-9_]*\z"#).unwrap(); } RE.is_match(s) } /// Determines whether the string is a valid GDScript node path. A /// valid GDScript node path is a sequence of one or more valid /// identifiers, delimited by forward slashes. /// /// Note: This function checks the *path* portion of the node path /// syntax, so a string containing a `$` will not be accepted. /// Additionally, this function is not designed to check the extended /// quoted node path syntax, so a string containing `"` will not be /// accepted. pub fn is_valid_node_path(s: &str) -> bool { s.split('/').all(is_valid_identifier) } #[cfg(test)] mod tests { use super::*; fn do_indent(ind: u32) -> String { let mut x = String::new(); indent(&mut x, ind).unwrap(); x } #[test] fn test_indent() { assert_eq!(do_indent(0), ""); assert_eq!(do_indent(1), " "); assert_eq!(do_indent(2), " "); assert_eq!(do_indent(3), " "); assert_eq!(do_indent(4), " "); } #[test] fn test_identifier() { // Valid assert_eq!(is_valid_identifier("foo"), true); assert_eq!(is_valid_identifier("foo99"), true); assert_eq!(is_valid_identifier("FOO99"), true); assert_eq!(is_valid_identifier("_"), true); assert_eq!(is_valid_identifier("_a_b_c"), true); assert_eq!(is_valid_identifier("_00_00_"), true); // Invalid assert_eq!(is_valid_identifier(""), false); assert_eq!(is_valid_identifier("0"), false); assert_eq!(is_valid_identifier("0.0"), false); assert_eq!(is_valid_identifier("a.b"), false); assert_eq!(is_valid_identifier("c/d"), false); assert_eq!(is_valid_identifier("-"), false); } #[test] fn test_path() { // Valid assert_eq!(is_valid_node_path("foo"), true); assert_eq!(is_valid_node_path("foo99"), true); assert_eq!(is_valid_node_path("_"), true); assert_eq!(is_valid_node_path("_a_b_c"), true); assert_eq!(is_valid_node_path("_00_00_"), true); assert_eq!(is_valid_node_path("a/b"), true); assert_eq!(is_valid_node_path("a/b/cdef"), true); assert_eq!(is_valid_node_path("A/B/C"), true); // Invalid assert_eq!(is_valid_node_path(""), false); assert_eq!(is_valid_node_path("0"), false); assert_eq!(is_valid_node_path("0.0"), false); assert_eq!(is_valid_node_path("a.b"), false); assert_eq!(is_valid_node_path("c//d"), false); assert_eq!(is_valid_node_path("/a"), false); assert_eq!(is_valid_node_path("a/"), false); assert_eq!(is_valid_node_path("/"), false); assert_eq!(is_valid_node_path("/0"), false); assert_eq!(is_valid_node_path("0/"), false); assert_eq!(is_valid_node_path("foo/bar/0"), false); assert_eq!(is_valid_node_path("-"), false); } } ================================================ FILE: src/gdscript/op.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! All of the operators recognized by GDScript. use serde::{Serialize, Deserialize}; /// Unary operators. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Serialize, Deserialize)] pub enum UnaryOp { BitNot, Negate, Not, } /// Binary operators. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Serialize, Deserialize)] pub enum BinaryOp { Times, Div, Mod, Add, Sub, LShift, RShift, BitAnd, BitXor, BitOr, LT, GT, Eq, NE, LE, GE, Is, In, And, Or, Cast, } /// Assignment operators. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Serialize, Deserialize)] pub enum AssignOp { Eq, Add, Sub, Times, Div, Mod, BitAnd, BitOr, } /// Ternary operators. /// /// There is only one ternary op in Godot: if conditionals. Hence, /// this struct is a singleton whose unique value represents ternary /// if conditions. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug)] pub struct TernaryOp; /// Information about an operator. #[derive(PartialEq, Eq, Clone, Hash, Debug)] pub struct OperatorInfo { /// The operator's precedence. Operators with higher precedence will /// bind more tightly. pub precedence: i32, /// The operator's name, as a string. pub name: &'static str, /// How spaces should be applied around an operator. pub padding: Padding, } /// This enum describes different mechanisms for providing padding /// around an operator. /// /// There are some operators in GDScript that require spaces around /// them, such as `is`. There are many more that, while surrounding /// spaces are not required, tend to be more readable if padding is /// added. Finally, there are some (especially unary operators) which /// simply do not require spaces for readability. /// /// An [operator's info](OperatorInfo) contains a [Padding] which /// describes how that operator would like to be spaced. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug)] pub enum Padding { /// The operator is ambivalent to padding. It may be added if needed /// for some other reason but is not required. NotRequired, /// The operator is more readable if spaces are added. It is not /// incorrect to omit spaces, but they should be added unless there /// is a good reason not to. Preferred, /// The operator requires spaces around it. Failing to include them /// may result in incorrect code. Required, } fn info(prec: i32, name: &'static str, padding: Padding) -> OperatorInfo { OperatorInfo { precedence: prec, name: name, padding: padding } } /// All of the operator types in this module can produce an /// [OperatorInfo] value describing their properties. This trait /// describes any type which can produce such an [OperatorInfo]. pub trait OperatorHasInfo { /// Produce [OperatorInfo]. fn op_info(&self) -> OperatorInfo; } impl OperatorHasInfo for UnaryOp { fn op_info(&self) -> OperatorInfo { match self { UnaryOp::BitNot => info(17, "~", Padding::NotRequired), UnaryOp::Negate => info(16, "-", Padding::NotRequired), UnaryOp::Not => info(6, "!", Padding::NotRequired), } } } impl OperatorHasInfo for BinaryOp { fn op_info(&self) -> OperatorInfo { match self { BinaryOp::Times => info(15, "*", Padding::Preferred), BinaryOp::Div => info(15, "/", Padding::Preferred), BinaryOp::Mod => info(15, "%", Padding::Preferred), BinaryOp::Add => info(14, "+", Padding::Preferred), BinaryOp::Sub => info(13, "-", Padding::Preferred), BinaryOp::LShift => info(12, "<<", Padding::Preferred), BinaryOp::RShift => info(12, ">>", Padding::Preferred), BinaryOp::BitAnd => info(11, "&", Padding::Preferred), BinaryOp::BitXor => info(10, "^", Padding::Preferred), BinaryOp::BitOr => info(9, "|", Padding::Preferred), BinaryOp::LT => info(8, "<", Padding::Preferred), BinaryOp::GT => info(8, ">", Padding::Preferred), BinaryOp::Eq => info(8, "==", Padding::Preferred), BinaryOp::NE => info(8, "!=", Padding::Preferred), BinaryOp::LE => info(8, "<=", Padding::Preferred), BinaryOp::GE => info(8, ">=", Padding::Preferred), BinaryOp::Is => info(18, "is", Padding::Required), BinaryOp::In => info(7, "in", Padding::Required), BinaryOp::And => info(5, "&&", Padding::Preferred), BinaryOp::Or => info(4, "||", Padding::Preferred), BinaryOp::Cast => info(2, "as", Padding::Preferred), } } } impl OperatorHasInfo for AssignOp { fn op_info(&self) -> OperatorInfo { let prec = 1; // All assignments are of the lowest precedence in GDScript let pad = Padding::Preferred; let name = match self { AssignOp::Eq => "=", AssignOp::Add => "+=", AssignOp::Sub => "-=", AssignOp::Times => "*=", AssignOp::Div => "/=", AssignOp::Mod => "%=", AssignOp::BitAnd => "&=", AssignOp::BitOr => "|=", }; info(prec, name, pad) } } impl OperatorHasInfo for TernaryOp { fn op_info(&self) -> OperatorInfo { info(3, "if-else", Padding::Required) } } ================================================ FILE: src/gdscript/pattern.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDScript patterns for use in pattern matching. //! //! This module defines a [datatype](Pattern) for representing //! patterns in the GDScript language, as well as [`Pattern::to_gd`] //! for converting to GDScript syntax. use crate::gdscript::literal::Literal; /// The type of GDScript patterns. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pattern { Literal(Literal), Var(String), Wildcard, BindingVar(String), Array(Vec, Wildcard), Dictionary(Vec<(Literal, Pattern)>, Wildcard), } /// This type is isomorphic to [`bool`] and indicates whether or not a /// wildcard was supplied to a pattern. #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub enum Wildcard { NoWildcard, Wildcard, } impl Pattern { /// Convert a GDScript pattern to a string. The result will contain /// valid GDScript syntax. pub fn to_gd(&self) -> String { match self { Pattern::Literal(lit) => lit.to_gd(), Pattern::Var(s) => s.clone(), Pattern::Wildcard => String::from("_"), // TODO Make sure to handle the case of a variable called _, as that's technically weirdly ambiguous here. Pattern::BindingVar(s) => format!("var {}", s), Pattern::Array(ptns, wild) => { let mut inner = ptns.iter().map(|ptn| ptn.to_gd()).collect::>(); if *wild == Wildcard::Wildcard { inner.push(String::from("..")); } format!("[{}]", inner.join(", ")) }, Pattern::Dictionary(d, wild) => { let mut inner = d.iter().map(|x| format!("{}: {}", x.0.to_gd(), x.1.to_gd())).collect::>(); if *wild == Wildcard::Wildcard { inner.push(String::from("..")); } format!("{{{}}}", inner.join(", ")) }, } } } impl From for bool { fn from(w: Wildcard) -> bool { w == Wildcard::Wildcard } } #[cfg(test)] mod tests { use super::*; #[test] fn atomic_patterns() { assert_eq!(Pattern::Literal(Literal::Int(3)).to_gd(), "3"); assert_eq!(Pattern::Var(String::from("var_name")).to_gd(), "var_name"); assert_eq!(Pattern::BindingVar(String::from("var_name")).to_gd(), "var var_name"); assert_eq!(Pattern::Wildcard.to_gd(), "_"); } #[test] fn compound_patterns() { let lit1 = Literal::Int(100); let lit2 = Literal::Int(200); let ptn1 = Pattern::Literal(Literal::Int(1)); let ptn2 = Pattern::Literal(Literal::Int(2)); assert_eq!(Pattern::Array(vec!(), Wildcard::NoWildcard).to_gd(), "[]"); assert_eq!(Pattern::Array(vec!(), Wildcard::Wildcard).to_gd(), "[..]"); assert_eq!(Pattern::Array(vec!(ptn1.clone()), Wildcard::NoWildcard).to_gd(), "[1]"); assert_eq!(Pattern::Array(vec!(ptn1.clone()), Wildcard::Wildcard).to_gd(), "[1, ..]"); assert_eq!(Pattern::Array(vec!(ptn1.clone(), ptn2.clone()), Wildcard::NoWildcard).to_gd(), "[1, 2]"); assert_eq!(Pattern::Array(vec!(ptn1.clone(), ptn2.clone()), Wildcard::Wildcard).to_gd(), "[1, 2, ..]"); assert_eq!(Pattern::Dictionary(vec!(), Wildcard::NoWildcard).to_gd(), "{}"); assert_eq!(Pattern::Dictionary(vec!(), Wildcard::Wildcard).to_gd(), "{..}"); assert_eq!(Pattern::Dictionary(vec!((lit1.clone(), ptn1.clone())), Wildcard::NoWildcard).to_gd(), "{100: 1}"); assert_eq!(Pattern::Dictionary(vec!((lit1.clone(), ptn1.clone())), Wildcard::Wildcard).to_gd(), "{100: 1, ..}"); assert_eq!(Pattern::Dictionary(vec!((lit1.clone(), ptn1.clone()), (lit2.clone(), ptn2.clone())), Wildcard::NoWildcard).to_gd(), "{100: 1, 200: 2}"); assert_eq!(Pattern::Dictionary(vec!((lit1.clone(), ptn1.clone()), (lit2.clone(), ptn2.clone())), Wildcard::Wildcard).to_gd(), "{100: 1, 200: 2, ..}"); } } ================================================ FILE: src/gdscript/spacing.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helper type for printing out [`Decl`](super::decl::Decl) instances //! with good spacing. use crate::util::group_by::group_by; use super::decl::{Decl, DeclF}; use std::fmt::{self, Write}; #[derive(Debug)] pub struct SpacedDeclPrinter { spacing: u32, indentation: u32, } impl SpacedDeclPrinter { /// A default [`SpacedDeclPrinter`], suitable for top-level /// pretty-printing. pub fn new() -> Self { Self::default() } /// Builder method which sets the spacing between non-grouped items /// in the declaration list. pub fn with_spacing(mut self, spacing: u32) -> Self { self.spacing = spacing; self } /// Builder method which sets the base indentation level for the /// declarations. pub fn with_indentation(mut self, indentation: u32) -> Self { self.indentation = indentation; self } /// Write the sequence of declarations, in order, inserting extra /// newlines where it makes sense for readability. pub fn write_gd<'a, W: Write, I: Iterator>(&self, w: &mut W, iter: I) -> Result<(), fmt::Error> { let mut first = true; for group in group_by(iter, |a, b| group_predicate(a, b)) { if !first { write_newlines(w, self.spacing)?; } first = false; for decl in group { decl.write_gd(w, self.indentation)?; } } Ok(()) } } /// Given two adjacent declarations, determine if they can be grouped. /// Two declarations can be grouped together if and only if both of /// the following are true: (a) The two declarations are of the same /// type, and (b) Both declarations are considered "short" /// declarations. /// /// The following declarations are considered "short": Variables, /// constants, signals, and `pass`. fn group_predicate(a: &Decl, b: &Decl) -> bool { #[allow(clippy::match_like_matches_macro)] // Looks much cleaner this way match (&a.value, &b.value) { (DeclF::VarDecl(_), DeclF::VarDecl(_)) => true, (DeclF::ConstDecl(_, _), DeclF::ConstDecl(_, _)) => true, (DeclF::SignalDecl(_, _), DeclF::SignalDecl(_, _)) => true, (DeclF::PassDecl, DeclF::PassDecl) => true, _ => false, } } /// Writes the given number of newlines to the `Write` object. fn write_newlines(w: &mut impl Write, count: u32) -> Result<(), fmt::Error> { let newlines = "\n".repeat(count as usize); write!(w, "{}", newlines) } impl Default for SpacedDeclPrinter { fn default() -> Self { SpacedDeclPrinter { spacing: 2, indentation: 0, } } } ================================================ FILE: src/gdscript/stmt.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDScript statements. //! //! This module defines a [datatype](Stmt) for representing statements //! in the GDScript language, as well as [`Stmt::write_gd`] for //! writing statements as GDScript syntax to a [`fmt::Write`] //! instance. use crate::gdscript::expr::Expr; use crate::gdscript::op::{self, AssignOp, OperatorHasInfo}; use crate::gdscript::pattern::Pattern; use crate::gdscript::indent; use crate::pipeline::source::{SourceOffset, Sourced}; use std::fmt; /// The type of GDScript statements. #[derive(Debug, Clone, PartialEq, Eq)] pub enum StmtF { /// An expression alone can stand as a statement. Expr(Expr), IfStmt(IfStmt), ForLoop(ForLoop), WhileLoop(WhileLoop), PassStmt, BreakStmt, ContinueStmt, MatchStmt(Expr, Vec<(Pattern, Vec)>), /// A variable declaration. Note that while GDScript allows variable /// declarations which do *not* assign a value, here we require that /// all declarations assign an initial value. VarDecl(String, Expr), ReturnStmt(Expr), /// An assignment. Note that GDScript restricts what can appear on /// the left-hand side of an assignment, whereas this type allows /// any [`Expr`]. Care must be taken to ensure that the left-hand /// side is syntactically valid. Assign(Box, AssignOp, Box), } /// GDScript statement with its source offset. See [`Sourced`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Stmt { pub value: StmtF, pub pos: SourceOffset, } /// The type of if statements, used in [`StmtF::IfStmt`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct IfStmt { pub if_clause: (Expr, Vec), pub elif_clauses: Vec<(Expr, Vec)>, pub else_clause: Option>, } /// The type of for loops, used in [`StmtF::ForLoop`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForLoop { pub iter_var: String, pub collection: Expr, pub body: Vec, } /// The type of while loops, used in [`StmtF::WhileLoop`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WhileLoop { pub condition: Expr, pub body: Vec, } /// Construct an `if` statement with no `elif` or `else` branches. pub fn if_then(cond: Expr, true_branch: Vec, pos: SourceOffset) -> Stmt { Stmt::new( StmtF::IfStmt(IfStmt { if_clause: (cond, true_branch), elif_clauses: vec!(), else_clause: None, }), pos, ) } /// Construct an `if` statement with an `else` branch but no `elif` /// branches. pub fn if_else(cond: Expr, true_branch: Vec, false_branch: Vec, pos: SourceOffset) -> Stmt { Stmt::new( StmtF::IfStmt(IfStmt { if_clause: (cond, true_branch), elif_clauses: vec!(), else_clause: Some(false_branch), }), pos, ) } /// General-purpose `if` statement constructor. /// /// Construct a sequence of statements which represents the branches. /// The first element of `cases` will be the `if_clause` of the /// statement, all remaining `cases` will be `elif_clauses`, and the /// `default` shall be `else_clause`. /// /// As a special corner case, if `cases` is empty, then `default` is /// returned unmodified. This is the only case in which more than one /// value might be returned. If `cases` is nonempty, then /// `if_branches` will always return a vector containing a single /// statement. pub fn if_branches(cases: Vec<(Expr, Vec)>, default: Vec, pos: SourceOffset) -> Vec { if cases.is_empty() { default } else { let if_clause = cases[0].clone(); let elif_clauses = cases[1..].to_vec(); vec!( Stmt::new( StmtF::IfStmt(IfStmt { if_clause, elif_clauses, else_clause: Some(default), }), pos, ) ) } } impl Stmt { /// A new `Stmt` at the given position in the source. pub fn new(value: StmtF, pos: SourceOffset) -> Stmt { Stmt { value, pos } } /// An expression, as a statement. pub fn expr(expr: Expr) -> Stmt { let pos = expr.pos; Stmt::new(StmtF::Expr(expr), pos) } /// A return statement. pub fn return_stmt(expr: Expr, pos: SourceOffset) -> Stmt { Stmt::new(StmtF::ReturnStmt(expr), pos) } /// Simple assignment to a given target. pub fn simple_assign(lhs: Expr, rhs: Expr, pos: SourceOffset) -> Stmt { Stmt::new(StmtF::Assign(Box::new(lhs), AssignOp::Eq, Box::new(rhs)), pos) } /// Declaration of a new variable. pub fn var_decl(var_name: String, value: Expr, pos: SourceOffset) -> Stmt { Stmt::new(StmtF::VarDecl(var_name, value), pos) } /// A `break` statement. pub fn break_stmt(pos: SourceOffset) -> Stmt { Stmt::new(StmtF::BreakStmt, pos) } /// A `continue` statement. pub fn continue_stmt(pos: SourceOffset) -> Stmt { Stmt::new(StmtF::ContinueStmt, pos) } /// A `pass` statement. pub fn pass_stmt(pos: SourceOffset) -> Stmt { Stmt::new(StmtF::PassStmt, pos) } /// Write the statement, as GDScript code, to the [`fmt::Write`] /// instance `w`. /// /// We are assumed to be at the indentation level `ind`, so that all /// lines in the result will be indented to that level. /// /// The writer `w` should currently be either empty or immediately /// after a newline. The statement will always end by printing a /// newline, making it suitable for writing a subsequent statement /// immediately after. pub fn write_gd(&self, w: &mut W, ind: u32) -> Result<(), fmt::Error> { indent(w, ind)?; match &self.value { StmtF::Expr(expr) => { writeln!(w, "{}", expr.to_gd()) } StmtF::IfStmt(IfStmt { if_clause, elif_clauses, else_clause }) => { writeln!(w, "if {}:", if_clause.0.to_gd())?; Stmt::write_gd_stmts(&if_clause.1, w, ind + 4)?; for clause in elif_clauses { indent(w, ind)?; writeln!(w, "elif {}:", clause.0.to_gd())?; Stmt::write_gd_stmts(&clause.1, w, ind + 4)?; } if let Some(else_clause) = else_clause { indent(w, ind)?; writeln!(w, "else:")?; Stmt::write_gd_stmts(else_clause, w, ind + 4)?; } Ok(()) } StmtF::PassStmt => writeln!(w, "pass"), StmtF::BreakStmt => writeln!(w, "break"), StmtF::ContinueStmt => writeln!(w, "continue"), StmtF::ForLoop(ForLoop { iter_var, collection, body }) => { writeln!(w, "for {} in {}:", iter_var, collection.to_gd())?; Stmt::write_gd_stmts(body, w, ind + 4) } StmtF::WhileLoop(WhileLoop { condition, body }) => { writeln!(w, "while {}:", condition.to_gd())?; Stmt::write_gd_stmts(body, w, ind + 4) } StmtF::MatchStmt(expr, clauses) => { writeln!(w, "match {}:", expr.to_gd())?; if clauses.is_empty() { // If you try to have an empty match body, you kinda deserve // the program to crash. But hey, I'm in a good mood, so // I'll handle the wonky corner case. :) indent(w, ind + 4)?; writeln!(w, "{}:", Pattern::Wildcard.to_gd())?; Stmt::write_gd_stmts(vec!(), w, ind + 8) } else { for (ptn, body) in clauses { indent(w, ind + 4)?; writeln!(w, "{}:", ptn.to_gd())?; Stmt::write_gd_stmts(body, w, ind + 8)?; } Ok(()) } } StmtF::VarDecl(name, expr) => { writeln!(w, "var {} = {}", name, expr.to_gd()) } StmtF::ReturnStmt(expr) => { writeln!(w, "return {}", expr.to_gd()) } StmtF::Assign(lhs, op, rhs) => { let info = op.op_info(); let lhs = lhs.to_gd(); let rhs = rhs.to_gd(); if info.padding == op::Padding::NotRequired { writeln!(w, "{}{}{}", lhs, info.name, rhs) } else { writeln!(w, "{} {} {}", lhs, info.name, rhs) } }, } } /// Write several statements in sequence, using [`Stmt::write_gd`]. /// /// If `iter` is empty, then `"pass\n"` will be written. pub fn write_gd_stmts<'a, W, I>(iter: I, w: &mut W, ind: u32) -> Result<(), fmt::Error> where W : fmt::Write, I : IntoIterator { let mut empty = true; for stmt in iter { stmt.write_gd(w, ind)?; empty = false; } if empty { Stmt::new(StmtF::PassStmt, SourceOffset::default()).write_gd(w, ind)?; } Ok(()) } /// Write the statement to a string, using [`Stmt::write_gd`]. /// /// # Panics /// /// This function panics if there is a write error to the string. If /// you wish to handle that case yourself, use [`Stmt::write_gd`] /// explicitly. pub fn to_gd(&self, ind: u32) -> String { let mut string = String::new(); self.write_gd(&mut string, ind).expect("Could not write to string in Stmt::to_gd"); string } } impl Sourced for Stmt { type Item = StmtF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &StmtF { &self.value } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::literal::Literal; use crate::pipeline::source::SourceOffset; fn assign(a: &Expr, op: AssignOp, b: &Expr) -> Stmt { s(StmtF::Assign(Box::new(a.clone()), op, Box::new(b.clone()))) } fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } #[test] fn expr_stmt() { assert_eq!(Stmt::expr(e(ExprF::Var(String::from("foobar")))).to_gd(0), "foobar\n"); assert_eq!(Stmt::expr(e(ExprF::from(9))).to_gd(0), "9\n"); } #[test] fn basic_indent() { assert_eq!(Stmt::expr(e(ExprF::Var(String::from("foobar")))).to_gd(0), "foobar\n"); assert_eq!(Stmt::expr(e(ExprF::Var(String::from("foobar")))).to_gd(4), " foobar\n"); assert_eq!(Stmt::expr(e(ExprF::Var(String::from("foobar")))).to_gd(8), " foobar\n"); } #[test] fn simple_stmts() { let expr = e(ExprF::from(1000)); assert_eq!(s(StmtF::VarDecl(String::from("var_name"), expr.clone())).to_gd(0), "var var_name = 1000\n"); assert_eq!(s(StmtF::ReturnStmt(expr.clone())).to_gd(0), "return 1000\n"); } #[test] fn if_stmt() { let cond1 = e(ExprF::Var(String::from("condition1"))); let cond2 = e(ExprF::Var(String::from("condition2"))); let cond3 = e(ExprF::Var(String::from("condition3"))); let stmt1 = Stmt::expr(e(ExprF::from(1))); let stmt2 = Stmt::expr(e(ExprF::from(2))); let stmt3 = Stmt::expr(e(ExprF::from(3))); let stmt4 = Stmt::expr(e(ExprF::from(4))); let stmt5 = Stmt::expr(e(ExprF::from(5))); let if1 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!()), elif_clauses: vec!(), else_clause: None })); assert_eq!(if1.to_gd(0), "if condition1:\n pass\n"); let if2 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone())), elif_clauses: vec!(), else_clause: None })); assert_eq!(if2.to_gd(0), "if condition1:\n 1\n"); let if3 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone(), stmt2.clone())), elif_clauses: vec!(), else_clause: None })); assert_eq!(if3.to_gd(0), "if condition1:\n 1\n 2\n"); let if4 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone(), stmt2.clone())), elif_clauses: vec!(), else_clause: Some(vec!()) })); assert_eq!(if4.to_gd(0), "if condition1:\n 1\n 2\nelse:\n pass\n"); let if5 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone(), stmt2.clone())), elif_clauses: vec!(), else_clause: Some(vec!(stmt3.clone())) })); assert_eq!(if5.to_gd(0), "if condition1:\n 1\n 2\nelse:\n 3\n"); let if6 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone(), stmt2.clone())), elif_clauses: vec!((cond2.clone(), vec!(stmt3.clone()))), else_clause: Some(vec!(stmt4.clone())) })); assert_eq!(if6.to_gd(0), "if condition1:\n 1\n 2\nelif condition2:\n 3\nelse:\n 4\n"); let if7 = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone(), stmt2.clone())), elif_clauses: vec!((cond2.clone(), vec!(stmt3.clone())), (cond3.clone(), vec!(stmt4.clone()))), else_clause: Some(vec!(stmt5.clone())) })); assert_eq!(if7.to_gd(0), "if condition1:\n 1\n 2\nelif condition2:\n 3\nelif condition3:\n 4\nelse:\n 5\n"); } #[test] fn nested_if() { let cond1 = e(ExprF::Var(String::from("condition1"))); let cond2 = e(ExprF::Var(String::from("condition2"))); let stmt1 = Stmt::expr(e(ExprF::from(1))); let stmt2 = Stmt::expr(e(ExprF::from(2))); let stmt3 = Stmt::expr(e(ExprF::from(3))); let stmt4 = Stmt::expr(e(ExprF::from(4))); let inner = s(StmtF::IfStmt(IfStmt { if_clause: (cond2.clone(), vec!(stmt2.clone(), stmt3.clone())), elif_clauses: vec!(), else_clause: None, })); let outer = s(StmtF::IfStmt(IfStmt { if_clause: (cond1.clone(), vec!(stmt1.clone(), inner, stmt4.clone())), elif_clauses: vec!(), else_clause: None, })); assert_eq!(outer.to_gd(0), "if condition1:\n 1\n if condition2:\n 2\n 3\n 4\n"); } #[test] fn for_loop() { let expr = e(ExprF::Var(String::from("collection"))); let stmt = Stmt::expr(e(ExprF::from(1))); let for1 = s(StmtF::ForLoop(ForLoop { iter_var: String::from("i"), collection: expr.clone(), body: vec!(), })); assert_eq!(for1.to_gd(0), "for i in collection:\n pass\n"); let for2 = s(StmtF::ForLoop(ForLoop { iter_var: String::from("i"), collection: expr.clone(), body: vec!(stmt.clone()), })); assert_eq!(for2.to_gd(0), "for i in collection:\n 1\n"); } #[test] fn while_loop() { let expr = e(ExprF::Var(String::from("condition"))); let stmt = Stmt::expr(e(ExprF::from(1))); let while1 = s(StmtF::WhileLoop(WhileLoop { condition: expr.clone(), body: vec!(), })); assert_eq!(while1.to_gd(0), "while condition:\n pass\n"); let while2 = s(StmtF::WhileLoop(WhileLoop { condition: expr.clone(), body: vec!(stmt.clone()), })); assert_eq!(while2.to_gd(0), "while condition:\n 1\n"); } #[test] fn match_stmt() { let expr = e(ExprF::Var(String::from("expr"))); let ptn1 = Pattern::Literal(Literal::Int(100)); let ptn2 = Pattern::Literal(Literal::Int(200)); let body1 = Stmt::expr(e(ExprF::Var(String::from("body1")))); let body2 = Stmt::expr(e(ExprF::Var(String::from("body2")))); let match1 = s(StmtF::MatchStmt(expr.clone(), vec!())); assert_eq!(match1.to_gd(0), "match expr:\n _:\n pass\n"); let match2 = s(StmtF::MatchStmt(expr.clone(), vec!((ptn1.clone(), vec!(body1.clone()))))); assert_eq!(match2.to_gd(0), "match expr:\n 100:\n body1\n"); let match3 = s(StmtF::MatchStmt(expr.clone(), vec!((ptn1.clone(), vec!(body1.clone())), (ptn2.clone(), vec!(body2.clone()))))); assert_eq!(match3.to_gd(0), "match expr:\n 100:\n body1\n 200:\n body2\n"); } #[test] fn assign_ops() { let a = e(ExprF::Var(String::from("a"))); let b = e(ExprF::Var(String::from("b"))); assert_eq!(assign(&a, AssignOp::Eq, &b).to_gd(0), "a = b\n"); assert_eq!(assign(&a, AssignOp::Add, &b).to_gd(0), "a += b\n"); assert_eq!(assign(&a, AssignOp::Sub, &b).to_gd(0), "a -= b\n"); assert_eq!(assign(&a, AssignOp::Times, &b).to_gd(0), "a *= b\n"); assert_eq!(assign(&a, AssignOp::Div, &b).to_gd(0), "a /= b\n"); assert_eq!(assign(&a, AssignOp::Mod, &b).to_gd(0), "a %= b\n"); assert_eq!(assign(&a, AssignOp::BitAnd, &b).to_gd(0), "a &= b\n"); assert_eq!(assign(&a, AssignOp::BitOr, &b).to_gd(0), "a |= b\n"); } } ================================================ FILE: src/graph/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Functions for working with //! [graphs](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)). //! //! The [`Graph`] datatype provides the core functionality for //! representing graphs. pub mod top_sort; pub mod tarjan; use std::collections::HashMap; use std::borrow::Borrow; use std::hash::Hash; // TODO Really, we should be storing the edge sets with some kind of // numerical index, rather than assuming the type T can be safely // cloned. /// A graph consists of nodes and edges. Here, we represent directed /// unlabeled multigraphs with loopback edges. /// /// That is, every edge has a head and a tail, there can be multiple /// edges with the same head and tail, and an edge can point from a /// node to itself. /// /// The type `T` of nodes must be [`Eq`] and [`Hash`] in order to /// perform any of the `Graph` operations on it. Generally, you will /// want it to have a sane [`Clone`] implementation as well, since a /// given node will need to be stored several times within the data /// structure. #[derive(Debug, Clone)] pub struct Graph { edges: HashMap>, } impl Graph where T : Eq + Hash { /// Given an iterator of edges, produce a [`Graph`]. Each element of /// the iterator shall be a pair, where the first term of the pair /// is the head of the edge and the second is the tail. pub fn from_edges(iter: I) -> Graph where T : Clone, I : Iterator { let mut edges = HashMap::new(); for (x, y) in iter { // Make sure the other node is present too. if !edges.contains_key(&y) { edges.insert(y.clone(), Vec::new()); } // Then put an entry for the edge let vec = edges.entry(x).or_insert_with(Vec::new); vec.push(y); } Graph { edges } } /// Produce an [empty /// graph](https://en.wikipedia.org/wiki/Empty_graph) containing the /// given node set. /// /// In the case of duplicate nodes, only the first will be kept. pub fn from_nodes(iter: I) -> Graph where I : Iterator { Graph { edges: iter.map(|x| (x, Vec::new())).collect() } } /// Produce an empty graph with no edges or nodes. pub fn new() -> Graph { Graph::default() } /// An iterator over the nodes of the graph, in an unspecified /// order. pub fn nodes(&self) -> impl Iterator { self.edges.keys() } /// Given a value `x`, get a reference to the unique node in the /// graph with is equal to `x`. If there is no such node, returns /// [`None`]. pub fn get_node(&self, x: &U) -> Option<&T> where U : Borrow { self.edges.get_key_value(x.borrow()).map(|(k, _)| k) } /// Returns whether or not the graph contains a node equal to `x`. pub fn has_node(&self, x: &U) -> bool where U : Borrow { self.get_node(x).is_some() } /// An iterator over all edges of the graph, in an unspecified /// order. pub fn all_edges(&self) -> impl Iterator { self.edges.iter().flat_map(|(x, ys)| ys.iter().map(move |y| (x, y))) } /// A vector over all of the outgoing edges from a given node, in an /// unspecified order. Returns [`None`] if the node does not exist. pub fn outgoing_edges(&self, node: impl Borrow) -> Option<&Vec> { self.edges.get(node.borrow()) } /// Add a new node to the graph. If there is already a node equal to /// `node` in the graph, this method does nothing. pub fn add_node(&mut self, node: T) { self.edges.entry(node).or_insert_with(Vec::new); } /// Add a new edge to the graph. Both `x` and `y` should already be /// in the graph, or the result is undefined. pub fn add_edge(&mut self, x: T, y: T) { let entry = self.edges.entry(x).or_insert_with(Vec::new); entry.push(y); } /// Add a new edge in the graph. If there is already an edge from /// `x` to `y`, do not add another. Both `x` and `y` should already /// be in the graph, or the result is undefined. pub fn add_edge_no_dup(&mut self, x: T, y: T) { if !self.has_edge(&x, &y) { self.add_edge(x, y); } } /// Check whether the graph has an edge from `x` to `y`. pub fn has_edge(&self, x: &U, y: &V) -> bool where U : Borrow, V : Borrow { match self.outgoing_edges(x.borrow()) { None => false, Some(vec) => vec.iter().any(|node| node == y.borrow()), } } /// Remove one edge from the graph pointing from `x` to `y`. If no /// such edge exists, then the graph is unchanged. pub fn remove_edge(&mut self, x: &U, y: &V) where U : Borrow, V : Borrow { if let Some(vec) = self.edges.get_mut(x.borrow()) { if let Some(pos) = vec.iter().position(|node| node == y.borrow()) { vec.swap_remove(pos); } } } /// The total number of nodes in the graph. pub fn node_count(&self) -> usize { self.edges.len() } /// The graph with all edges flipped. The resulting graph will have /// the same nodes as `self`, but every edge will have its head and /// tail swapped. pub fn transpose(&self) -> Graph where T : Clone { let mut graph = Graph::from_nodes(self.nodes().cloned()); for (x, y) in self.all_edges() { graph.add_edge(y.clone(), x.clone()); } graph } } impl Default for Graph { fn default() -> Self { Graph { edges: HashMap::default() } } } ================================================ FILE: src/graph/tarjan.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Implementation of [Tarjan's SCC Algorithm](https://en.wikipedia.org/wiki/Tarjan%E2%80%99s_strongly_connected_components_algorithm) //! //! Given a graph, we say that a nonempty set of nodes is a *strongly //! connected component* if, given any `u` and `v` in that set, there //! is a path in the graph from `u` to `v`. This module implements an //! algorithm to find all strongly connected components in a graph. use super::*; use std::collections::HashMap; use std::collections::HashSet; use std::cmp::min; use std::mem::swap; /// A strongly connected component is a nonempty set of nodes. #[derive(Clone, Debug)] pub struct SCC<'a, T>(pub HashSet<&'a T>); /// The result type of [`find_scc`]. #[derive(Clone, Debug)] pub struct SCCSummary<'a, T> { sccs: Vec>, scc_lookup: HashMap<&'a T, usize>, } struct Algorithm<'a, T> { graph: &'a Graph, sccs: Vec>, indices: HashMap<&'a T, usize>, lowlinks: HashMap<&'a T, usize>, index: usize, stack: Vec<&'a T>, current_scc: SCC<'a, T>, } impl<'a, T> PartialEq for SCC<'a, T> where T : Eq + Hash { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } impl<'a, T> Default for SCC<'a, T> { fn default() -> Self { SCC(HashSet::new()) } } impl<'a, T> SCCSummary<'a, T> where T : Eq + Hash { /// Given an ID value, get the strongly connected component /// associated to that ID. /// /// The ID values are integers from 0 up to (and excluding) the /// total number of SCCs. The order of ID values is unspecified. /// Given a node, the relevant ID value for the SCC can be obtained /// from [`SCCSummary::get_scc_id`], which this function serves as a spiritual /// inverse to. pub fn get_scc_by_id(&self, id: usize) -> Option<&SCC<'a, T>> { self.sccs.get(id) } /// Given a node, get the ID value of the SCC containing that node. pub fn get_scc_id(&self, node: &'a T) -> Option { self.scc_lookup.get(node).copied() } /// Given a node, get the SCC containing that node. pub fn get_scc(&self, node: &'a T) -> Option<&SCC<'a, T>> { self.scc_lookup.get(node).map(|idx| &self.sccs[*idx]) } /// Return whether two nodes are in the same SCC or not. /// /// If either node is not in the graph, returns false. pub fn is_in_same(&self, a: &'a T, b: &'a T) -> bool { match self.get_scc(a) { None => false, Some(SCC(nodes)) => nodes.contains(b), } } /// Return the total number of SCCs. /// /// The valid ID values for [`SCCSummary::get_scc_by_id`] range from 0 up to /// (and excluding) [`SCCSummary::count()`]. pub fn count(&self) -> usize { self.sccs.len() } } impl<'a, T> Algorithm<'a, T> where T : Eq + Hash { fn new(graph: &'a Graph) -> Algorithm<'a, T> { Algorithm { graph: graph, sccs: Vec::new(), indices: HashMap::new(), lowlinks: HashMap::new(), index: 0, stack: Vec::new(), current_scc: SCC::default(), } } fn strongconnect(&mut self, v: &'a T) { self.indices.insert(v, self.index); self.lowlinks.insert(v, self.index); self.index += 1; self.stack.push(v); for w in self.graph.outgoing_edges(v).expect("Node not found") { if !self.indices.contains_key(w) { self.strongconnect(w); let a = *self.lowlinks.get(w).expect("No lowlink for w"); let b = *self.lowlinks.get(v).expect("No lowlink for v"); self.lowlinks.insert(v, min(a, b)); } else if self.stack.iter().any(|x| x == &w) { let a = *self.indices.get(w).expect("No index for w"); let b = *self.lowlinks.get(v).expect("No lowlink for v"); self.lowlinks.insert(v, min(a, b)); } } if self.indices.get(v) == self.lowlinks.get(v) { while { let w = self.stack.pop().expect("Ran out of stack in strongconnect"); self.current_scc.0.insert(w); w != v } {} let mut curr = SCC::default(); swap(&mut curr, &mut self.current_scc); self.sccs.push(curr); } } } impl<'a, T> From> for SCCSummary<'a, T> where T : Eq + Hash { fn from(alg: Algorithm<'a, T>) -> SCCSummary<'a, T> { let sccs = alg.sccs; let mut scc_lookup = HashMap::new(); for (idx, SCC(nodes)) in sccs.iter().enumerate() { for node in nodes { scc_lookup.insert(*node, idx); } } SCCSummary { sccs, scc_lookup } } } /// Given a graph, identify all of its strongly connected components. pub fn find_scc(graph: &Graph) -> SCCSummary<'_, T> where T : Eq + Hash { let mut alg = Algorithm::new(graph); for v in graph.nodes() { if !alg.indices.contains_key(v) { alg.strongconnect(v); } } alg.into() } /// Given a graph, produce a graph where the nodes are SCCs. /// /// The `summary` argument must be equivalent to `find_scc(graph)`. /// /// The resulting graph contains nodes from 0 up to (and excluding) /// `summary.count()`. The graph contains no loopback edges and no /// duplicate edges. Given two SCCs `u` and `v`, the graph has an edge /// from `u` to `v` iff there is a path from some node in `u` to some /// node in `v`. pub fn build_scc_graph<'a, 'b, T>(graph: &'a Graph, summary: &'b SCCSummary<'a, T>) -> Graph where T : Eq + Hash { let mut new_graph = Graph::from_nodes(0..summary.count()); for (x, y) in graph.all_edges() { let xscc = summary.get_scc_id(x).expect("Node not found in SCCSummary"); let yscc = summary.get_scc_id(y).expect("Node not found in SCCSummary"); if xscc != yscc { new_graph.add_edge_no_dup(xscc, yscc); } } new_graph } #[cfg(test)] mod tests { use super::*; #[test] fn scc_test_isolated() { let graph = Graph::from_nodes(vec!(1, 2, 4, 3).into_iter()); let result = find_scc(&graph); assert_eq!(result.count(), 4); assert!(!result.is_in_same(&1, &2)); assert!(!result.is_in_same(&1, &3)); assert!(!result.is_in_same(&1, &4)); assert!(!result.is_in_same(&2, &3)); assert!(!result.is_in_same(&2, &4)); } #[test] fn scc_test_path() { let graph = Graph::from_edges(vec!((1, 2), (2, 3), (3, 4)).into_iter()); let result = find_scc(&graph); assert_eq!(result.count(), 4); assert!(!result.is_in_same(&1, &2)); assert!(!result.is_in_same(&1, &3)); assert!(!result.is_in_same(&1, &4)); assert!(!result.is_in_same(&2, &3)); assert!(!result.is_in_same(&2, &4)); } #[test] fn scc_test_cycle_rhs() { let graph = Graph::from_edges(vec!((1, 2), (2, 3), (3, 4), (4, 2)).into_iter()); let result = find_scc(&graph); assert_eq!(result.count(), 2); assert!(!result.is_in_same(&1, &2)); assert!(!result.is_in_same(&1, &3)); assert!(!result.is_in_same(&1, &4)); assert!(result.is_in_same(&2, &3)); assert!(result.is_in_same(&2, &4)); } #[test] fn scc_test_cycle_lhs() { let graph = Graph::from_edges(vec!((1, 2), (2, 3), (3, 4), (3, 1)).into_iter()); let result = find_scc(&graph); assert_eq!(result.count(), 2); assert!(result.is_in_same(&1, &2)); assert!(result.is_in_same(&1, &3)); assert!(!result.is_in_same(&1, &4)); assert!(result.is_in_same(&2, &3)); assert!(!result.is_in_same(&2, &4)); } #[test] fn scc_test_one_big_cycle() { let graph = Graph::from_edges(vec!((1, 2), (2, 3), (3, 4), (4, 1)).into_iter()); let result = find_scc(&graph); assert_eq!(result.count(), 1); } #[test] fn scc_test_two_connected_cycles() { let graph = Graph::from_edges(vec!((1, 2), (2, 3), (3, 4), (4, 5), (3, 1), (5, 3)).into_iter()); let result = find_scc(&graph); assert_eq!(result.count(), 1); } } ================================================ FILE: src/graph/top_sort.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Topological sorting of a graph. use super::*; use std::collections::HashSet; /// Error type produced if [`top_sort()`] finds a cycle. #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct CycleInTopSortError {} /// Any directed acyclic graph can be topologically sorted. A valid /// topological sorting of an acyclic graph is a total order of the /// nodes in that graph such that, for any edge from `u` to `v` in the /// graph, `u <= v` in the total order. /// /// This function finds some valid topological sorting of the nodes in /// the graph. If there is a cycle in the graph, then an error is /// returned. pub fn top_sort(graph: &Graph) -> Result, CycleInTopSortError> where T : Eq + Hash { let nodes: HashSet<_> = graph.nodes().collect(); let mut visited: HashSet<&T> = HashSet::new(); let mut result = Vec::new(); while visited.len() < nodes.len() { match nodes.iter().find(|node| { // To be a valid candidate, every outgoing node must be in visited. !visited.contains(*node) && graph.outgoing_edges(**node).expect("Node not found").iter().all(|out| { visited.contains(out) }) }) { None => { return Err(CycleInTopSortError {}) } Some(node) => { result.push(*node); visited.insert(node); } } } result.reverse(); Ok(result) } #[cfg(test)] mod tests { use super::*; #[test] fn test_top_sort_1() { // There's only one topological sort possible. let graph = Graph::from_edges(vec!((0, 1), (1, 2), (2, 3)).into_iter()); let result = top_sort(&graph); assert_eq!(result, Ok(vec!(&0, &1, &2, &3))); } #[test] fn test_top_sort_2() { // There's only one topological sort possible. let graph = Graph::from_edges(vec!((0, 1), (1, 2), (0, 2)).into_iter()); let result = top_sort(&graph); assert_eq!(result, Ok(vec!(&0, &1, &2))); } #[test] fn test_top_sort_3() { // Two options; as long as it's one of them, we're happy. let graph = Graph::from_edges(vec!((0, 1), (1, 2), (0, 3), (3, 2)).into_iter()); let result = top_sort(&graph).expect("No topological sort found"); assert_eq!(result[0], &0); assert_eq!(result[3], &2); } #[test] fn test_top_sort_4() { // No topological sort. let graph = Graph::from_edges(vec!((0, 1), (1, 2), (2, 3), (3, 1)).into_iter()); let result = top_sort(&graph); assert_eq!(result, Err(CycleInTopSortError {})); } } ================================================ FILE: src/ir/access_type.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the [`AccessType`] enum, for describing methods of //! accessing a local variable. use serde::{Serialize, Deserialize}; use crate::util::lattice::Lattice; /// `AccessType` describes the different ways we can access a variable. /// /// Note that, generally speaking, an `AccessType` should be regarded /// as context-sensitive. That is, an access type of /// [`None`](AccessType::None) does *not* necessarily mean that the /// variable is irrelevant to the whole program; it simply means that /// the variable is never accessed in the scope we're worried about. /// /// These possibilities do *not* form a total ordering, but they do /// form a [lattice](crate::util::lattice::Lattice) as follows. /// /// ```text /// ClosedRW /// / \ /// RW ClosedRead /// \ / /// Read /// | /// None /// ``` /// /// This lattice is realized by the [`AccessType::meet`] and /// [`AccessType::join`] functions. /// /// The logic for the lattice is as follows. If a variable is accessed /// in two ways, `a` and `b` (both `AccessType` values), then we need /// at least `AccessType::join(a, b)` control over the variable to be /// able to implement both accesses correctly. For most access types, /// this is straightforward: if a variable is read (`Read`) and /// written to (`RW`) but never in a closure, then we need `RW`. If a /// variable is read in (`ClosedRead`) and out (`Read`) of a closure /// but never written to, then we need `ClosedRead`. The one slightly /// more provocative corner case is when a variable is written to /// outside of a closure (`RW`) and read within one (`ClosedRead`). /// This is equivalent in power to writing to a variable within a /// closure (`ClosedRW`), since in both cases we will need an explicit /// cell wrapper around the variable to implement the read-write /// correctly. /// /// Note that, for the purposes of `AccessType`, we only care about /// actual modifications to the *value* of the variable. Internal /// mutability, such as modifying the *fields* of an instance stored /// in a variable, are irrelevant. That is, /// /// ```text /// var x = 1 /// x = 2 /// var y = [1] /// y[0] = 2 /// ``` /// /// In this example, `x` requires `RW` as the variable is directly /// written to. However, `y` only requires `Read`, because the /// variable, while internally mutated, it never directly rewritten. /// We draw this distinction because `AccessType` is intended for /// determining what sort of closure we need to construct for a /// variable for lambda purposes, and a closure does not care about /// internal mutability, only the top-level value and how constant it /// remains. #[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize)] pub enum AccessType { /// The variable in question is never accessed in the relevant /// scope. None, /// The variable is read *directly* in the relevant scope. Read, /// The variable is written to *directly* in the relevant scope. /// /// This includes direct assignment to the variable itself or to an /// instance variable *on* this variable. Nested assignment to an /// instance variable of an instance variable (or deeper) is /// excluded specifically. This condition is included so as to /// ensure that COW data structures such as Vector behave correctly /// inside closures. Specifically, COW structures should be wrapped /// in cells if a slot on them is ever changed, not just if the /// variable itself is assigned to. RW, /// The variable is read within a closure in the relevant scope. ClosedRead, /// The variable is written to within a closure in the relevant /// scope. ClosedRW, } // TODO We could get PartialOrd on this (not the derived instance but a custom one). impl AccessType { /// Given an access type which occurs inside of a lambda, convert it /// to the access type observed from outside the lambda. This method /// converts `Read` into `ClosedRead` and `RW` into `ClosedRW`, /// leaving all other inputs alone. /// /// # Examples /// /// ``` /// # use gdlisp::ir::access_type::AccessType; /// assert_eq!(AccessType::None.closed(), AccessType::None); /// assert_eq!(AccessType::Read.closed(), AccessType::ClosedRead); /// assert_eq!(AccessType::RW.closed(), AccessType::ClosedRW); /// assert_eq!(AccessType::ClosedRead.closed(), AccessType::ClosedRead); /// assert_eq!(AccessType::ClosedRW.closed(), AccessType::ClosedRW); /// ``` pub fn closed(&self) -> AccessType { match *self { AccessType::None => AccessType::None, AccessType::Read | AccessType::ClosedRead => AccessType::ClosedRead, AccessType::RW | AccessType::ClosedRW => AccessType::ClosedRW, } } /// Returns whether the access type requires a cell. /// /// Only [`AccessType::ClosedRW`] requires a cell. Any variables /// which are never used within a closure are [`AccessType::RW`] or /// less, and those variables can simply be written to directly, as /// the standard Godot local variable semantics will handle this /// case correctly. Any variables which are read-only (possibly /// within a closure) are at most [`AccessType::ClosedRead`], and /// those variables can likewise be read directly, since they'll /// never be changed, so Godot's built-in pointer copying is /// harmless. /// /// However, if a variable is written to within a closure, or if a /// variable is written to and (separately) read within a closure, /// then the standard process of closing around the variable will /// implicitly copy the pointer to the variable, which means that /// changes to the original will not be reflected in the closure and /// vice versa. Hence, in this situation, we need to wrap the local /// variable in a cell. `Cell` is a GDLisp standard library class /// which has a single field and no methods. Its only purpose is /// adding one layer of indirection to avoid this implicit copy /// problem. pub fn requires_cell(&self) -> bool { *self == AccessType::ClosedRW } /// Returns whether or not the access type involves modifying the /// variable. [`AccessType::ClosedRW`] and [`AccessType::RW`] /// involves pub fn is_written_to(&self) -> bool { *self == AccessType::ClosedRW || *self == AccessType::RW } } impl Lattice for AccessType { /// The least-upper-bound of `a` and `b`, under the lattice /// described in [`AccessType`]. fn join(self, b: AccessType) -> AccessType { if self == AccessType::None { return b; } if b == AccessType::None { return self; } if self == AccessType::Read { return b; } if b == AccessType::Read { return self; } if self == b { return self; } AccessType::ClosedRW } /// The greatest-lower-bound of `a` and `b`, under the lattice /// described in [`AccessType`]. fn meet(self, b: AccessType) -> AccessType { if self == AccessType::ClosedRW { return b; } if b == AccessType::ClosedRW { return self; } if self == b { return self; } if self == AccessType::None { return self; } if b == AccessType::None { return b; } AccessType::Read } } ================================================ FILE: src/ir/arglist/constructor.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`ConstructorArgList`] type, the type of argument //! lists which allow for instance variables to be initialized //! directly from them. use super::error::{ArgListParseError, ArgListParseErrorF}; use super::simple::SimpleArgList; use super::general::{GeneralArgList, GeneralArg}; use super::parser; use crate::sxp::ast::AST; use crate::compile::names::NameTrans; use crate::compile::names::generator::NameGenerator; use crate::pipeline::source::SourceOffset; use crate::gdscript::arglist::ArgList as GDArgList; use std::borrow::Borrow; use std::convert::TryFrom; /// A constructor argument list consists of only required arguments, /// similar to [`SimpleArgList`]. However, some of the arguments can /// be marked as instance fields. These will be assigned to the /// corresponding instance variable on the class at runtime. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ConstructorArgList { /// The list of required arguments, together with whether or not /// they've been marked as instance variables. pub args: Vec<(String, bool)>, } impl ConstructorArgList { /// Converts the argument list into a GDScript argument list, using /// the given name generator to produce unique names, similar to /// [`ArgList::into_gd_arglist`](super::ordinary::ArgList::into_gd_arglist). /// /// The second return value of this function includes, in addition /// to the name translations, whether or not each argument is marked /// as a constructor instance field. pub fn into_gd_arglist(self, gen: &mut impl NameGenerator) -> (GDArgList, Vec<(NameTrans, bool)>) { let (args, instance_fields): (Vec<_>, Vec<_>) = self.args.into_iter().unzip(); let arglist = SimpleArgList { args }; let (gd_arglist, trans) = arglist.into_gd_arglist(gen); let names: Vec<(NameTrans, bool)> = trans.into_iter().zip(instance_fields.into_iter()).collect(); (gd_arglist, names) } /// Returns true iff any arguments are marked as instance fields. pub fn has_any_instance_fields(&self) -> bool { self.args.iter().any(|(_, b)| *b) } /// An iterator over all variable names, together with whether or /// not they are instance variables, mentioned in the argument list, /// in order. pub fn iter_vars(&self) -> impl Iterator { self.args.iter().map(|(name, field)| (name.borrow(), *field)) } /// An iterator over all variable names, together with whether or /// not they are instance variables, as a mutable iterator. pub fn iter_vars_mut(&mut self) -> impl Iterator { self.args.iter_mut() } /// Attempts to parse a constructor argument list from the iterator. /// A constructor argument list can contain ordinary names, just /// like a [`SimpleArgList`], but those names can also be written in /// one of the following three equivalent forms. /// /// ```text /// @foobar /// self:foobar /// (access-slot self foobar) /// ``` /// /// In that case, the argument will be marked as a constructor /// instance field argument. pub fn parse<'a>(args: impl IntoIterator, pos: SourceOffset) -> Result { parser::parse(args).and_then(|arglist| { ConstructorArgList::try_from(arglist).map_err(|err| ArgListParseError::new(err, pos)) }) } /// The length of the argument list. pub fn len(&self) -> usize { self.args.len() } /// Whether the argument list consists of zero arguments. pub fn is_empty(&self) -> bool { self.args.is_empty() } } impl From for GeneralArgList { fn from(arglist: ConstructorArgList) -> GeneralArgList { let required_args: Vec<_> = arglist.args.into_iter().map(|(name, is_instance_field)| { GeneralArg { name, is_instance_field } }).collect(); GeneralArgList { required_args: required_args, optional_args: vec!(), rest_arg: None, } } } impl TryFrom for ConstructorArgList { type Error = ArgListParseErrorF; fn try_from(arglist: GeneralArgList) -> Result { if arglist.optional_args.is_empty() && arglist.rest_arg.is_none() { let required_args: Vec<_> = arglist.required_args.into_iter().map(|x| { (x.name, x.is_instance_field) }).collect(); Ok(ConstructorArgList { args: required_args }) } else { Err(ArgListParseErrorF::ConstructorArgListExpected) } } } ================================================ FILE: src/ir/arglist/error.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides [`ArgListParseError`], the type of argument list parse //! errors. use crate::sxp::ast::AST; use crate::pipeline::source::{Sourced, SourceOffset}; use std::fmt; use std::error::Error; /// `ArgListParseErrorF` describes the types of errors that can occur /// when parsing an [`AST`] argument list. #[derive(Debug, Clone, Eq, PartialEq)] pub enum ArgListParseErrorF { /// An argument of some specific type was expected but something /// else was provided. (TODO Remove this in favor of more specific /// errors) InvalidArgument(AST), /// An `@` (or `self:`) argument was found in a non-constructor /// context. BadSelf(AST), /// An `&` directive was provided but the name was unknown. UnknownDirective(String), /// An `&` directive appeared in the wrong place in an argument /// list, such as attempting to specify `&opt` arguments after /// `&rest`. DirectiveOutOfOrder(String), /// A simple argument list with no directives was expected, but /// directives were used. SimpleArgListExpected, /// A simple (unmodified) argument was expected, but an instance /// field was named. SimpleArgExpected, /// A constructor argument list was expected, but directives were /// used. ConstructorArgListExpected, } /// An [`ArgListParseErrorF`] together with [`SourceOffset`] data. #[derive(Debug, Clone, Eq, PartialEq)] pub struct ArgListParseError { pub value: ArgListParseErrorF, pub pos: SourceOffset, } impl ArgListParseError { pub fn new(value: ArgListParseErrorF, pos: SourceOffset) -> ArgListParseError { ArgListParseError { value, pos } } } impl fmt::Display for ArgListParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.value { ArgListParseErrorF::InvalidArgument(ast) => { write!(f, "Invalid arglist argument {}", ast) } ArgListParseErrorF::BadSelf(_) => { write!(f, "'@' arguments are not supported here") } ArgListParseErrorF::UnknownDirective(s) => { write!(f, "Unknown arglist directive {}", s) } ArgListParseErrorF::DirectiveOutOfOrder(s) => { write!(f, "Arglist directive appeared out of order {}", s) } ArgListParseErrorF::SimpleArgListExpected => { write!(f, "Only simple arglists are allowed in this context") } ArgListParseErrorF::SimpleArgExpected => { write!(f, "Only simple arguments (not instance variables) are allowed in this context") } ArgListParseErrorF::ConstructorArgListExpected => { write!(f, "Only constructor arglists are allowed in this context") } } } } impl Sourced for ArgListParseError { type Item = ArgListParseErrorF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &ArgListParseErrorF { &self.value } } impl Error for ArgListParseError {} ================================================ FILE: src/ir/arglist/general.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`GeneralArgList`] type, for the most general type of //! argument list available. use crate::sxp::ast::{ASTF, AST}; use crate::ir::special_form::access_slot::AccessSlotSyntax; use super::vararg::VarArg; use super::error::{ArgListParseError, ArgListParseErrorF}; /// `GeneralArgList` is the largest argument list type available in /// this crate. It is not used directly in GDLisp and is instead /// converted to one of the several more restrictive types after /// parsing. Every single argument list in GDLisp is capable of being /// converted losslessly to `GeneralArgList`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct GeneralArgList { /// The list of required arguments. pub required_args: Vec, /// The list of optional arguments. pub optional_args: Vec, /// The "rest" argument. pub rest_arg: Option<(GeneralArg, VarArg)>, } /// An argument in a [`GeneralArgList`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct GeneralArg { pub name: String, pub is_instance_field: bool, } impl GeneralArgList { /// An empty argument list, taking no arguments and accepting no /// "rest" argument. pub fn empty() -> GeneralArgList { GeneralArgList { required_args: vec!(), optional_args: vec!(), rest_arg: None, } } } impl GeneralArg { /// A simple argument, with no instance field modifiers. pub fn simple(name: String) -> GeneralArg { GeneralArg { name, is_instance_field: false } } /// Parse the argument from an AST of the form `name` or /// `(access-slot self name)`. Any other form will be rejected with /// an error. pub fn parse(ast: &AST) -> Result { if let Some(name) = ast.as_symbol_ref() { // Ordinary argument Ok(GeneralArg { name: name.to_owned(), is_instance_field: false, }) } else { // Try to parse as `self:foobar`. let AccessSlotSyntax { object, slot_name } = parse_access_slot(ast)?; verify_self(object)?; Ok(GeneralArg { name: slot_name, is_instance_field: true, }) } } /// Returns the name, if there are no adornments or modifiers /// applied. Returns an error otherwise. This is the retraction of /// [`GeneralArg::simple`]. pub fn into_simple_name(self) -> Result { if self.is_instance_field { Err(ArgListParseErrorF::SimpleArgExpected) } else { Ok(self.name) } } } /// Equivalent to [`AccessSlotSyntax::parse_ast`], but with the error /// type converted to [`ArgListParseError`]. The original error's /// [`SourceOffset`](crate::pipeline::source::SourceOffset) will be /// preserved. fn parse_access_slot(ast: &AST) -> Result, ArgListParseError> { AccessSlotSyntax::parse_ast(ast) .map_err(|err| ArgListParseError::new(ArgListParseErrorF::InvalidArgument(ast.clone()), err.pos)) } /// Verifies that the AST is literally the symbol `self`. fn verify_self(ast: &AST) -> Result<(), ArgListParseError> { if ast.value == ASTF::symbol(String::from("self")) { Ok(()) } else { Err(ArgListParseError::new(ArgListParseErrorF::InvalidArgument(ast.clone()), ast.pos)) } } ================================================ FILE: src/ir/arglist/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Argument list types as supported by the IR. //! //! See [`crate::gdscript::arglist`] for the companion module on the //! GDScript side. pub mod constructor; pub mod error; pub mod general; pub mod ordinary; pub mod parser; pub mod simple; pub mod vararg; // TODO Split these tests up into the new submodules. #[cfg(test)] mod tests { use super::*; use ordinary::ArgList; use error::ArgListParseError; use vararg::VarArg; use crate::AST_PARSER; use crate::sxp::ast::AST; use crate::sxp::dotted::DottedExpr; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::names::NameTrans; use crate::gdscript::arglist::ArgList as GDArgList; use crate::pipeline::source::SourceOffset; use std::convert::TryInto; fn parse_ast(input: &str) -> AST { AST_PARSER.parse(input).unwrap() } fn parse_arglist(input: &str) -> Result { let ast = parse_ast(input); let dotted: Vec<_> = DottedExpr::new(&ast).try_into().unwrap(); ArgList::parse(dotted, SourceOffset(0)) } fn arglist(req: Vec<&str>, opt: Vec<&str>, rest: Option<(&str, VarArg)>) -> ArgList { ArgList { required_args: req.into_iter().map(|x| x.to_owned()).collect(), optional_args: opt.into_iter().map(|x| x.to_owned()).collect(), rest_arg: rest.map(|(x, y)| (x.to_owned(), y)), } } fn gdarglist(req: Vec<&str>) -> GDArgList { GDArgList::required(req.into_iter().map(|x| x.to_owned()).collect()) } fn into_gd(args: ArgList) -> (GDArgList, Vec) { let mut tmp = FreshNameGenerator::new(vec!()); args.into_gd_arglist(&mut tmp) } #[test] fn test_parsing() { assert_eq!(parse_arglist("()").unwrap(), arglist(vec!(), vec!(), None)); assert_eq!(parse_arglist("(a)").unwrap(), arglist(vec!("a"), vec!(), None)); assert_eq!(parse_arglist("(a b)").unwrap(), arglist(vec!("a", "b"), vec!(), None)); assert_eq!(parse_arglist("(a b &opt c)").unwrap(), arglist(vec!("a", "b"), vec!("c"), None)); assert_eq!(parse_arglist("(a &rest rest)").unwrap(), arglist(vec!("a"), vec!(), Some(("rest", VarArg::RestArg)))); assert_eq!(parse_arglist("(a b c &opt d &rest e)").unwrap(), arglist(vec!("a", "b", "c"), vec!("d"), Some(("e", VarArg::RestArg)))); assert_eq!(parse_arglist("(a b c &opt d e &rest f)").unwrap(), arglist(vec!("a", "b", "c"), vec!("d", "e"), Some(("f", VarArg::RestArg)))); assert_eq!(parse_arglist("(a b c &opt d e &arr f)").unwrap(), arglist(vec!("a", "b", "c"), vec!("d", "e"), Some(("f", VarArg::ArrArg)))); assert_eq!(parse_arglist("(a b c &opt d e)").unwrap(), arglist(vec!("a", "b", "c"), vec!("d", "e"), None)); } #[test] fn test_invalid_parse() { assert!(parse_arglist("(&silly-name)").is_err()); assert!(parse_arglist("(&opt a &opt b)").is_err()); assert!(parse_arglist("(&rest a &opt b)").is_err()); assert!(parse_arglist("(&arr a &opt b)").is_err()); assert!(parse_arglist("(&rest a &rest b)").is_err()); assert!(parse_arglist("(&rest a b)").is_err()); assert!(parse_arglist("(&rest a &arr b)").is_err()); assert!(parse_arglist("(&arr a &rest b)").is_err()); } #[test] fn test_arglist_gen() { assert_eq!(into_gd(arglist(vec!(), vec!(), None)).0, gdarglist(vec!())); assert_eq!(into_gd(arglist(vec!("a"), vec!(), None)).0, gdarglist(vec!("a_0"))); assert_eq!(into_gd(arglist(vec!("a", "b"), vec!(), None)).0, gdarglist(vec!("a_0", "b_1"))); assert_eq!(into_gd(arglist(vec!("a"), vec!("b"), None)).0, gdarglist(vec!("a_0", "b_1"))); assert_eq!(into_gd(arglist(vec!("a"), vec!("b"), Some(("r", VarArg::RestArg)))).0, gdarglist(vec!("a_0", "b_1", "r_2"))); assert_eq!(into_gd(arglist(vec!("a"), vec!("b"), Some(("r", VarArg::ArrArg)))).0, gdarglist(vec!("a_0", "b_1", "r_2"))); } // TODO Test constructor and simple arglists } ================================================ FILE: src/ir/arglist/ordinary.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the ordinary argument list type [`ArgList`] for //! module-level functions and for lambda expressions. use crate::sxp::ast::AST; use crate::compile::names::{self, NameTrans}; use crate::compile::names::generator::NameGenerator; use crate::compile::symbol_table::function_call::FnSpecs; use crate::gdscript::arglist::ArgList as GDArgList; use crate::pipeline::source::SourceOffset; use super::error::{ArgListParseError, ArgListParseErrorF}; use super::vararg::VarArg; use super::general::{GeneralArg, GeneralArgList}; use super::parser; use std::convert::TryFrom; use std::borrow::Borrow; /// An argument list in GDLisp consists of a sequence of zero or more /// required arguments, followed by zero or more optional arguments, /// followed by (optionally) a "rest" argument. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArgList { /// The list of required argument names. pub required_args: Vec, /// The list of optional argument names. Note that optional /// arguments in GDLisp always default to `nil`, so no default value /// is explicitly mentioned here. pub optional_args: Vec, /// The "rest" argument. If present, this indicates the name of the /// argument and the type of "rest" argument. pub rest_arg: Option<(String, VarArg)>, } impl ArgList { /// An empty argument list, taking no required or optional arguments /// and having no "rest" argument. pub fn empty() -> ArgList { ArgList { required_args: vec!(), optional_args: vec!(), rest_arg: None, } } /// An argument list consisting only of a single [`VarArg::RestArg`] /// argument with a default name. #[deprecated(note="Directly construct the arglist with an explicit rest name instead")] pub fn rest() -> ArgList { ArgList { required_args: vec!(), optional_args: vec!(), rest_arg: Some((String::from("rest-arg"), VarArg::RestArg)), } } /// An argument list consisting only of required arguments with the /// given names. pub fn required(args: Vec) -> ArgList { ArgList { required_args: args, optional_args: vec!(), rest_arg: None, } } /// Converts an [`FnSpecs`] to an [`ArgList`] with dummy names for /// the variables. /// /// The names of the generated arguments may change and should not /// be relied upon; only the shape and lengths should be considered /// stable. pub fn from_specs(specs: FnSpecs) -> ArgList { // Uses dummy names for variables. let required_args = (0..specs.required).into_iter().map(|i| format!("required_arg{}", i)).collect(); let optional_args = (0..specs.optional).into_iter().map(|i| format!("optional_arg{}", i)).collect(); let rest_arg = specs.rest.map(|arg| (String::from("rest_arg"), arg)); ArgList { required_args, optional_args, rest_arg } } /// Parse an argument list from an iterator of `AST` values. Returns /// either the [`GeneralArgList`] or an appropriate error. pub fn parse<'a>(args: impl IntoIterator, pos: SourceOffset) -> Result { let general_arglist = parser::parse(args)?; ArgList::try_from(general_arglist).map_err(|err| ArgListParseError::new(err, pos)) } /// Converts the argument list into a GDScript argument list, using /// the given name generator to produce unique GDScript names. /// /// * Each required argument will be translated into a GDScript /// argument. /// /// * Each optional argument, likewise, will be translated into a /// GDScript argument. On the GDScript side, required and optional /// arguments are indistinguishable. /// /// * If there is a "rest" argument of any kind, it is translated to /// a single GDScript argument as well. pub fn into_gd_arglist(self, gen: &mut impl NameGenerator) -> (GDArgList, Vec) { let cap = 1 + self.required_args.len() + self.optional_args.len(); let mut name_translations = Vec::with_capacity(cap); let mut args = Vec::with_capacity(cap); for arg in self.required_args { let gd = gen.generate_with(&names::lisp_to_gd(&arg)); name_translations.push(NameTrans { lisp_name: arg, gd_name: gd.clone() }); args.push(gd); } for arg in self.optional_args { let gd = gen.generate_with(&names::lisp_to_gd(&arg)); name_translations.push(NameTrans { lisp_name: arg, gd_name: gd.clone() }); args.push(gd); } if let Some((arg, _)) = self.rest_arg { let gd = gen.generate_with(&names::lisp_to_gd(&arg)); name_translations.push(NameTrans { lisp_name: arg, gd_name: gd.clone() }); args.push(gd); } (GDArgList::required(args), name_translations) } /// An iterator over all variable names mentioned in the argument /// list, in order. pub fn iter_vars(&self) -> impl Iterator { self.required_args.iter() .chain(self.optional_args.iter()) .chain(self.rest_arg.iter().map(|x| &x.0)) .map(|x| x.borrow()) } } impl From for FnSpecs { /// [`FnSpecs`] is simply an [`ArgList`] without the argument names; /// it merely preserves the shape. From an `ArgList` we can always /// construct an `FnSpecs` in a canonical way. fn from(arglist: ArgList) -> FnSpecs { // TODO We need to define an upper limit on argument list length // (and check if Godot already has one we need to respect) FnSpecs::new( arglist.required_args.len(), arglist.optional_args.len(), arglist.rest_arg.map(|x| x.1), ) } } impl From for GeneralArgList { fn from(arglist: ArgList) -> GeneralArgList { let required_args: Vec<_> = arglist.required_args.into_iter().map(GeneralArg::simple).collect(); let optional_args: Vec<_> = arglist.optional_args.into_iter().map(GeneralArg::simple).collect(); let rest_arg: Option<_> = arglist.rest_arg.map(|(name, vararg)| (GeneralArg::simple(name), vararg)); GeneralArgList { required_args, optional_args, rest_arg, } } } impl TryFrom for ArgList { type Error = ArgListParseErrorF; fn try_from(arglist: GeneralArgList) -> Result { let required_args: Vec<_> = arglist.required_args.into_iter().map(|x| x.into_simple_name()).collect::>()?; let optional_args: Vec<_> = arglist.optional_args.into_iter().map(|x| x.into_simple_name()).collect::>()?; let rest_arg: Option<_> = arglist.rest_arg.map(|(arg, vararg)| Ok((arg.into_simple_name()?, vararg))).transpose()?; Ok(ArgList { required_args, optional_args, rest_arg, }) } } ================================================ FILE: src/ir/arglist/parser.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! The general-purpose parsing module of [`GeneralArgList`]. use crate::sxp::ast::{AST, ASTF}; use crate::sxp::literal::Literal; use super::general::{GeneralArgList, GeneralArg}; use super::error::{ArgListParseError, ArgListParseErrorF}; use super::vararg::VarArg; use std::cmp::Ordering; use std::borrow::Borrow; /// The current type of argument we're looking for when parsing an /// argument list. /// /// This is an internal type to this module and, generally, callers /// from outside the module should not need to interface with it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ParseState { /// We're expecting required arguments. This is the state we begin /// in. Required, /// We're expecting optional arguments. This is the state following /// `&opt`. Optional, /// We're expecting the single `&rest` argument. Rest, /// We're expecting the single `&arr` argument. Arr, /// We have passed the "rest" argument and are expecting the end of /// an argument list. If *any* arguments occur in this state, an /// error will be issued. RestInvalid, } /// `ParseState` implements `PartialOrd` to indicate valid orderings /// in which the argument type directives can occur. Specifically, we /// can transition from a state `u` to a state `v` if and only if `u /// <= v` is true. If we attempt a state transition where that is not /// true, then an [`ArgListParseErrorF::DirectiveOutOfOrder`] error /// will be issued. /// /// There are two chains in this ordering: /// /// * `Required < Optional < Rest < RestInvalid` /// /// * `Required < Optional < Arr < RestInvalid` /// /// The two states `Rest` and `Arr` are incomparable. impl PartialOrd for ParseState { fn partial_cmp(&self, other: &Self) -> Option { if *self == *other { return Some(Ordering::Equal); } if *self == ParseState::Required { return Some(Ordering::Less); } if *other == ParseState::Required { return Some(Ordering::Greater); } if *self == ParseState::Optional { return Some(Ordering::Less); } if *other == ParseState::Optional { return Some(Ordering::Greater); } if *self == ParseState::RestInvalid { return Some(Ordering::Greater); } if *other == ParseState::RestInvalid { return Some(Ordering::Less); } None } } impl ParseState { /// The state to begin parsing an argument list in. pub const START_STATE: ParseState = ParseState::Required; /// Given an argument directive, returns the state represented by /// that directive, or `None` if the string is not a valid argument /// directive. /// /// Note that [`ParseState::Required`] is not represented by an /// argument directive, as it is the start state and never requires /// (or admits) an `&` directive to transition *to* that state. pub fn state_transition(self, arg: &str) -> Option { match arg.borrow() { "&opt" => Some(ParseState::Optional), "&rest" => Some(ParseState::Rest), "&arr" => Some(ParseState::Arr), _ => None } } /// Parse the given [`AST`] value, modifying the current parse /// state, and the [`GeneralArgList`] being built, as necessary. pub fn parse_once(&mut self, arglist: &mut GeneralArgList, arg: &AST) -> Result<(), ArgListParseError> { if self.parse_state_transition(arg)? { Ok(()) } else { let pos = arg.pos; match self { ParseState::Required => { let general_arg = GeneralArg::parse(arg)?; arglist.required_args.push(general_arg); } ParseState::Optional => { let general_arg = GeneralArg::parse(arg)?; arglist.optional_args.push(general_arg); } ParseState::Rest => { let general_arg = GeneralArg::parse(arg)?; arglist.rest_arg = Some((general_arg, VarArg::RestArg)); *self = ParseState::RestInvalid; } ParseState::Arr => { let general_arg = GeneralArg::parse(arg)?; arglist.rest_arg = Some((general_arg, VarArg::ArrArg)); *self = ParseState::RestInvalid; } ParseState::RestInvalid => { return Err(ArgListParseError::new(ArgListParseErrorF::InvalidArgument(arg.clone()), pos)); } } Ok(()) } } fn parse_state_transition(&mut self, arg: &AST) -> Result { // Returns whether or not a transition was parsed. let pos = arg.pos; match &arg.value { ASTF::Atom(Literal::Symbol(arg)) if arg.starts_with('&') => { let new_state = self.state_transition(arg.borrow()); match new_state { None => { Err(ArgListParseError::new(ArgListParseErrorF::UnknownDirective(arg.to_owned()), pos)) } Some(new_state) => { if *self < new_state { *self = new_state; Ok(true) } else { Err(ArgListParseError::new(ArgListParseErrorF::DirectiveOutOfOrder(arg.to_owned()), pos)) } } } } _ => { Ok(false) } } } } /// Parse an argument list from an iterator of `AST` values. Returns /// either the [`GeneralArgList`] or an appropriate error. pub fn parse<'a>(args: impl IntoIterator) -> Result { let mut state = ParseState::START_STATE; let mut result = GeneralArgList::empty(); for arg in args { state.parse_once(&mut result, arg)?; } Ok(result) } ================================================ FILE: src/ir/arglist/simple.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides [`SimpleArgList`], the type of argument lists which //! consist of simple names and no other features. use super::error::{ArgListParseError, ArgListParseErrorF}; use super::general::{GeneralArgList, GeneralArg}; use super::ordinary::ArgList; use super::parser; use crate::sxp::ast::AST; use crate::gdscript::arglist::ArgList as GDArgList; use crate::compile::names::generator::NameGenerator; use crate::compile::names::NameTrans; use crate::pipeline::source::SourceOffset; use std::convert::TryFrom; use std::borrow::Borrow; /// A simple argument list consists only of required arguments and /// nothing more. This is required in contexts where GDLisp cannot /// determine the arity of a call, such as when invoking instance /// methods on an unknown object in GDLisp. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SimpleArgList { /// The list of required arguments. pub args: Vec, } impl SimpleArgList { /// Converts the argument list into a GDScript argument list, using /// the given name generator to produce unique names, similar to /// [`ArgList::into_gd_arglist`]. pub fn into_gd_arglist(self, gen: &mut impl NameGenerator) -> (GDArgList, Vec) { ArgList::from(self).into_gd_arglist(gen) } /// An iterator over all variable names mentioned in the argument /// list, in order. pub fn iter_vars(&self) -> impl Iterator { self.args.iter().map(|x| x.borrow()) } /// Attempts to parse a simple argument list from the iterator, as /// though by [`ArgList::parse`], and then attempts to convert that /// argument list into a [`SimpleArgList`]. If either step fails, an /// error is reported. pub fn parse<'a>(args: impl IntoIterator, pos: SourceOffset) -> Result { parser::parse(args).and_then(|arglist| { SimpleArgList::try_from(arglist).map_err(|err| ArgListParseError::new(err, pos)) }) } /// The length of the argument list. pub fn len(&self) -> usize { self.args.len() } /// Whether the argument list consists of zero arguments. pub fn is_empty(&self) -> bool { self.args.is_empty() } } impl From for GeneralArgList { fn from(arglist: SimpleArgList) -> GeneralArgList { let required_args: Vec<_> = arglist.args.into_iter().map(GeneralArg::simple).collect(); GeneralArgList { required_args: required_args, optional_args: vec!(), rest_arg: None, } } } impl TryFrom for SimpleArgList { type Error = ArgListParseErrorF; fn try_from(arglist: GeneralArgList) -> Result { if arglist.optional_args.is_empty() && arglist.rest_arg.is_none() { let required_args: Vec<_> = arglist.required_args.into_iter().map(|x| x.into_simple_name()).collect::>()?; Ok(SimpleArgList { args: required_args }) } else { Err(ArgListParseErrorF::SimpleArgListExpected) } } } impl From for ArgList { fn from(arglist: SimpleArgList) -> ArgList { let required_args: Vec<_> = arglist.args; ArgList { required_args: required_args, optional_args: vec!(), rest_arg: None, } } } ================================================ FILE: src/ir/arglist/vararg.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the [`VarArg`] enum, which indicates the types of variable //! argument tails available to lambda lists. use serde::{Serialize, Deserialize}; /// The type of "rest" argument which accumulates any extra arguments /// to a function call. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum VarArg { /// A `&rest` argument accumulates the arguments into a GDLisp list. RestArg, /// An `&arr` argument accumulates the arguments into a Godot array. ArrArg, } impl VarArg { /// An [`i32`] constant representing no variable argument at all. pub const NONE: i32 = 0; /// Converts `self` into a numerical value, suitable for /// communication with GDScript. This method is guaranteed to never /// return [`VarArg::NONE`]. pub fn into_constant(self) -> i32 { match self { VarArg::RestArg => 1, VarArg::ArrArg => 2, } } /// Returns [`VarArg::NONE`] if `opt` is `None`, or calls /// [`VarArg::into_constant`] otherwise. pub fn arg_to_const(opt: Option) -> i32 { opt.map_or(VarArg::NONE, VarArg::into_constant) } } ================================================ FILE: src/ir/bootstrapping.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::incremental::IncCompiler; use super::decl::{Decl, DeclF, DeclareDecl, DeclareType, EnumDecl, ClassDecl}; use super::expr::Expr; use super::export::Visibility; use crate::compile::error::{GDError, GDErrorF}; use crate::gdscript::library::gdnative::NativeClasses; use crate::gdscript::library::{constant_loader, class_loader}; use crate::pipeline::Pipeline; use crate::pipeline::source::SourceOffset; use crate::util::one::One; pub fn compile_bootstrapping_decl( _icompiler: &mut IncCompiler, pipeline: &mut Pipeline, acc: &mut impl Extend, directive: &str, pos: SourceOffset, ) -> Result<(), GDError> { let native = pipeline.get_native_classes(); match directive { "constants" => { bootstrap_constant_names(native, acc, pos); } "constant-enums" => { bootstrap_constant_enums(native, acc, pos); } "non-singleton-types" => { bootstrap_non_singletons(native, acc, pos); } "singleton-types" => { bootstrap_singletons(native, acc, pos); } _ => { return Err(GDError::new(GDErrorF::BadBootstrappingDirective(directive.to_owned()), pos)); } } Ok(()) } pub fn compile_bootstrapping_class_inner_decl( _icompiler: &mut IncCompiler, pipeline: &mut Pipeline, acc: &mut ClassDecl, directive: &str, pos: SourceOffset, ) -> Result<(), GDError> { let native = pipeline.get_native_classes(); match directive { "singleton-backing-types" => { bootstrap_singleton_backing_types(native, acc, pos); } _ => { return Err(GDError::new(GDErrorF::BadBootstrappingDirective(directive.to_owned()), pos)); } } Ok(()) } pub fn compile_bootstrapping_expr( _icompiler: &mut IncCompiler, pipeline: &mut Pipeline, directive: &str, pos: SourceOffset, ) -> Result { let native = pipeline.get_native_classes(); match directive { "native-types-table" => { Ok(class_loader::native_types_dictionary_initializer(native, pos)) } _ => { Err(GDError::new(GDErrorF::BadBootstrappingDirective(directive.to_owned()), pos)) } } } fn bootstrap_constant_names( native: &NativeClasses, acc: &mut impl Extend, pos: SourceOffset, ) { let all_constant_names = constant_loader::get_all_constants(native); acc.extend(all_constant_names.map(|name| declare_superglobal(name, pos))); } fn declare_superglobal(name: &str, pos: SourceOffset) -> Decl { Decl::new( DeclF::DeclareDecl( DeclareDecl { visibility: Visibility::Public, declare_type: DeclareType::Superglobal, name: name.to_owned(), target_name: Some(name.to_owned()), } ), pos, ) } fn bootstrap_constant_enums( native: &NativeClasses, acc: &mut impl Extend, pos: SourceOffset, ) { let all_enums = constant_loader::get_all_constant_enums(native, pos); for constant_enum in all_enums { let decl = EnumDecl::from(constant_enum); acc.extend(One(Decl::new(DeclF::EnumDecl(decl), pos))); } } fn bootstrap_non_singletons( native: &NativeClasses, acc: &mut impl Extend, pos: SourceOffset, ) { let all_non_singleton_classes = class_loader::get_non_singleton_declarations(native); acc.extend(all_non_singleton_classes.map(|decl| Decl::new(DeclF::DeclareDecl(decl), pos))); } fn bootstrap_singletons( native: &NativeClasses, acc: &mut impl Extend, pos: SourceOffset, ) { let all_singleton_classes = class_loader::get_singleton_declarations(native); acc.extend(all_singleton_classes.into_iter().map(|decl| Decl::new(DeclF::DeclareDecl(decl), pos))); } fn bootstrap_singleton_backing_types( native: &NativeClasses, acc: &mut ClassDecl, pos: SourceOffset, ) { let all_singleton_classes = class_loader::get_singleton_class_var_declarations(native, pos); acc.decls.extend(all_singleton_classes); } ================================================ FILE: src/ir/call_name.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! [`CallName`] is the type of valid cars in a call [`AST`] //! expression. use super::incremental::IncCompiler; use super::expr::Expr; use crate::sxp::ast::{AST, ASTF}; use crate::sxp::dotted::DottedExpr; use crate::compile::error::{GDError, GDErrorF}; use crate::pipeline::Pipeline; use crate::pipeline::error::PError; use crate::pipeline::source::SourceOffset; use std::convert::TryFrom; /// GDLisp is fairly conservative about what sort of [`AST`] values /// are allowed as the subject of a call. Excluding special forms and /// other quoted constructs, an `AST` appearing in evaluation context /// must have a car of one of the forms permitted by `CallName`. #[derive(Clone, Debug, PartialEq, Eq)] pub enum CallName { /// A simple /// [`Literal::Symbol`](crate::sxp::literal::Literal::Symbol) name. SimpleName(String), /// An `access-slot` qualified call. MethodName(Box, String), /// A `literally` call. AtomicName(String), /// A call on the special `super` keyword, to invoke the superclass /// method with the given name. SuperName(String), } impl CallName { /// Identifies the type of call being referred to by a particular /// AST. `ast` shall be the AST we're calling, excluding any /// arguments or enclosing structures. pub fn resolve_call_name(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, ast: &AST) -> Result { if let Some((lhs, name)) = CallName::try_resolve_method_name(ast) { // Might be a super call; check for that first. if lhs.value == ASTF::symbol("super") { Ok(CallName::SuperName(name.to_owned())) } else { let lhs = icompiler.compile_expr(pipeline, lhs)?; Ok(CallName::MethodName(Box::new(lhs), name.to_owned())) } } else if let Some(name) = CallName::try_resolve_atomic_name(ast) { Ok(CallName::AtomicName(name.to_owned())) } else if let Some(s) = ast.as_symbol_ref() { Ok(CallName::SimpleName(s.to_owned())) } else { Err(PError::from(GDError::new(GDErrorF::CannotCall(ast.clone()), ast.pos))) } } pub fn into_expr(self, icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { match self { CallName::SimpleName(head) => { icompiler.resolve_simple_call(pipeline, &head, tail, pos) } CallName::MethodName(target, head) => { let args = tail.iter().map(|x| icompiler.compile_expr(pipeline, x)).collect::, _>>()?; Ok(target.method_call(head, args, pos)) } CallName::AtomicName(head) => { let args = tail.iter().map(|x| icompiler.compile_expr(pipeline, x)).collect::, _>>()?; Ok(Expr::atomic_call(head, args, pos)) } CallName::SuperName(head) => { let args = tail.iter().map(|x| icompiler.compile_expr(pipeline, x)).collect::, _>>()?; Ok(Expr::super_call(head, args, pos)) } } } /// Attempts to resolve `ast` as an `access-slot` pair, with an /// `AST` left-hand side and a string method name. fn try_resolve_method_name(ast: &AST) -> Option<(&AST, &str)> { if let Ok(vec) = Vec::<&AST>::try_from(DottedExpr::new(ast)) { if vec.len() == 3 && vec[0].value == ASTF::symbol("access-slot") { if let Some(name) = vec[2].as_symbol_ref() { return Some((vec[1], name)); } } } None } /// Attempts to resolve `ast` as a `literally` name with a single /// symbol argument. fn try_resolve_atomic_name(ast: &AST) -> Option<&str> { if let Ok(vec) = Vec::<&AST>::try_from(DottedExpr::new(ast)) { if vec.len() == 2 && vec[0].value == ASTF::symbol("literally") { if let Some(name) = vec[1].as_symbol_ref() { return Some(name); } } } None } } #[cfg(test)] mod tests { use super::*; use crate::AST_PARSER; use crate::pipeline::Pipeline; use crate::pipeline::source::SourceOffset; use crate::pipeline::config::ProjectConfig; use crate::pipeline::resolver::PanickingNameResolver; use crate::runner::version::VersionInfo; use std::path::PathBuf; fn parse_ast(input: &str) -> AST { AST_PARSER.parse(input).unwrap() } fn dummy_config() -> ProjectConfig { ProjectConfig { root_directory: PathBuf::from(r"."), optimizations: false, godot_version: VersionInfo::default(), } } fn dummy_pipeline() -> Pipeline { Pipeline::with_resolver(dummy_config(), Box::new(PanickingNameResolver)) } fn resolve_call(input: &str) -> Result { let ast = parse_ast(input); let mut icompiler = IncCompiler::new(vec!()); CallName::resolve_call_name(&mut icompiler, &mut dummy_pipeline(), &ast) } #[test] fn simple_call_name() { assert_eq!(resolve_call("foobar"), Ok(CallName::SimpleName(String::from("foobar")))); assert_eq!(resolve_call("some-complicated-name"), Ok(CallName::SimpleName(String::from("some-complicated-name")))); // Built-in names are still ordinary calls according to CallName. assert_eq!(resolve_call("if"), Ok(CallName::SimpleName(String::from("if")))); // `self` is just a name like any other. assert_eq!(resolve_call("self"), Ok(CallName::SimpleName(String::from("self")))); // Although probably a user mistake, `super` is a valid function // name. assert_eq!(resolve_call("super"), Ok(CallName::SimpleName(String::from("super")))); } #[test] fn method_name() { assert_eq!(resolve_call("foo:bar"), Ok(CallName::MethodName(Box::new(Expr::var("foo", SourceOffset(0))), String::from("bar")))); assert_eq!(resolve_call("self:bar"), Ok(CallName::MethodName(Box::new(Expr::var("self", SourceOffset(0))), String::from("bar")))); } #[test] fn super_name() { assert_eq!(resolve_call("super:bar"), Ok(CallName::SuperName(String::from("bar")))); } #[test] fn atomic_name() { assert_eq!(resolve_call("(literally bar)"), Ok(CallName::AtomicName(String::from("bar")))); } } ================================================ FILE: src/ir/classification.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Convenience functions for checking the type of thing we're looking //! at, for instance whether a given [`AST`] is a declaration or an //! expression. use crate::sxp::ast::{AST, ASTF}; use crate::sxp::dotted::DottedExpr; use crate::pipeline::source::SourceOffset; use std::convert::TryInto; use std::borrow::Borrow; /// A `super` call detected by [`detect_super`]. #[derive(Clone, Debug)] pub struct DetectedSuperCall<'a> { pub arguments: Vec<&'a AST>, pub pos: SourceOffset, } /// Here, we list the heads for all valid declaration types. Note that /// `progn` is specifically not included here; `progn` is an /// expression-level special form which is also a deeply magical /// construct treated in a special way by the compiler during parsing. /// It is *not* a declaration, even though it can look like one /// syntactically. pub const DECL_HEADS: [&str; 9] = [ "defn", "defmacro", "defconst", "defclass", "defenum", "sys/declare", "define-symbol-macro", "sys/bootstrap", "sys/min-godot-version", ]; /// A simple check to see whether a given AST should be parsed as a /// declaration or not. /// /// There are many contexts (including the very top-level of a file) /// where either a declaration or an expression is acceptable. This /// function is used to determine whether the AST should be parsed as /// a declaration or an expression. Note that a `true` result for /// `is_decl` is *not* a guarantee that parsing as a declaration will /// be error-free; it is merely an indication that the declaration /// parse should be attempted. /// /// If `decl` is not a proper list (as per the definition in /// [`DottedExpr`](crate::sxp::dotted::DottedExpr)), then `is_decl` /// returns false. Otherwise, the first term of the list is checked /// against several known symbol values ([`DECL_HEADS`]) to determine /// if the AST represents a declaration. pub fn is_decl(decl: &AST) -> bool { let vec: Result, _> = DottedExpr::new(decl).try_into(); if let Ok(vec) = vec { if let Some(head) = vec.get(0).and_then(|x| x.as_symbol_ref()) { return DECL_HEADS.contains(&head.borrow()); } } false } /// Given the body of a [`FnDecl`](super::decl::FnDecl) or a possible /// [`ConstructorDecl`](super::decl::ConstructorDecl), check to see if /// the first expression present is a "super" call. /// /// If a "super" call is present, its arguments, as well as a slice of /// the rest of the body, is returned. Otherwise, the whole slice is /// returned untouched. pub fn detect_super<'a, 'b>(body: &'a [&'b AST]) -> (Option>, &'a [&'b AST]) { if !body.is_empty() { let first: Result, _> = DottedExpr::new(body[0]).try_into(); if let Ok(mut first) = first { if !first.is_empty() && first[0].value == ASTF::symbol(String::from("super")) { let super_symbol = first.remove(0); let super_args = first; let super_call = DetectedSuperCall { arguments: super_args, pos: super_symbol.pos, }; return (Some(super_call), &body[1..]); } } } (None, body) } #[cfg(test)] mod tests { use super::*; use crate::AST_PARSER; fn parse_ast(input: &str) -> AST { AST_PARSER.parse(input).unwrap() } #[test] fn is_decl_test() { assert!(is_decl(&parse_ast("(defn foo ())"))); assert!(is_decl(&parse_ast("(defclass Example (Reference) (defn bar (x)))"))); assert!(is_decl(&parse_ast("(defmacro foo ())"))); assert!(is_decl(&parse_ast("(defconst MY_CONST 3)"))); assert!(is_decl(&parse_ast("(defenum MyEnum A B C)"))); assert!(is_decl(&parse_ast("(define-symbol-macro my-macro 3)"))); assert!(is_decl(&parse_ast("(sys/declare value xyz)"))); assert!(is_decl(&parse_ast("(sys/min-godot-version 3030000)"))); } #[test] fn is_not_decl_test() { assert!(!is_decl(&parse_ast("100"))); assert!(!is_decl(&parse_ast("((defn foo ()))"))); assert!(!is_decl(&parse_ast("abc"))); assert!(!is_decl(&parse_ast("(progn 1 2 3)"))); assert!(!is_decl(&parse_ast("(progn (defconst MY_CONST 3))"))); assert!(!is_decl(&parse_ast("#t"))); } } ================================================ FILE: src/ir/closure_names.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helper for storing collections of values to be closed over. //! //! This module defines the [`ClosureNames`] type, which maintains a //! collection of names and some reference to how they're used. If a //! name is "used" multiple times (i.e. [`ClosureNames::visit`] is //! called multiple times with the same name), then the usage //! references will be combined via [`Lattice::join`]. use crate::util::lattice::Lattice; use crate::pipeline::source::SourceOffset; use std::collections::HashMap; use std::iter::IntoIterator; /// A collection of names and usage information. The usage information /// shall be an instance of [`Lattice`]. The `Lattice` instance is /// used for combining duplicate keys in the table. #[derive(PartialEq, Eq, Debug, Clone)] pub struct ClosureNames(HashMap); impl ClosureNames { /// Constructs a new, empty `ClosureNames`. pub fn new() -> Self { ClosureNames::default() } /// Constructs a `ClosureNames` from a hashmap mapping names to /// usage information. pub fn from_hashmap(map: HashMap) -> Self { // TODO Do we ever use this function? Its signature is kinda... unfortunately complicated. ClosureNames(map) } /// Gets the usage information associated to the name, or [`None`] /// if the name is unknown to `self`. pub fn get(&self, name: &str) -> Option<&T> { self.0.get(name).map(|x| &x.0) } /// Whether or not the `ClosureNames` contains the given name. /// Equivalent to `self.get(name).is_some()`. pub fn contains(&self, name: &str) -> bool { self.get(name).is_some() } /// Marks the given name as being used in the given way. If the name /// was not known to `self` before, it is added to the table. If the /// variable was known, then the previous usage information and /// `usage` are joined via [`Lattice::join`] to produce the new /// usage. /// /// In case of a conflict, the minimum of the two source positions /// is chosen, since errors reported earlier in the file are /// generally more intuitive than those reported later, all other /// things being equal. pub fn visit(&mut self, name: String, usage: T, pos: SourceOffset) { match self.0.remove(&name) { None => self.0.insert(name, (usage, pos)), Some((prev_usage, prev_pos)) => self.0.insert(name, (prev_usage.join(usage), prev_pos.min(pos))), }; } /// Removes the name from the table, returning its usage type if one /// existed. pub fn remove(&mut self, name: &str) -> Option { self.0.remove(name).map(|x| x.0) } /// An iterator over all names known to the `ClosureNames` instance. pub fn names(&self) -> impl Iterator { self.0.keys().map(|x| &x[..]) } /// As [`ClosureNames::names`] but takes ownership over the values /// in `self`. pub fn into_names(self) -> impl Iterator { self.0.into_iter().map(|x| x.0) } /// Returns whether `self` is devoid of any names. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Adds all of the names from `b` into `self`. Any names which did /// not exist in `self` are added, and those which did will have /// their two usage values joined via [`Lattice::join`]. pub fn merge_with(&mut self, b: ClosureNames) { for (name, data, pos) in b.into_iter_with_offset() { self.visit(name, data, pos); } } /// Filters the `ClosureNames` instance in-place, retaining only /// those values for which `f` returns true. pub fn retain(&mut self, mut f: impl FnMut(&str, &mut T) -> bool) { self.0.retain(|x, (y, _)| f(x, y)) } // TODO "with offset" versions of iter(), iter_mut(), and a "non-with-offset" version of into_iter(). /// Iterates over the entries in the table. pub fn iter(&self) -> impl Iterator { self.0.iter().map(|(x, (y, _))| (&x[..], y)) } /// Iterates over the entries in the table, allowing mutation on the /// usage information. pub fn iter_mut(&mut self) -> impl Iterator { self.0.iter_mut().map(|(x, (y, _))| (&x[..], y)) } /// Iterates over the entries in the table, taking ownership during iteration. pub fn into_iter_with_offset(self) -> impl Iterator { self.0.into_iter().map(|(s, (t, p))| (s, t, p)) } /// Iterates over the entries in the table, by immutable reference. pub fn iter_with_offset(&self) -> impl Iterator { self.0.iter().map(|(s, (t, p))| (s.as_str(), t, *p)) } } impl Default for ClosureNames { fn default() -> Self { ClosureNames(HashMap::default()) } } ================================================ FILE: src/ir/decl.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::arglist::ordinary::ArgList; use super::arglist::constructor::ConstructorArgList; use super::arglist::simple::SimpleArgList; use super::expr::{self, Expr, Locals, Functions}; use super::literal::Literal; use super::import::ImportDecl; use super::identifier::{Namespace, ClassNamespace, Id, IdLike}; use super::export::Visibility; use crate::sxp::ast::AST; use crate::sxp::dotted::DottedExpr; use crate::gdscript::decl::Static; use crate::gdscript::library; use crate::pipeline::source::{SourceOffset, Sourced}; use crate::compile::body::class_initializer::InitTime; use crate::compile::body::synthetic_field::{Getter, Setter}; use std::collections::HashMap; use std::borrow::Cow; use std::convert::TryInto; use std::iter; #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct TopLevel { pub imports: Vec, pub decls: Vec, pub minimalist_flag: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum DeclF { FnDecl(FnDecl), MacroDecl(MacroDecl), SymbolMacroDecl(SymbolMacroDecl), ConstDecl(ConstDecl), ClassDecl(ClassDecl), EnumDecl(EnumDecl), DeclareDecl(DeclareDecl), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Decl { pub value: DeclF, pub pos: SourceOffset, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct FnDecl { pub visibility: Visibility, pub call_magic: Option, pub name: String, pub args: ArgList, pub body: Expr, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MacroDecl { pub visibility: Visibility, pub name: String, pub args: ArgList, pub body: Expr, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct SymbolMacroDecl { pub visibility: Visibility, pub name: String, pub body: Expr, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ConstDecl { pub visibility: Visibility, pub name: String, pub value: Expr, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EnumDecl { pub visibility: Visibility, pub name: String, pub clauses: Vec<(String, Option)>, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClassDecl { pub visibility: Visibility, pub name: String, pub extends: String, pub main_class: bool, pub constructor: Option, pub decls: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ConstructorDecl { pub args: ConstructorArgList, pub super_call: SuperCall, pub body: Expr, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct SuperCall { pub call: Vec, pub pos: SourceOffset, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum ClassInnerDeclF { ClassSignalDecl(ClassSignalDecl), ClassConstDecl(ConstDecl), ClassVarDecl(ClassVarDecl), ClassFnDecl(ClassFnDecl), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClassInnerDecl { pub value: ClassInnerDeclF, pub pos: SourceOffset, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClassSignalDecl { pub name: String, pub args: SimpleArgList, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClassVarDecl { pub export: Option, pub name: String, pub value: Option, pub init_time: InitTime, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClassFnDecl { /// Indicates whether or not the function is a static instance function. pub is_static: Static, /// If this field is true, then all of the arguments to this /// function will be compiled to optional GDScript arguments whose /// default values are `null`. /// /// There is no user-exposed way to set this flag to true. This is /// used in `GDLisp.lisp` to emulate variable arguments for small /// argument counts. pub is_nullargs: bool, /// The name of the function, which can be an ordinary name or a /// getter or setter. pub name: InstanceFunctionName, /// The list of arguments to the function. These arguments are /// always required on the GDLisp side. When compiled, these /// arguments compile to required arguments if `is_nullargs` is /// false, or optional ones otherwise. pub args: SimpleArgList, /// The body of the function, as a single [`Expr`]. For functions /// which require multiple expressions, [`expr::ExprF::Progn`] can be /// used. pub body: Expr, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct DeclareDecl { pub visibility: Visibility, pub declare_type: DeclareType, pub name: String, // If this doesn't exist, it will be treated as // `lisp_to_gd(self.name)` after compilation. The declaration is // allowed to override this, however. pub target_name: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum DeclareType { /// A value, with no hint as to its value or `const` status. Value, /// A value, equivalent to [`DeclareType::Value`], but with a value /// hint indicating that its value is immutable at runtime. Constant, /// A function available at the top-level of the current module. Function(ArgList), /// A superglobal value, available from everywhere in the Godot /// ecosystem without import. Superglobal, /// A superglobal function, available from everywhere in the Godot /// ecosystem without import. SuperglobalFn(ArgList), } // TODO This is a bit confusing, since "export" is used in GDLisp to // mean "exported from a module", not "exported to the interface". We // should probably rename this so that GDScript "exports" are called // something else. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Export { pub args: Vec, } /// The name of an instance function in GDLisp, which can either be an /// ordinary string or a special accessor. #[derive(Clone, Debug, Eq, PartialEq)] pub enum InstanceFunctionName { /// An ordinary string name, which will compile to an ordinary /// method in GDScript. Ordinary(String), /// A setter for the field with the given name. Setter(String), /// A getter for the field with the given name. Getter(String), } #[derive(Clone, Debug, Copy, Eq, PartialEq)] pub struct DuplicateMainClassError(pub SourceOffset); impl TopLevel { pub fn new() -> TopLevel { TopLevel::default() } /// Find the class in `self` for which [`ClassDecl::main_class`] is /// true. If there is no such class, then `None` is returned. If /// there are multiple such classes, then /// [`DuplicateMainClassError`] is returned. pub fn find_main_class(&self) -> Result, DuplicateMainClassError> { let all_main_classes: Vec<_> = self.decls.iter().filter(|decl| { if let DeclF::ClassDecl(cdecl) = &decl.value { cdecl.main_class } else { false } }).collect(); match all_main_classes.len() { 0 => { // No main class; return None Ok(None) } 1 => { // Single main class; return it if let DeclF::ClassDecl(cdecl) = &all_main_classes[0].value { Ok(Some(cdecl)) } else { panic!("Internal error in TopLevel::find_main_class (this is a bug in GDLisp)") } } _ => { // Duplicate main classes; produce an error at the position of // the second let second_main_class = all_main_classes[1]; Err(DuplicateMainClassError(second_main_class.pos)) } } } pub fn inner_exprs(&self) -> impl Iterator + '_ { self.decls.iter().flat_map(|x| x.inner_exprs()) } } impl Decl { pub fn new(value: DeclF, pos: SourceOffset) -> Decl { Decl { value, pos } } pub fn to_id(&self) -> Id { Id::new(self.namespace(), self.name().to_owned()) } pub fn id_like<'a>(&'a self) -> Box + 'a> { Id::build(self.namespace(), self.name()) } pub fn name(&self) -> &str { match &self.value { DeclF::FnDecl(decl) => &decl.name, DeclF::MacroDecl(decl) => &decl.name, DeclF::SymbolMacroDecl(decl) => &decl.name, DeclF::ConstDecl(decl) => &decl.name, DeclF::ClassDecl(decl) => &decl.name, DeclF::EnumDecl(decl) => &decl.name, DeclF::DeclareDecl(decl) => &decl.name, } } // Gets the direct dependencies required by the declaration. pub fn dependencies(&self) -> HashMap { match &self.value { DeclF::FnDecl(f) => { let mut ids: HashMap = f.body.get_ids().collect(); for name in f.args.iter_vars() { ids.remove(&*Id::build(Namespace::Value, name)); } ids } DeclF::MacroDecl(m) => { let mut ids: HashMap = m.body.get_ids().collect(); for name in m.args.iter_vars() { ids.remove(&*Id::build(Namespace::Value, name)); } ids } DeclF::SymbolMacroDecl(m) => { m.body.get_ids().collect() } DeclF::ConstDecl(c) => { c.value.get_ids().collect() } DeclF::ClassDecl(c) => { let mut ids = HashMap::new(); ids.insert(Id::new(Namespace::Value, c.extends.to_owned()), self.pos); ids.extend(c.constructor_or_default(SourceOffset::from(0)).dependencies()); for d in &c.decls { ids.extend(d.dependencies()); } ids.remove(&Id::new(Namespace::Value, String::from("self"))); ids } DeclF::EnumDecl(enum_decl) => { let mut ids = HashMap::new(); for (_, expr) in &enum_decl.clauses { if let Some(expr) = expr { ids.extend(expr.get_ids()); } } ids } DeclF::DeclareDecl(_) => { // Declare declarations have no dependencies; they are // assertions to the compiler. HashMap::new() } } } pub fn is_macro(&self) -> bool { matches!(&self.value, DeclF::MacroDecl(_) | DeclF::SymbolMacroDecl(_)) } #[deprecated(note="Use visibility() or export::Visibility constants instead")] pub fn is_exported_by_default(&self) -> bool { // (sys/declare ...) statements are never exported and are always // file-local by default. !(matches!(&self.value, DeclF::DeclareDecl(_))) } pub fn namespace(&self) -> Namespace { match &self.value { DeclF::FnDecl(_) => Namespace::Function, DeclF::MacroDecl(_) => Namespace::Function, DeclF::SymbolMacroDecl(_) => Namespace::Value, DeclF::ConstDecl(_) => Namespace::Value, DeclF::ClassDecl(_) => Namespace::Value, DeclF::EnumDecl(_) => Namespace::Value, DeclF::DeclareDecl(d) => d.declare_type.namespace(), } } pub fn visibility(&self) -> Visibility { match &self.value { DeclF::FnDecl(d) => d.visibility, DeclF::MacroDecl(d) => d.visibility, DeclF::SymbolMacroDecl(d) => d.visibility, DeclF::ConstDecl(d) => d.visibility, DeclF::ClassDecl(d) => d.visibility, DeclF::EnumDecl(d) => d.visibility, DeclF::DeclareDecl(d) => d.visibility, } } pub fn visibility_mut(&mut self) -> &mut Visibility { match &mut self.value { DeclF::FnDecl(d) => &mut d.visibility, DeclF::MacroDecl(d) => &mut d.visibility, DeclF::SymbolMacroDecl(d) => &mut d.visibility, DeclF::ConstDecl(d) => &mut d.visibility, DeclF::ClassDecl(d) => &mut d.visibility, DeclF::EnumDecl(d) => &mut d.visibility, DeclF::DeclareDecl(d) => &mut d.visibility, } } pub fn inner_exprs(&self) -> Box + '_> { match &self.value { DeclF::FnDecl(cdecl) => Box::new(iter::once(&cdecl.body)), DeclF::MacroDecl(cdecl) => Box::new(iter::once(&cdecl.body)), DeclF::SymbolMacroDecl(cdecl) => Box::new(iter::once(&cdecl.body)), DeclF::ConstDecl(cdecl) => Box::new(iter::once(&cdecl.value)), DeclF::DeclareDecl(_) => Box::new(iter::empty()), DeclF::EnumDecl(cdecl) => { Box::new(cdecl.clauses.iter().flat_map(|(_name, value)| value)) } DeclF::ClassDecl(cdecl) => { let constructor_exprs = cdecl.constructor.iter().flat_map(|constructor| { constructor.super_call.call.iter() .chain(iter::once(&constructor.body)) }); let inner_exprs = cdecl.decls.iter().flat_map(ClassInnerDecl::inner_exprs); Box::new(constructor_exprs.chain(inner_exprs)) } } } } impl EnumDecl { pub fn value_names(&self) -> impl Iterator { self.clauses.iter().map(|(x, _)| &**x) } } impl ClassDecl { pub fn new(name: String, extends: String) -> ClassDecl { ClassDecl { visibility: Visibility::CLASS, name: name, extends: extends, main_class: false, constructor: None, decls: vec!(), } } /// The class' constructor, or an empty constructor if there is no /// constructor. In the latter case, the empty constructor will be /// reported as being at source position default_pos, which should /// be the position of the start of the class declaration. pub fn constructor_or_default(&self, default_pos: SourceOffset) -> Cow { self.constructor.as_ref().map_or_else(|| Cow::Owned(ConstructorDecl::empty(default_pos)), Cow::Borrowed) } } impl ConstructorDecl { /// An empty constructor, marked as starting at `pos`. `pos` should /// be the position of the start of the class declaration. pub fn empty(pos: SourceOffset) -> ConstructorDecl { ConstructorDecl { args: ConstructorArgList { args: vec!() }, super_call: SuperCall::empty(pos), body: Expr::literal(Literal::Nil, pos), } } pub fn dependencies(&self) -> HashMap { let mut ids: HashMap = self.body.get_ids().collect(); for expr in &self.super_call.call { for (id, pos) in expr.get_ids() { ids.insert(id, pos); } } for (name, is_instance_field) in self.args.iter_vars() { // Instance field arguments are accessed directly on `self` and // are not available as GDLisp-side local variables. if !is_instance_field { ids.remove(&*Id::build(Namespace::Value, name)); } } ids.remove(&*Id::build(Namespace::Value, "self")); ids } pub fn get_names(&self) -> (Locals, Functions) { let (mut loc, func) = self.body.get_names(); for (name, is_instance_field) in self.args.iter_vars() { // Instance field arguments are accessed directly on `self` and // are not available as GDLisp-side local variables. if !is_instance_field { loc.remove(name); } } loc.remove("self"); (loc, func) } } impl SuperCall { /// An empty super call, which invokes the super constructor with no /// arguments. pub fn empty(pos: SourceOffset) -> SuperCall { SuperCall { call: vec!(), pos: pos } } } impl ClassInnerDecl { pub fn new(value: ClassInnerDeclF, pos: SourceOffset) -> ClassInnerDecl { ClassInnerDecl { value, pos } } pub fn dependencies(&self) -> HashMap { match &self.value { ClassInnerDeclF::ClassSignalDecl(_) => HashMap::new(), ClassInnerDeclF::ClassConstDecl(_) => HashMap::new(), ClassInnerDeclF::ClassVarDecl(_) => HashMap::new(), ClassInnerDeclF::ClassFnDecl(func) => { let mut ids: HashMap = func.body.get_ids().collect(); for name in func.args.iter_vars() { ids.remove(&*Id::build(Namespace::Value, name)); } ids.remove(&*Id::build(Namespace::Value, "self")); ids } } } pub fn name(&self) -> Cow { match &self.value { ClassInnerDeclF::ClassSignalDecl(signal) => Cow::Borrowed(&signal.name), ClassInnerDeclF::ClassConstDecl(constant) => Cow::Borrowed(&constant.name), ClassInnerDeclF::ClassVarDecl(var) => Cow::Borrowed(&var.name), ClassInnerDeclF::ClassFnDecl(func) => func.name.method_name(), } } pub fn namespace(&self) -> ClassNamespace { match &self.value { ClassInnerDeclF::ClassSignalDecl(_) => ClassNamespace::Signal, ClassInnerDeclF::ClassConstDecl(_) => ClassNamespace::Value, ClassInnerDeclF::ClassVarDecl(_) => ClassNamespace::Value, ClassInnerDeclF::ClassFnDecl(_) => ClassNamespace::Function, } } pub fn get_names(&self) -> (Locals, Functions) { match &self.value { ClassInnerDeclF::ClassSignalDecl(_) | ClassInnerDeclF::ClassConstDecl(_) => { (Locals::new(), Functions::new()) } ClassInnerDeclF::ClassVarDecl(vdecl) => { if let Some(initial_value) = &vdecl.value { initial_value.get_names() } else { (Locals::new(), Functions::new()) } } ClassInnerDeclF::ClassFnDecl(fndecl) => { let (mut loc, func) = fndecl.body.get_names(); for name in fndecl.args.iter_vars() { loc.remove(name); } if !bool::from(fndecl.is_static) { loc.remove("self"); } (loc, func) } } } pub fn is_static(&self) -> bool { match &self.value { ClassInnerDeclF::ClassSignalDecl(_) | ClassInnerDeclF::ClassVarDecl(_) => false, ClassInnerDeclF::ClassConstDecl(_) => true, ClassInnerDeclF::ClassFnDecl(decl) => decl.is_static.into(), } } pub fn inner_exprs(&self) -> Box + '_> { match &self.value { ClassInnerDeclF::ClassSignalDecl(_) => Box::new(iter::empty()), ClassInnerDeclF::ClassConstDecl(cdecl) => Box::new(iter::once(&cdecl.value)), ClassInnerDeclF::ClassVarDecl(cdecl) => Box::new(cdecl.value.iter()), ClassInnerDeclF::ClassFnDecl(cdecl) => Box::new(iter::once(&cdecl.body)), } } } impl DeclareType { pub fn namespace(&self) -> Namespace { match self { DeclareType::Value | DeclareType::Constant | DeclareType::Superglobal => Namespace::Value, DeclareType::Function(_) | DeclareType::SuperglobalFn(_) => Namespace::Function, } } } impl InstanceFunctionName { /// Given an [`AST`] representing the name of an instance function, /// returns the name as an [`InstanceFunctionName`]. Specifically, /// the following are attempted in order. /// /// 1. If the [`AST`] argument holds a /// [`Literal::Symbol`](crate::sxp::literal::Literal::Symbol), then /// this is an [`InstanceFunctionName::Ordinary`] name. /// /// 2. Otherwise, if the argument is a (proper) list of two /// elements, both of which are symbols, and if the first symbol is /// `set`, then this is an [`InstanceFunctionName::Setter`]. /// /// 3. If the argument is a (proper) list of two elements, both of /// which are symbols, and if the first symbol is `get`, then this /// is an [`InstanceFunctionName::Getter`]. /// /// 4. Otherwise, parsing fails and `None` is returned. pub fn parse(ast: &AST) -> Option { if let Some(name) = ast.as_symbol_ref() { return Some(InstanceFunctionName::Ordinary(name.to_owned())); } let list: Vec<_> = DottedExpr::new(ast).try_into().ok()?; if let [accessor_type, field_name] = &*list { if let Some(field_name) = field_name.as_symbol_ref() { if let Some(accessor_type) = accessor_type.as_symbol_ref() { match accessor_type { "set" => { return Some(InstanceFunctionName::Setter(field_name.to_owned())); } "get" => { return Some(InstanceFunctionName::Getter(field_name.to_owned())); } _ => {} } } } } None } /// The name of the method being defined, using the GDLisp naming /// conventions (e.g., characters such as `-` will *not* be /// converted in the returned value). For /// [`InstanceFunctionName::Ordinary`], this is simply the declared /// name of the method. For other types of method names, specialized /// prefixes will be added to compute the runtime name. pub fn method_name(&self) -> Cow { match self { InstanceFunctionName::Ordinary(name) => Cow::Borrowed(name), InstanceFunctionName::Setter(field_name) => Cow::Owned(Setter::method_name(field_name)), InstanceFunctionName::Getter(field_name) => Cow::Owned(Getter::method_name(field_name)), } } /// Returns true if this is the name of a constructor function, i.e. /// if this is an ordinary function whose name is equal to /// [`CONSTRUCTOR_NAME`](library::CONSTRUCTOR_NAME). pub fn is_constructor_function(&self) -> bool { if let InstanceFunctionName::Ordinary(fname) = self { fname == library::CONSTRUCTOR_NAME } else { false } } } impl Sourced for Decl { type Item = DeclF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &DeclF { &self.value } } impl Sourced for ClassInnerDecl { type Item = ClassInnerDeclF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &ClassInnerDeclF { &self.value } } impl From for Namespace { fn from(d: DeclareType) -> Namespace { d.namespace() } } impl From for MacroDecl { fn from(decl: SymbolMacroDecl) -> MacroDecl { MacroDecl { visibility: decl.visibility, name: decl.name, args: ArgList::empty(), body: decl.body, } } } impl From<(ClassDecl, Vec)> for expr::LambdaClass { fn from(arg: (ClassDecl, Vec)) -> expr::LambdaClass { let (decl, args) = arg; expr::LambdaClass { extends: decl.extends, args: args, constructor: decl.constructor, decls: decl.decls, } } } #[cfg(test)] mod tests { use super::*; use crate::AST_PARSER; fn parse_ast(input: &str) -> AST { AST_PARSER.parse(input).unwrap() } fn sample_class(class_name: &str, main_class: bool) -> ClassDecl { ClassDecl { visibility: Visibility::Public, name: String::from(class_name), extends: String::from("Reference"), main_class, constructor: None, decls: vec!(), } } #[test] fn find_main_class_test_1() { let example = TopLevel { imports: vec!(), decls: vec!( Decl::new(DeclF::ClassDecl(sample_class("Foo", false)), SourceOffset::default()), Decl::new(DeclF::ClassDecl(sample_class("Bar", false)), SourceOffset::default()), ), minimalist_flag: false, }; assert_eq!(example.find_main_class(), Ok(None)); } #[test] fn find_main_class_test_2() { let example = TopLevel { imports: vec!(), decls: vec!( Decl::new(DeclF::ClassDecl(sample_class("Foo", true)), SourceOffset::default()), Decl::new(DeclF::ClassDecl(sample_class("Bar", false)), SourceOffset::default()), ), minimalist_flag: false, }; assert_eq!(example.find_main_class(), Ok(Some(&sample_class("Foo", true)))); } #[test] fn find_main_class_test_3() { let example = TopLevel { imports: vec!(), decls: vec!( Decl::new(DeclF::ClassDecl(sample_class("Foo", false)), SourceOffset::default()), Decl::new(DeclF::ClassDecl(sample_class("Bar", true)), SourceOffset::default()), ), minimalist_flag: false, }; assert_eq!(example.find_main_class(), Ok(Some(&sample_class("Bar", true)))); } #[test] fn find_main_class_test_4() { let example = TopLevel { imports: vec!(), decls: vec!( Decl::new(DeclF::ClassDecl(sample_class("Foo", true)), SourceOffset::default()), Decl::new(DeclF::ClassDecl(sample_class("Bar", true)), SourceOffset::default()), ), minimalist_flag: false, }; assert_eq!(example.find_main_class(), Err(DuplicateMainClassError(SourceOffset(0)))); } #[test] fn find_main_class_test_5() { // Nontrivial source offset let example = TopLevel { imports: vec!(), decls: vec!( Decl::new(DeclF::ClassDecl(sample_class("Foo", true)), SourceOffset(10)), Decl::new(DeclF::ClassDecl(sample_class("Bar", true)), SourceOffset(20)), ), minimalist_flag: false, }; // Should be reported at the source position of the *second* main class assert_eq!(example.find_main_class(), Err(DuplicateMainClassError(SourceOffset(20)))); } #[test] fn parse_instance_function_name_test() { assert_eq!(InstanceFunctionName::parse(&parse_ast("abc")), Some(InstanceFunctionName::Ordinary(String::from("abc")))); assert_eq!(InstanceFunctionName::parse(&parse_ast("foo-bar")), Some(InstanceFunctionName::Ordinary(String::from("foo-bar")))); assert_eq!(InstanceFunctionName::parse(&parse_ast("(set pizza)")), Some(InstanceFunctionName::Setter(String::from("pizza")))); assert_eq!(InstanceFunctionName::parse(&parse_ast("(get pizza)")), Some(InstanceFunctionName::Getter(String::from("pizza")))); } #[test] fn parse_instance_function_name_failures_test() { assert_eq!(InstanceFunctionName::parse(&parse_ast("0")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("(get a b c)")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("(put abc)")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("(get 100)")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("\"alpha\"")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("()")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("(set)")), None); assert_eq!(InstanceFunctionName::parse(&parse_ast("(get)")), None); } #[test] fn is_constructor_name_test() { assert!(InstanceFunctionName::Ordinary(String::from("_init")).is_constructor_function()); assert!(!InstanceFunctionName::Ordinary(String::from("init")).is_constructor_function()); assert!(!InstanceFunctionName::Ordinary(String::from("foobar")).is_constructor_function()); assert!(!InstanceFunctionName::Setter(String::from("_init")).is_constructor_function()); assert!(!InstanceFunctionName::Getter(String::from("_init")).is_constructor_function()); assert!(!InstanceFunctionName::Setter(String::from("abcdef")).is_constructor_function()); assert!(!InstanceFunctionName::Getter(String::from("baz")).is_constructor_function()); } } ================================================ FILE: src/ir/declaration_table.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! A [`DeclarationTable`] is a sequence of declarations which can be //! efficiently indexed by [`Id`]. use super::Decl; use super::identifier::{Id, IdLike, Namespace}; use crate::compile::error::{GDError, GDErrorF}; use std::collections::HashMap; use std::mem; /// A `DeclarationTable` stores [`Decl`] declarations, indexed by /// their names. A `DeclarationTable` should be thought of as a /// `Vec`, but with efficient access indexed by the /// [identifier](Id) of a declaration. There are `From` and `Into` /// instances converting from and to `Vec`. /// /// Note that, for indexing purposes, the identifier of a `Decl` is /// given by [`Decl::to_id`]. #[derive(Clone, Debug, Default)] pub struct DeclarationTable { values: HashMap, in_order: Vec, } impl DeclarationTable { /// Constructs a new, empty `DeclarationTable`. pub fn new() -> DeclarationTable { DeclarationTable::default() } /// Gets the declaration with the given identifier. pub fn get<'a>(&self, id: &(dyn IdLike + 'a)) -> Option<&Decl> { self.values.get(id).map(|idx| &self.in_order[*idx]) } /// Adds a [`Decl`] to the `DeclarationTable`, to be indexed via its /// [`Decl::to_id`] value. If a declaration with that ID is already /// present in this table, then that declaration is replaced with /// `value`. Otherwise, `value` is added at the end of the table. #[allow(clippy::map_entry)] // Using the Entry API would require that value be cloned. pub fn add(&mut self, value: Decl) -> Option { let id = value.to_id(); let new_idx = self.in_order.len(); if self.values.contains_key(&id) { let idx = *self.values.get(&id).unwrap(); Some(mem::replace(&mut self.in_order[idx], value)) } else { self.values.insert(id, new_idx); self.in_order.push(value); None } } /// Adds a [`Decl`] to the `DeclarationTable`, if no declaration /// with that name already exists. If a declaration with the given /// name already exists, an error is reported and the table is /// unmodified. pub fn add_unless_exists(&mut self, value: Decl) -> Result<(), GDError> { if self.get(&*value.id_like()).is_some() { Err( GDError::new(GDErrorF::DuplicateName(value.namespace().into(), value.name().to_owned()), value.pos), ) } else { let result = self.add(value); assert!(result.is_none(), "Internal error in DeclarationTable::add_unless_exists"); Ok(()) } } /// Removes the declaration with identifier `id` from the table and /// returns it. Returns `None` if no matching declaration exists. pub fn del<'a>(&mut self, id: &(dyn IdLike + 'a)) -> Option { if let Some(idx) = self.values.remove(id) { let decl = self.in_order.remove(idx); for v in self.values.values_mut() { // We shifted declarations over, so we need to update all // indices that were to the right of the removed one. if *v > idx { *v -= 1; } } Some(decl) } else { None } } /// Checks whether a declaration with the given identifier exists in /// the table. Equivalent to `self.get(id).is_some()`. pub fn has<'a>(&self, id: &(dyn IdLike + 'a)) -> bool { self.get(id).is_some() } /// Filters the `DeclarationTable` by the predicate `condition`, /// returning a new table where only the entries which returned true /// under the predicate are kept. pub fn filter(&self, condition: impl FnMut(&Decl) -> bool) -> DeclarationTable { let vec = Vec::from(self.clone()); let filtered = vec.into_iter().filter(condition).collect::>(); filtered.into() } /// Iterates over the elements of `self` in order. pub fn iter(&self) -> impl Iterator { self.in_order.iter() } } impl From for Vec { fn from(table: DeclarationTable) -> Vec { table.in_order } } impl From> for DeclarationTable { fn from(decls: Vec) -> DeclarationTable { let mut table = DeclarationTable::new(); for decl in decls { table.add(decl); } table } } ================================================ FILE: src/ir/depends.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::declaration_table::DeclarationTable; use super::identifier::{IdLike, Id, Namespace}; use crate::pipeline::source::SourceOffset; use std::collections::{HashSet, HashMap}; // TODO Make these fields private #[derive(Clone, Debug, Eq, PartialEq)] pub struct Dependencies { pub known: HashMap, pub imports: HashMap, pub unknown: HashMap, } #[derive(Debug)] pub enum DependencyError { UnknownName(Id, SourceOffset), } impl Dependencies { pub fn identify<'a>(table: &DeclarationTable, known_imports: &HashSet, id: &(dyn IdLike + 'a), pos: SourceOffset) -> Dependencies { let mut visited = HashMap::new(); let mut imports = HashMap::new(); let mut unknown = HashMap::new(); match table.get(id) { None => { // No traversal necessary. unknown.insert(id.to_owned(), pos); } Some(initial) => { let mut frontier = vec!((initial, pos)); while let Some((current, pos)) = frontier.pop() { if visited.contains_key(&*current.id_like()) { visited.entry(current.to_id()).and_modify(|old_pos: &mut SourceOffset| { *old_pos = (*old_pos).min(pos); }); continue; } let deps = current.dependencies(); for (dep, pos) in deps { if let Some(next) = table.get(&dep) { frontier.push((next, pos)); } else if known_imports.contains(&dep) { imports.insert(dep, pos); } else { unknown.insert(dep, pos); } } visited.insert(current.to_id(), pos); } } } let mut known = visited; for imported_name in imports.keys() { known.remove(imported_name); } Dependencies { known, imports, unknown } } // TODO Rather than purge after the fact, we can use the imports // argument above to account for built-ins. pub fn purge_unknowns<'a, 'b, I>(&mut self, purge: I) where I : Iterator + 'b)>, 'b : 'a { for s in purge { self.unknown.remove(s); } } pub fn try_into_knowns(self) -> Result, DependencyError> { // Note: We explicitly don't care about imports here. As long as // all names are either knowns or imports (and there are no // unknowns), then we can safely discard the imports and keep only // the knowns, as the imports are already loaded elsewhere. if let Some((unknown, pos)) = self.unknown.into_iter().next() { Err(DependencyError::UnknownName(unknown, pos)) } else { Ok(self.known.into_iter().map(|(k, _)| k).collect()) } } } ================================================ FILE: src/ir/export.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Export and visibility rules for GDLisp modules. use super::decl::Decl; use super::identifier::Id; /// A name defined in a GDLisp module is either public or private. A /// private name is only accessible from the current module and cannot /// be imported into other modules. A public name can be imported and /// used in other modules. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Visibility { Public, Private } impl Visibility { /// Default visibility for function declarations. pub const FUNCTION: Visibility = Visibility::Public; /// Default visibility for macro declarations. pub const MACRO: Visibility = Visibility::Public; /// Default visibility for symbol macro declarations. pub const SYMBOL_MACRO: Visibility = Visibility::Public; /// Default visibility for constant declarations. pub const CONST: Visibility = Visibility::Public; /// Default visibility for class declarations. pub const CLASS: Visibility = Visibility::Public; /// Default visibility for object declarations. pub const OBJECT: Visibility = Visibility::Public; /// Default visibility for enum declarations. pub const ENUM: Visibility = Visibility::Public; /// Default visibility for `sys/declare` declarations. pub const DECLARE: Visibility = Visibility::Private; } /// Returns a vector of identifiers exported from the declarations /// `decls`. Private identifiers are *not* included in this vector. pub fn get_export_list<'a>(decls: impl IntoIterator) -> Vec { let mut exports = Vec::new(); for decl in decls { if decl.visibility() == Visibility::Public { exports.push(decl.to_id()); } } exports } ================================================ FILE: src/ir/expr.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::literal; use super::decl; use super::arglist::ordinary::ArgList; use super::closure_names::ClosureNames; use super::access_type::AccessType; use super::identifier::{Namespace, Id}; use super::special_ref::SpecialRef; use super::special_form::local_binding; use crate::sxp::ast::AST; use crate::sxp::literal::{Literal as ASTLiteral}; use crate::compile::names; use crate::pipeline::source::{SourceOffset, Sourced}; use crate::runner::path::RPathBuf; use std::collections::HashSet; use std::collections::hash_map::RandomState; use std::borrow::Cow; pub const DEFAULT_SPLIT_NAME: &str = "_split"; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ExprF { BareName(BareName), // A (possibly atomic) bare name, referring to a variable. Call(CallTarget, String, Vec), Literal(literal::Literal), Progn(Vec), CondStmt(Vec<(Expr, Option)>), WhileStmt(Box, Box), ForStmt(String, Box, Box), Let(Vec, Box), FunctionLet(FunctionBindingType, Vec, Box), Lambda(ArgList, Box), FuncRef(FuncRefTarget), Assign(AssignTarget, Box), Quote(AST), FieldAccess(Box, String), LambdaClass(Box), Yield(Option<(Box, Box)>), Assert(Box, Option>), Return(Box), Break, Continue, SpecialRef(SpecialRef), ContextualFilename(RPathBuf), Split(String, Box), // Compiles the inner expression, but forces it to be stored in a local variable with a generated name (the string argument is a prefix for the name) Preload(String), } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Expr { pub value: ExprF, pub pos: SourceOffset, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum AssignTarget { Variable(SourceOffset, String), InstanceField(SourceOffset, Box, String), } /// The object on which a function call is being made, if any. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallTarget { /// The call is being made on the current scope itself, not on any /// particular object. Scoped, /// The call is being made on the special `super` object. /// /// This does *not* include superclass *constructor* calls, which /// are handled by a special method modifier in the syntax itself. /// See /// [`compile_class_inner_decl`](crate::ir::incremental::IncCompiler::compile_class_inner_decl) /// for details on those super calls. Super, /// The call is being made on a function whose name is considered /// atomic. This is similar to `Scoped` but the name will not be /// considered during any semantic analysis and will be passed /// through to GDScript unmodified. Atomic, /// The call is being made on an ordinary object in GDLisp. That is, /// this is a method call. Object(Box), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum BareName { /// An ordinary name, referring to a variable in GDLisp. Plain(String), /// An atomic name, which will be translated literally into GDScript /// without any regard for semantics. Atomic(String), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum FuncRefTarget { SimpleName(String), } /// The type of binding to use in a function-namespaced let-binding, /// i.e. [`ExprF::FunctionLet`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FunctionBindingType { /// Outer-scoped binding, a la `flet`. Each function name is bound /// in an inner scope, with the function body interpreted in the /// outer scope that `flet` was invoked in. That is, functions /// cannot see themselves or other functions in the same `flet` /// block. OuterScoped, /// Recursive binding, a la `labels`. Each function name is bound in /// an inner scope, with the function body interpreted in that same /// inner scope. Functions in this binding type can see their own /// name and other functions from the same block. Recursive, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LambdaClass { pub extends: String, pub args: Vec, pub constructor: Option, pub decls: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LocalFnClause { pub name: String, pub args: ArgList, pub body: Expr, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LocalVarClause { pub name: String, pub value: Expr, } /// A collection of local variables, as well as the broadest /// [`AccessType`] the variables need. pub type Locals = ClosureNames; /// A collection of functions, either local or global. /// /// Unlike [`Locals`], `Functions` does not need to keep track of /// access types, since it is impossible to reassign a function in the /// function namespace after its been declared. It's possible to /// shadow functions with local ones, but this doesn't mutate the /// existing one and a closure around the existing function will still /// reflect the old value of the name. pub type Functions = ClosureNames<()>; impl Expr { pub fn new(value: ExprF, pos: SourceOffset) -> Expr { Expr { value, pos } } pub fn var(name: impl Into, pos: SourceOffset) -> Expr { Expr::new(ExprF::BareName(BareName::Plain(name.into())), pos) } pub fn atomic_var(name: impl Into, pos: SourceOffset) -> Expr { Expr::new(ExprF::BareName(BareName::Atomic(name.into())), pos) } pub fn while_stmt(cond: Expr, body: Expr, pos: SourceOffset) -> Expr { Expr::new(ExprF::WhileStmt(Box::new(cond), Box::new(body)), pos) } pub fn for_stmt(name: String, iter: Expr, body: Expr, pos: SourceOffset) -> Expr { Expr::new(ExprF::ForStmt(name, Box::new(iter), Box::new(body)), pos) } pub fn literal(literal: literal::Literal, pos: SourceOffset) -> Expr { Expr::new(ExprF::Literal(literal), pos) } pub fn nil(pos: SourceOffset) -> Expr { Expr::literal(literal::Literal::Nil, pos) } pub fn progn(body: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::Progn(body), pos) } pub fn call(name: impl Into, args: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::Call(CallTarget::Scoped, name.into(), args), pos) } pub fn atomic_call(name: impl Into, args: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::Call(CallTarget::Atomic, name.into(), args), pos) } pub fn super_call(name: impl Into, args: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::Call(CallTarget::Super, name.into(), args), pos) } pub fn method_call(self, name: impl Into, args: Vec, pos: SourceOffset) -> Expr { Expr::new(ExprF::Call(CallTarget::Object(Box::new(self)), name.into(), args), pos) } pub fn yield_none(pos: SourceOffset) -> Expr { Expr::new(ExprF::Yield(None), pos) } pub fn yield_some(lhs: Expr, rhs: Expr, pos: SourceOffset) -> Expr { Expr::new(ExprF::Yield(Some((Box::new(lhs), Box::new(rhs)))), pos) } pub fn assert_expr(cond: Expr, message: Option, pos: SourceOffset) -> Expr { Expr::new(ExprF::Assert(Box::new(cond), message.map(Box::new)), pos) } /// If `self` is a [`BareName::Plain`], then returns a reference to /// the inside of the name. If not, returns `None`. pub fn as_plain_name(&self) -> Option<&str> { if let ExprF::BareName(BareName::Plain(s)) = &self.value { Some(s) } else { None } } /// Converts the AST [`Literal`](crate::sxp::literal::Literal) value /// into an [`Expr`]. pub fn from_ast_literal(ast_literal: &ASTLiteral, pos: SourceOffset) -> Expr { match ast_literal { ASTLiteral::Nil => { Expr::new(ExprF::Literal(literal::Literal::Nil), pos) } ASTLiteral::Int(n) => { Expr::new(ExprF::Literal(literal::Literal::Int(*n)), pos) } ASTLiteral::Bool(b) => { Expr::new(ExprF::Literal(literal::Literal::Bool(*b)), pos) } ASTLiteral::Float(f) => { Expr::new(ExprF::Literal(literal::Literal::Float(*f)), pos) } ASTLiteral::String(s) => { Expr::new(ExprF::Literal(literal::Literal::String(s.to_owned())), pos) } ASTLiteral::Symbol(s) => { Expr::new(ExprF::Literal(literal::Literal::Symbol(s.to_owned())), pos) } } } /// Wraps the expression in a 0-ary lambda which is immediately /// invoked. /// /// If `expr` is the starting expression, then the result is /// conceptually `(funcall (lambda () expr))`. This is used in /// certain contexts in compilation, such as in a parent constructor /// invocation, where we have expression context but have nowhere to /// place helper statements produced by a /// [`StmtBuilder`](crate::compile::body::builder::StmtBuilder). pub fn self_evaluating_lambda(self) -> Expr { let pos = self.pos; Expr::call( String::from("sys/funcall"), vec!(Expr::new(ExprF::Lambda(ArgList::empty(), Box::new(self)), pos)), pos, ) } pub fn from_value(value: T, pos: SourceOffset) -> Expr where ExprF : From { Expr::new(ExprF::from(value), pos) } pub fn named_split(self, name: &str, pos: SourceOffset) -> Expr { Expr::new(ExprF::Split(name.to_owned(), Box::new(self)), pos) } pub fn split(self, pos: SourceOffset) -> Expr { self.named_split(DEFAULT_SPLIT_NAME, pos) } fn walk_locals(&self, acc_vars: &mut Locals, acc_fns: &mut Functions) { match &self.value { ExprF::BareName(name) => { match name { BareName::Plain(s) => { acc_vars.visit(s.to_owned(), AccessType::Read, self.pos); } BareName::Atomic(_) => { // Never try to reason about these; they have no semantics // by definition. } } } ExprF::Literal(_) => {} ExprF::Progn(exprs) => { for expr in exprs { expr.walk_locals(acc_vars, acc_fns); } } ExprF::CondStmt(clauses) => { for clause in clauses { clause.0.walk_locals(acc_vars, acc_fns); if let Some(body) = &clause.1 { body.walk_locals(acc_vars, acc_fns); } } } ExprF::WhileStmt(cond, body) => { cond.walk_locals(acc_vars, acc_fns); body.walk_locals(acc_vars, acc_fns); } ExprF::ForStmt(var, iter, body) => { let mut local_vars = Locals::new(); iter.walk_locals(acc_vars, acc_fns); body.walk_locals(&mut local_vars, acc_fns); local_vars.remove(var); acc_vars.merge_with(local_vars); } ExprF::Call(object, name, args) => { match object { CallTarget::Scoped => { acc_fns.visit(name.to_owned(), (), self.pos); } CallTarget::Atomic => { // Do not log any additional names, other than those // visited in the arguments. } CallTarget::Super => { // A super call implicitly requires read access to a // `self` variable. acc_vars.visit(String::from("self"), AccessType::Read, self.pos); } CallTarget::Object(object) => { object.walk_locals(acc_vars, acc_fns); } } for expr in args { expr.walk_locals(acc_vars, acc_fns); } } ExprF::Let(clauses, body) => { let mut vars = HashSet::new(); for clause in clauses { vars.insert(clause.name.to_owned()); clause.value.walk_locals(acc_vars, acc_fns); } let mut local_scope = Locals::new(); body.walk_locals(&mut local_scope, acc_fns); for (var, access_type, pos) in local_scope.into_iter_with_offset() { if !vars.contains(&var) { acc_vars.visit(var, access_type, pos); } } } ExprF::FunctionLet(FunctionBindingType::OuterScoped, clauses, body) => { let mut fns = HashSet::new(); for clause in clauses { let LocalFnClause { name, args, body: fbody } = clause; fns.insert(name.to_owned()); let lambda_body = ExprF::Lambda(args.to_owned(), Box::new(fbody.to_owned())); Expr::new(lambda_body, self.pos).walk_locals(acc_vars, acc_fns); } let mut local_scope = Functions::new(); body.walk_locals(acc_vars, &mut local_scope); for (func, (), pos) in local_scope.into_iter_with_offset() { if !fns.contains(&func) { acc_fns.visit(func, (), pos); } } } ExprF::FunctionLet(FunctionBindingType::Recursive, clauses, body) => { let mut fns = HashSet::new(); let mut local_scope = Functions::new(); for clause in clauses { let LocalFnClause { name, args, body: fbody } = clause; fns.insert(name.to_owned()); let lambda_body = ExprF::Lambda(args.to_owned(), Box::new(fbody.to_owned())); Expr::new(lambda_body, self.pos).walk_locals(acc_vars, &mut local_scope); } body.walk_locals(acc_vars, &mut local_scope); for (func, (), pos) in local_scope.into_iter_with_offset() { if !fns.contains(&func) { acc_fns.visit(func, (), pos); } } } ExprF::Lambda(args, body) => { let vars: HashSet<_, RandomState> = args.iter_vars().map(|x| x.to_owned()).collect(); let mut local_scope = Locals::new(); body.walk_locals(&mut local_scope, acc_fns); for (var, access_type, pos) in local_scope.into_iter_with_offset() { if !vars.contains(&var) { acc_vars.visit(var, access_type.closed(), pos); } } } ExprF::Assign(target, expr) => { match target { AssignTarget::Variable(pos, s) => { acc_vars.visit(s.to_owned(), AccessType::RW, *pos); } AssignTarget::InstanceField(_, lhs, _) => { lhs.walk_locals(acc_vars, acc_fns); // If the LHS is specifically a variable, then that // variable also becomes RW, since it might contain a COW // value. if let Some(v) = lhs.as_plain_name() { acc_vars.visit(v.to_owned(), AccessType::RW, lhs.pos); } } } expr.walk_locals(acc_vars, acc_fns); } ExprF::FuncRef(target) => { match target { FuncRefTarget::SimpleName(name) => acc_fns.visit(name.to_owned(), (), self.pos), } } ExprF::Quote(_) => {} ExprF::FieldAccess(lhs, _) => { lhs.walk_locals(acc_vars, acc_fns); } ExprF::LambdaClass(cls) => { let LambdaClass { extends, args, constructor, decls } = &**cls; for arg in args { arg.walk_locals(acc_vars, acc_fns); } acc_vars.visit(extends.to_owned(), AccessType::ClosedRead, self.pos); let (mut con_vars, con_fns) = constructor.as_ref().map_or_else(|| (ClosureNames::new(), ClosureNames::new()), |x| x.get_names()); for (_, access_type) in con_vars.iter_mut() { *access_type = access_type.closed(); } acc_vars.merge_with(con_vars); acc_fns.merge_with(con_fns); for decl in decls { let (mut decl_vars, decl_fns) = decl.get_names(); for (_, access_type) in decl_vars.iter_mut() { *access_type = access_type.closed(); } acc_vars.merge_with(decl_vars); acc_fns.merge_with(decl_fns); } } ExprF::Yield(arg) => { if let Some((x, y)) = arg { x.walk_locals(acc_vars, acc_fns); y.walk_locals(acc_vars, acc_fns); } } ExprF::Assert(cond, message) => { cond.walk_locals(acc_vars, acc_fns); if let Some(message) = message { message.walk_locals(acc_vars, acc_fns); } } ExprF::Return(expr) => { expr.walk_locals(acc_vars, acc_fns); } ExprF::Break => {} ExprF::Continue => {} ExprF::SpecialRef(_) => {} ExprF::ContextualFilename(_) => {} ExprF::Split(_, expr) => { // The "name" of the split is not a GDLisp-level name; it's a // hint to the later stages of the compiler. So it doesn't // matter for our purposes. expr.walk_locals(acc_vars, acc_fns); } ExprF::Preload(_) => {} }; } // Returns all of the variable names which appear unbound in the // current scope. Crucially, this excludes names which are bound to // lambda arguments or let instantiations. pub fn get_locals(&self) -> Locals { self.get_names().0 } // Returns all of the function names which appear unbound in the // current scope. pub fn get_functions(&self) -> Functions { self.get_names().1 } pub fn get_names(&self) -> (Locals, Functions) { let mut vars = Locals::new(); let mut fns = Functions::new(); self.walk_locals(&mut vars, &mut fns); (vars, fns) } pub fn get_ids(&self) -> impl Iterator { let (vars, fns) = self.get_names(); let vars = vars.into_iter_with_offset().map(|(name, _, pos)| (Id::new(Namespace::Value, name), pos)); let fns = fns.into_iter_with_offset().map(|(name, (), pos)| (Id::new(Namespace::Function, name), pos)); Iterator::chain(vars, fns) } } impl LambdaClass { /// See [`decl::ClassDecl::constructor_or_default`]. pub fn constructor_or_default(&self, default_pos: SourceOffset) -> Cow { self.constructor.as_ref().map_or_else(|| Cow::Owned(decl::ConstructorDecl::empty(default_pos)), Cow::Borrowed) } } impl Sourced for Expr { type Item = ExprF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &ExprF { &self.value } } impl From for ExprF where literal::Literal: From { fn from(value: T) -> ExprF { ExprF::Literal(literal::Literal::from(value)) } } impl BareName { pub fn to_gd_name(&self) -> String { match self { BareName::Plain(name) => names::lisp_to_gd(name), BareName::Atomic(name) => name.to_owned(), } } pub fn to_gd_name_bare(&self) -> String { match self { BareName::Plain(name) => names::lisp_to_gd_bare(name), BareName::Atomic(name) => name.to_owned(), } } } impl FunctionBindingType { /// Returns a correct [`local_binding::LocalBinding`] implementation /// for the function binding type. /// /// This function is the two-sided inverse of /// [`local_binding::LocalBinding::function_binding_type`]. pub fn into_local_binding(self) -> Box { match self { FunctionBindingType::OuterScoped => Box::new(local_binding::FLetLocalBinding), FunctionBindingType::Recursive => Box::new(local_binding::LabelsLocalBinding), } } } #[cfg(test)] mod tests { use super::*; use literal::Literal; fn lhash(vec: Vec) -> Locals { Locals::from_hashmap(vec.into_iter().map(|x| (x, (AccessType::Read, SourceOffset(0)))).collect()) } fn lhash_rw(vec: Vec<(String, AccessType)>) -> Locals { Locals::from_hashmap(vec.into_iter().map(|(x, t)| (x, (t, SourceOffset(0)))).collect()) } fn fhash(vec: Vec) -> Functions { Functions::from_hashmap(vec.into_iter().map(|x| (x, ((), SourceOffset(0)))).collect()) } fn lvc(name: &str, value: Expr) -> LocalVarClause { LocalVarClause { name: name.to_owned(), value, } } // For most of these tests, we don't care about SourceOffset, so // let's just make it easier to construct values with SourceOffset // of 0. fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } #[test] fn test_locals_simple() { assert_eq!(Expr::var("foobar", SourceOffset(0)).get_locals(), lhash(vec!("foobar".to_owned()))); assert_eq!(Expr::var("aaa", SourceOffset(0)).get_locals(), lhash(vec!("aaa".to_owned()))); assert_eq!(e(ExprF::Literal(Literal::Int(99))).get_locals(), lhash(vec!())); assert_eq!(e(ExprF::Literal(Literal::Nil)).get_locals(), lhash(vec!())); assert_eq!(e(ExprF::Progn(vec!())).get_locals(), lhash(vec!())); } #[test] fn test_locals_compound() { let progn = e(ExprF::Progn(vec!(Expr::var("aa", SourceOffset(0)), Expr::var("bb", SourceOffset(0))))); assert_eq!(progn.get_locals(), lhash(vec!("aa".to_owned(), "bb".to_owned()))); } #[test] fn test_locals_super_call() { let super_call = Expr::super_call("foobar", vec!(Expr::var("aa", SourceOffset(0))), SourceOffset(0)); assert_eq!(super_call.get_locals(), lhash(vec!("aa".to_owned(), "self".to_owned()))); } #[test] fn test_locals_let() { // Declared variable let e1 = e(ExprF::Let(vec!(lvc("var", e(ExprF::Literal(Literal::Nil)))), Box::new(e(ExprF::Literal(Literal::Nil))))); assert_eq!(e1.get_locals(), lhash(vec!())); // Declared and used variable let e2 = e(ExprF::Let(vec!(lvc("var", e(ExprF::Literal(Literal::Nil)))), Box::new(Expr::var("var", SourceOffset(0))))); assert_eq!(e2.get_locals(), lhash(vec!())); // Different variable let e3 = e(ExprF::Let(vec!(lvc("var_unused", e(ExprF::Literal(Literal::Nil)))), Box::new(Expr::var("var1", SourceOffset(0))))); assert_eq!(e3.get_locals(), lhash(vec!("var1".to_owned()))); // Variable in decl let e4 = e(ExprF::Let(vec!(lvc("var_unused", Expr::var("var", SourceOffset(0)))), Box::new(e(ExprF::Literal(Literal::Nil))))); assert_eq!(e4.get_locals(), lhash(vec!("var".to_owned()))); // Variable in decl (soon to be shadowed) let e4 = e(ExprF::Let(vec!(lvc("var", Expr::var("var", SourceOffset(0)))), Box::new(e(ExprF::Literal(Literal::Nil))))); assert_eq!(e4.get_locals(), lhash(vec!("var".to_owned()))); } #[test] fn test_locals_assignment() { // Simple assignment let e1 = e(ExprF::Assign(AssignTarget::Variable(SourceOffset::default(), String::from("var")), Box::new(e(ExprF::Literal(Literal::Nil))))); assert_eq!(e1.get_locals(), lhash_rw(vec!(("var".to_owned(), AccessType::RW)))); // Assignment including RHS let e2 = e(ExprF::Assign(AssignTarget::Variable(SourceOffset::default(), String::from("var1")), Box::new(Expr::var("var2", SourceOffset(0))))); assert_eq!(e2.get_locals(), lhash_rw(vec!(("var1".to_owned(), AccessType::RW), ("var2".to_owned(), AccessType::Read)))); // Reading and writing (I) let e3 = e(ExprF::Progn(vec!( e(ExprF::Assign(AssignTarget::Variable(SourceOffset::default(), String::from("var")), Box::new(e(ExprF::Literal(Literal::Nil))))), Expr::var("var", SourceOffset(0)), ))); assert_eq!(e3.get_locals(), lhash_rw(vec!(("var".to_owned(), AccessType::RW)))); // Reading and writing (II) let e4 = e(ExprF::Progn(vec!( Expr::var("var", SourceOffset(0)), e(ExprF::Assign(AssignTarget::Variable(SourceOffset::default(), String::from("var")), Box::new(e(ExprF::Literal(Literal::Nil))))), ))); assert_eq!(e4.get_locals(), lhash_rw(vec!(("var".to_owned(), AccessType::RW)))); } #[test] fn test_locals_slot_assignment() { // Simple slot assignment let e1 = e(ExprF::Assign( AssignTarget::InstanceField(SourceOffset::default(), Box::new(Expr::var("var", SourceOffset(0))), String::from("foo")), Box::new(e(ExprF::Literal(Literal::Nil))), )); assert_eq!(e1.get_locals(), lhash_rw(vec!(("var".to_owned(), AccessType::RW)))); // Nested slot assignment let e2 = e(ExprF::Assign( AssignTarget::InstanceField(SourceOffset::default(), Box::new(e(ExprF::FieldAccess(Box::new(Expr::var("var", SourceOffset(0))), String::from("foo")))), String::from("baro")), Box::new(e(ExprF::Literal(Literal::Nil))), )); assert_eq!(e2.get_locals(), lhash_rw(vec!(("var".to_owned(), AccessType::Read)))); } #[test] fn test_functions_trivial() { let e1 = e(ExprF::Literal(Literal::Int(1))); assert_eq!(e1.get_functions(), fhash(vec!())); } #[test] fn test_functions_calls() { let e1 = Expr::call("abc", vec!(Expr::call("def", vec!(), SourceOffset(0))), SourceOffset(0)); assert_eq!(e1.get_functions(), fhash(vec!("abc".to_owned(), "def".to_owned()))); } #[test] fn test_functions_ref() { let e1 = e(ExprF::FuncRef(FuncRefTarget::SimpleName("abc".to_owned()))); assert_eq!(e1.get_functions(), fhash(vec!("abc".to_owned()))); } #[test] fn test_function_binding_type_local_binding_inverse() { assert_eq!(FunctionBindingType::OuterScoped.into_local_binding().function_binding_type(), FunctionBindingType::OuterScoped); assert_eq!(FunctionBindingType::Recursive.into_local_binding().function_binding_type(), FunctionBindingType::Recursive); } } ================================================ FILE: src/ir/identifier.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! This module defines the [`Id`] type, for namespaced identifiers. use serde::{Serialize, Deserialize}; use std::borrow::{Borrow, ToOwned}; use std::hash::{Hash, Hasher}; /// An identifier consists of a namespace and a name. Two identifiers /// which happen to share a name but are in different namespaces are /// considered unrelated and distinct names. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Id { /// The identifier namespace. pub namespace: Namespace, /// The identifier name. pub name: String, } /// There are two namespaces in GDLisp: the value namespace and the /// function namespace. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Namespace { /// The value namespace consists of variables (local and global), /// class declarations, enums, and constants. Value, /// The function namespace consists of functions (local and global) /// and macros. Function, } /// While [`Namespace`] suffices for most of GDLisp, there is /// technically a third scope that sometimes applies, that of signals. /// Signals cannot be referenced by name at runtime, as they don't /// actually exist in GDScript in that form. But for the purposes of /// name resolution, there are occasions where we need to check them /// for correctness. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ClassNamespace { /// See [`Namespace::Value`]. Value, /// See [`Namespace::Function`]. Function, /// A signal declaration, i.e. the result of a `defsignal` declaration. Signal, } // This trait is a specialized version of KeyPair from this // StackOverflow answer. We specialize it to work on &str. // // https://stackoverflow.com/a/45795699/2288659 /// [`Id`] is frequently used as the key type in some kind of /// associative data structure. `Id`, however, requires ownership of /// its string value, which is not always ideal for simple lookups. /// /// Similar to how `String` keys in a hashmap can be queried with a /// `&str`, this trait allows `Id` keys in a hashmap or similar /// structure to be queried by any `IdLike` implementor, most namely a /// tuple `(Namespace, &str)`. Any type which has a [`Namespace`] and /// a `&str` name can implement this trait. The trait object type `dyn /// IdLike` implements the necessary [`Borrow`] and [`ToOwned`] traits /// to be used to reference `Id` keys in a hashmap. pub trait IdLike { /// The underlying namespace type. type NS; /// Gets the namespace for `self`. [`Namespace`] is a [`Copy`] type, /// so we return by value here, since returning a `&Namespace` is /// pointless and verbose. fn namespace(&self) -> Self::NS; /// Gets the name for `self`, by reference. fn name(&self) -> &str; } impl From for ClassNamespace { fn from(ns: Namespace) -> ClassNamespace { match ns { Namespace::Value => ClassNamespace::Value, Namespace::Function => ClassNamespace::Function, } } } impl Id { /// A new `Id` value with the given name and namespace. pub fn new(namespace: Namespace, name: String) -> Self { Id { namespace, name } } /// A new [`IdLike`] value with the given name and namespace. This /// function does not take ownership of `name`, nor does it clone /// it. pub fn build<'a, NS: Clone + 'a>(namespace: NS, name: &'a str) -> Box + 'a> { Box::new((namespace, name)) } } impl Namespace { pub fn name(self) -> &'static str { ClassNamespace::from(self).name() } } impl ClassNamespace { pub fn name(self) -> &'static str { match self { ClassNamespace::Value => "value", ClassNamespace::Function => "function", ClassNamespace::Signal => "signal", } } } impl IdLike for Id { type NS = Namespace; fn namespace(&self) -> Namespace { self.namespace } fn name(&self) -> &str { &self.name } } impl IdLike for (NS, &str) { type NS = NS; fn namespace(&self) -> NS { self.0.clone() } fn name(&self) -> &str { self.1 } } impl IdLike for (NS, String) { type NS = NS; fn namespace(&self) -> NS { self.0.clone() } fn name(&self) -> &str { &self.1 } } impl<'a> Borrow + 'a> for Id { fn borrow(&self) -> &(dyn IdLike + 'a) { self } } impl<'a, NS: Clone + 'a> Borrow + 'a> for (NS, String) { fn borrow(&self) -> &(dyn IdLike + 'a) { self } } impl<'a, NS: Hash> Hash for (dyn IdLike + 'a) { fn hash(&self, state: &mut H) { self.namespace().hash(state); self.name().hash(state); } } impl<'a, NS: PartialEq> PartialEq for (dyn IdLike + 'a) { fn eq(&self, other: &Self) -> bool { self.namespace() == other.namespace() && self.name() == other.name() } } impl<'a, NS: PartialEq> Eq for (dyn IdLike + 'a) {} impl<'a> ToOwned for (dyn IdLike + 'a) { type Owned = Id; fn to_owned(&self) -> Id { Id::new(self.namespace(), self.name().to_owned()) } } #[cfg(test)] mod tests { use super::*; use std::collections::{HashMap, HashSet}; #[test] fn test_id_in_hashmap() { let mut container: HashMap = HashMap::new(); container.insert(Id::new(Namespace::Function, String::from("foobar")), 945); assert_eq!(container.get(&*Id::build(Namespace::Function, "foobar")), Some(&945)); } #[test] fn test_id_in_hashset() { let mut container: HashSet = HashSet::new(); container.insert(Id::new(Namespace::Function, String::from("foobar"))); assert!(container.contains(&*Id::build(Namespace::Function, "foobar"))); } } ================================================ FILE: src/ir/import.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::sxp::ast::{AST, ASTF}; use crate::sxp::literal::Literal; use crate::sxp::dotted::DottedExpr; use crate::runner::path::{RPathBuf, PathSrc}; use crate::pipeline::source::SourceOffset; use super::identifier::{Namespace, Id, IdLike}; use std::convert::{TryInto, TryFrom}; use std::fmt; use std::error::Error; // Import syntax: // // (1) Qualified import // (use "res://example/foo.lisp") // Imports "example/foo.lisp" as example/foo // // (2) Qualified import (aliased) // (use "res://example/foo.lisp" as renamed-foo) // Imports "example/foo.lisp" as renamed-foo // // (3) Explicit import // (use "res://example/foo.lisp" (a b c (d function))) // Imports functions a, b, c from "example/foo.lisp" // // (4) Explicit import (aliased) // (use "res://example/foo.lisp" ((a as a1) (b as b1) (c as c1) (d value as d1))) // Imports functions a, b, c as a1, b1, c1 from "example/foo.lisp" // // (5) Wildcard import // (use "res://example/foo.lisp" open) // Imports all names into the current scope #[derive(Clone, Debug, Eq, PartialEq)] pub struct ImportDecl { pub filename: RPathBuf, pub details: ImportDetails, pub pos: SourceOffset, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum ImportDetails { Named(String), // (1) and (2) above Restricted(Vec>>), // (3) and (4) above Open, // (5) above } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ImportName { pub namespace: NS, pub in_name: String, pub out_name: String, } #[derive(Debug, PartialEq, Eq)] pub enum ImportDeclParseError { NoFilename, BadFilename(AST), InvalidPath(String), MalformedFunctionImport(AST), InvalidEnding(AST), } #[derive(Debug)] pub enum ImportNameResolutionError { UnknownName(Id), AmbiguousNamespace(String), } impl ImportDecl { pub fn default_import_name(path: &RPathBuf) -> String { let mut path = path.clone(); path.path_mut().set_extension(""); path .components_no_root() .filter_map(|x| x.as_os_str().to_str()) .last() .unwrap_or("/") .to_owned() } pub fn parse_path_param(arg: &str) -> Option { // Paths must start with "res://" RPathBuf::try_from(String::from(arg)).ok().filter(|path| { path.source() == PathSrc::Res }) } pub fn named(filename: RPathBuf, name: Option, pos: SourceOffset) -> ImportDecl { let name = name.unwrap_or_else(|| ImportDecl::default_import_name(&filename)); ImportDecl { filename: filename, details: ImportDetails::Named(name), pos, } } pub fn restricted(filename: RPathBuf, imports: Vec>>, pos: SourceOffset) -> ImportDecl { ImportDecl { filename: filename, details: ImportDetails::Restricted(imports), pos, } } pub fn open(filename: RPathBuf, pos: SourceOffset) -> ImportDecl { ImportDecl { filename: filename, details: ImportDetails::Open, pos, } } pub fn names(&self, exports: &[Id]) -> Vec> { exports.iter().cloned().filter_map(|export| { let Id { namespace: export_namespace, name: export_name } = export; let import_name = match &self.details { ImportDetails::Named(s) => { Some(format!("{}/{}", s, export_name)) } ImportDetails::Open => { Some(export_name.clone()) } ImportDetails::Restricted(vec) => { // Find it in the import list. vec.iter() .find(|x| x.namespace.map_or(true, |ns| ns == export_namespace) && x.out_name == *export_name) .map(|name_match| name_match.in_name.clone()) } }; import_name.map(|import_name| { ImportName::new(export_namespace, import_name, export_name) }) }).collect() } pub fn parse(tail: &[&AST]) -> Result { if tail.is_empty() { return Err(ImportDeclParseError::NoFilename); } let filename = match &tail[0].value { ASTF::Atom(Literal::String(s)) => ImportDecl::parse_path_param(s).ok_or_else(|| { ImportDeclParseError::InvalidPath(s.clone()) }), _ => Err(ImportDeclParseError::BadFilename(tail[0].clone())), }?; match tail.len() { 0 => { unreachable!() } // We checked tail.is_empty() already 1 => { // (1) Qualified import Ok(ImportDecl::named(filename, None, tail[0].pos)) } 2 => { match &tail[1].value { ASTF::Atom(Literal::Symbol(open)) if open == "open" => { // (5) Wildcard import Ok(ImportDecl::open(filename, tail[0].pos)) } ASTF::Atom(Literal::Nil) | ASTF::Cons(_, _) => { // (3) or (4) Explicit import (possibly aliased) let imports: Vec<_> = DottedExpr::new(tail[1]).try_into().map_err(|_| invalid_ending_err(&tail[1..], tail[1].pos))?; let imports = imports.into_iter() .map(ImportName::>::parse) .collect::, _>>()?; Ok(ImportDecl::restricted(filename, imports, tail[0].pos)) } _ => { Err(invalid_ending_err(&tail[1..], tail[1].pos)) } } } 3 => { // (2) Qualified import (aliased) if tail[1].value != ASTF::symbol("as") { return Err(invalid_ending_err(&tail[1..], tail[1].pos)); } match &tail[2].value { ASTF::Atom(Literal::Symbol(s)) => Ok(ImportDecl::named(filename, Some(s.clone()), tail[0].pos)), _ => Err(invalid_ending_err(&tail[1..], tail[1].pos)) } } _ => { Err(invalid_ending_err(&tail[1..], tail[1].pos)) } } } } impl ImportName { pub fn new(namespace: NS, in_name: String, out_name: String) -> ImportName { ImportName { namespace, in_name, out_name } } pub fn simple(namespace: NS, in_name: String) -> ImportName { let out_name = in_name.clone(); ImportName { namespace, in_name, out_name } } fn symbol_to_namespace(symbol: &str, pos: SourceOffset) -> Result { match symbol { "value" => Ok(Namespace::Value), "function" => Ok(Namespace::Function), _ => Err(ImportDeclParseError::MalformedFunctionImport(AST::new(ASTF::symbol(symbol), pos))), } } pub fn parse(clause: &AST) -> Result>, ImportDeclParseError> { match &clause.value { ASTF::Atom(Literal::Symbol(s)) => { Ok(ImportName::simple(None, s.clone())) } ASTF::Cons(_, _) => { let vec: Vec<_> = DottedExpr::new(clause).try_into().map_err(|_| ImportDeclParseError::MalformedFunctionImport(clause.clone()))?; let shape_vec: Option> = vec .into_iter() .map(|x| x.as_symbol_ref()) .collect(); match shape_vec.as_deref() { Some([o, "as", i]) => { Ok(ImportName::new(None, (*i).to_owned(), (*o).to_owned())) } Some([o, ns, "as", i]) => { let ns = ImportName::>::symbol_to_namespace(ns, clause.pos)?; Ok(ImportName::new(Some(ns), (*i).to_owned(), (*o).to_owned())) } Some([o, ns]) => { let ns = ImportName::>::symbol_to_namespace(ns, clause.pos)?; Ok(ImportName::new(Some(ns), (*o).to_owned(), (*o).to_owned())) } _ => { Err(ImportDeclParseError::MalformedFunctionImport(clause.clone())) } } } _ => { Err(ImportDeclParseError::MalformedFunctionImport(clause.clone())) } } } } impl ImportName> { pub fn refine(&self, exports: &[Id]) -> Result, ImportNameResolutionError> { let mut matches = Vec::new(); for export_id in exports { if self.namespace.map_or(true, |ns| ns == export_id.namespace) && export_id.name == self.out_name { matches.push(export_id); } } if matches.is_empty() { Err(ImportNameResolutionError::UnknownName(Id::new(self.namespace.unwrap_or(Namespace::Function), self.out_name.to_owned()))) } else if matches.len() == 1 { Ok(ImportName::new(matches[0].namespace, self.in_name.to_owned(), self.out_name.to_owned())) } else { Err(ImportNameResolutionError::AmbiguousNamespace(self.out_name.to_owned())) } } } impl ImportName { pub fn into_imported_id(self) -> Id { Id::new(self.namespace, self.in_name) } pub fn into_exported_id(self) -> Id { Id::new(self.namespace, self.out_name) } pub fn to_imported_id<'a>(&'a self) -> Box + 'a> { Id::build(self.namespace, &self.in_name) } pub fn to_exported_id<'a>(&'a self) -> Box + 'a> { Id::build(self.namespace, &self.out_name) } } fn invalid_ending_err(tail: &[&AST], pos: SourceOffset) -> ImportDeclParseError { let ending: Vec = tail.iter().map(|x| (*x).clone()).collect(); ImportDeclParseError::InvalidEnding(AST::dotted_list(ending, AST::nil(pos))) } impl fmt::Display for ImportDeclParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ImportDeclParseError::NoFilename => { write!(f, "Expected filename in import") } ImportDeclParseError::BadFilename(ast) => { write!(f, "Not a valid filename in import {}", ast) } ImportDeclParseError::InvalidPath(s) => { write!(f, "Not a valid path in import {}", s) } ImportDeclParseError::MalformedFunctionImport(ast) => { write!(f, "Malformed function import {}", ast) } ImportDeclParseError::InvalidEnding(ast) => { write!(f, "Invalid end of import {}", ast) } } } } impl fmt::Display for ImportNameResolutionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ImportNameResolutionError::UnknownName(id) => { write!(f, "Unknown name in import resolution {}", id.name) } ImportNameResolutionError::AmbiguousNamespace(name) => { write!(f, "Ambiguous namespace at {} in import", name) } } } } impl Error for ImportDeclParseError {} #[cfg(test)] mod tests { use super::*; use crate::AST_PARSER; fn parse_ast(input: &str) -> AST { AST_PARSER.parse(input).unwrap() } fn parse_import(input: &str) -> Result { let ast = parse_ast(input); let dotted: Vec<_> = DottedExpr::new(&ast).try_into().unwrap(); ImportDecl::parse(&dotted) } fn str_to_rpathbuf(input: &str) -> RPathBuf { RPathBuf::try_from(String::from(input)).unwrap() } fn so(x: usize) -> SourceOffset { SourceOffset(x) } #[test] fn default_import_name_test_relative() { assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("res://foo/bar")), "bar"); assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("res://foo/bar.lisp")), "bar"); assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("res://foo/bar.gd")), "bar"); assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("res://")), "/"); // Degenerate case } #[test] #[cfg(target_family = "windows")] fn default_import_name_test_absolute_windows() { assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("C:/a/b/c")), "c"); assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("C:/abcd")), "abcd"); } #[test] #[cfg(target_family = "unix")] fn default_import_name_test_absolute_unix() { assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("/a/b/c")), "c"); assert_eq!(ImportDecl::default_import_name(&str_to_rpathbuf("/abcd")), "abcd"); } #[test] fn test_parsing() { assert_eq!(parse_import(r#"("res://foo/bar")"#).unwrap(), ImportDecl::named(str_to_rpathbuf("res://foo/bar"), None, so(1))); assert_eq!(parse_import(r#"("res://foo/bar")"#).unwrap(), ImportDecl::named(str_to_rpathbuf("res://foo/bar"), Some(String::from("bar")), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" as foo)"#).unwrap(), ImportDecl::named(str_to_rpathbuf("res://foo/bar"), Some(String::from("foo")), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" as foo.baz)"#).unwrap(), ImportDecl::named(str_to_rpathbuf("res://foo/bar"), Some(String::from("foo.baz")), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" open)"#).unwrap(), ImportDecl::open(str_to_rpathbuf("res://foo/bar"), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" (a b))"#).unwrap(), ImportDecl::restricted(str_to_rpathbuf("res://foo/bar"), vec!(ImportName::simple(None, String::from("a")), ImportName::simple(None, String::from("b"))), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" ())"#).unwrap(), ImportDecl::restricted(str_to_rpathbuf("res://foo/bar"), vec!(), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" ((a as a1) b))"#).unwrap(), ImportDecl::restricted(str_to_rpathbuf("res://foo/bar"), vec!(ImportName::new(None, String::from("a1"), String::from("a")), ImportName::simple(None, String::from("b"))), so(1))); assert_eq!(parse_import(r#"("res://foo/bar" ((a function as a1) (b value)))"#).unwrap(), ImportDecl::restricted(str_to_rpathbuf("res://foo/bar"), vec!(ImportName::new(Some(Namespace::Function), String::from("a1"), String::from("a")), ImportName::simple(Some(Namespace::Value), String::from("b"))), so(1))); } #[test] fn test_invalid_parsing() { assert!(parse_import(r#"(10)"#).is_err()); assert!(parse_import(r#"("foo/bar")"#).is_err()); assert!(parse_import(r#"("res://foo/bar" as a as b)"#).is_err()); assert!(parse_import(r#"("res://foo/bar" as 1)"#).is_err()); assert!(parse_import(r#"("res://foo/bar" open-NOT-CORRECT)"#).is_err()); assert!(parse_import(r#"("res://foo/bar" (1))"#).is_err()); assert!(parse_import(r#"("res://foo/bar" ((a as)))"#).is_err()); assert!(parse_import(r#"("res://foo/bar" ((a b c)))"#).is_err()); assert!(parse_import(r#"("res://foo/bar" ([]))"#).is_err()); assert!(parse_import(r#"("res://foo/bar" (()))"#).is_err()); } } ================================================ FILE: src/ir/incremental.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . // Incremental compilation (supplies backbone for macro resolution) use super::declaration_table::DeclarationTable; use super::main_function::MainFunctionHandler; use super::call_name::CallName; use super::classification; use super::arglist; use super::arglist::error::ArgListParseError; use super::arglist::ordinary::ArgList; use super::arglist::constructor::ConstructorArgList; use super::arglist::simple::SimpleArgList; use super::import::{ImportDecl, ImportName}; use super::expr::Expr; use super::special_form; use super::depends::Dependencies; use super::decl::{self, Decl, DeclF, InstanceFunctionName}; use super::macros::{self, MacroData}; use super::identifier::{Id, IdLike, Namespace}; use super::modifier::{self, ParseRule}; use super::export::Visibility; use super::bootstrapping::{compile_bootstrapping_decl, compile_bootstrapping_class_inner_decl}; use crate::util::one::One; use crate::sxp::dotted::{DottedExpr, TryFromDottedExprError}; use crate::sxp::ast::{AST, ASTF}; use crate::sxp::literal::{Literal as ASTLiteral}; use crate::sxp::reify::pretty::reify_pretty_expr; use crate::compile::symbol_table::SymbolTable; use crate::compile::symbol_table::function_call::FnSpecs; use crate::compile::error::{GDError, GDErrorF}; use crate::compile::resource_type::ResourceType; use crate::compile::args::{self, Expecting, ExpectedShape}; use crate::compile::names; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::names::generator::NameGenerator; use crate::compile::frame::MAX_QUOTE_REIFY_DEPTH; use crate::compile::body::class_initializer::InitTime; use crate::gdscript::library; use crate::gdscript::metadata; use crate::gdscript::decl::Static; use crate::pipeline::error::{PError, IOError}; use crate::pipeline::Pipeline; use crate::pipeline::translation_unit::TranslationUnit; use crate::pipeline::source::SourceOffset; use std::convert::{TryFrom, TryInto}; use std::borrow::{Cow, Borrow}; use std::hash::Hash; use std::collections::{HashMap, HashSet}; pub struct IncCompiler { names: FreshNameGenerator, table: DeclarationTable, macros: HashMap, imports: Vec, ambient_symbols: SymbolTable, minimalist: bool, // A minimalist compiler doesn't use stdlib and doesn't do macro expansion } #[allow(clippy::new_without_default)] impl IncCompiler { pub fn new(names: Vec<&str>) -> IncCompiler { IncCompiler::with_ambient_symbols(names, SymbolTable::new()) } pub fn with_ambient_symbols(names: Vec<&str>, ambient_symbols: SymbolTable) -> IncCompiler { let names = FreshNameGenerator::new(names); IncCompiler { names: names, table: DeclarationTable::new(), macros: HashMap::new(), imports: vec!(), ambient_symbols: ambient_symbols, minimalist: false, } } fn resolve_macro_call(&mut self, pipeline: &mut Pipeline, head: &K, tail: &[&AST], macro_data: &MacroData, pos: SourceOffset) -> Result where Id : Borrow, K : Hash + Eq + ToOwned + ?Sized { // If we're in a minimalist file, then macro expansion is automatically an error if self.minimalist { let head = head.to_owned().name; return Err(PError::from(GDError::new(GDErrorF::MacroInMinimalistError(head), pos))); } // Local name generator that will be shared among the // arguments but not used outside of this function. let mut local_gen = FreshNameGenerator::new(vec!()); let mut prelude = vec!(); let mut args = vec!(); for arg in tail { let (stmts, expr) = reify_pretty_expr(arg, MAX_QUOTE_REIFY_DEPTH, &mut local_gen); prelude.extend(stmts.into_iter()); args.push(expr); } let server = pipeline.get_server_mut(); server.set_global_name_generator(&self.names).map_err(|err| IOError::new(err, pos))?; let ast = server.run_server_file(macro_data.id, prelude, macro_data.specs, args, pos); server.reset_global_name_generator().map_err(|err| IOError::new(err, pos))?; // Set the source position for the entire macro expansion to // be the source of the macro call let mut ast = ast?; ast.each_source_mut(|_| pos); Ok(ast) } fn try_resolve_macro_call(&mut self, pipeline: &mut Pipeline, ast: &AST) -> Result, PError> { let vec: Vec<&AST> = match DottedExpr::new(ast).try_into() { Err(TryFromDottedExprError { pos: _ }) => return Ok(None), Ok(v) => v, }; if !vec.is_empty() { let head = CallName::resolve_call_name(self, pipeline, vec[0])?; if let CallName::SimpleName(head) = head { let tail = &vec[1..]; if let Some(macro_data) = self.macros.get(&*Id::build(Namespace::Function, &head)) { let macro_data = macro_data.to_owned(); return self.resolve_macro_call(pipeline, &*Id::build(Namespace::Function, &head), tail, ¯o_data, ast.pos).map(Some); } } } Ok(None) } fn try_resolve_symbol_macro_call(&mut self, pipeline: &mut Pipeline, head: &str, pos: SourceOffset) -> Result, PError> { if let Some(macro_data) = self.macros.get(&*Id::build(Namespace::Value, head)) { let macro_data = macro_data.to_owned(); return self.resolve_macro_call(pipeline, &*Id::build(Namespace::Value, head), &[], ¯o_data, pos).map(Some); } Ok(None) } pub fn resolve_simple_call(&mut self, pipeline: &mut Pipeline, head: &str, tail: &[&AST], pos: SourceOffset) -> Result { if let Some(sf) = special_form::dispatch_form(self, pipeline, head, tail, pos)? { Ok(sf) } else if let Some(macro_data) = self.macros.get(&*Id::build(Namespace::Function, head)) { let macro_data = macro_data.to_owned(); let result = self.resolve_macro_call(pipeline, &*Id::build(Namespace::Function, head), tail, ¯o_data, pos)?; self.compile_expr(pipeline, &result) } else { let args = tail.iter().map(|x| self.compile_expr(pipeline, x)).collect::, _>>()?; Ok(Expr::call(head, args, pos)) } } pub fn compile_expr(&mut self, pipeline: &mut Pipeline, expr: &AST) -> Result { match &expr.value { ASTF::Cons(_, _) => { let vec: Vec<&AST> = DottedExpr::new(expr).try_into()?; assert!(!vec.is_empty(), "Internal error in compile_expr, expecting non-empty list, got {:?}", &expr); let head = CallName::resolve_call_name(self, pipeline, vec[0])?; let tail = &vec[1..]; head.into_expr(self, pipeline, tail, expr.pos) } ASTF::Atom(lit) => { // We handle symbols specially, but for any other literal, we // simply delegate to [`Expr::from_ast_literal`]. if let ASTLiteral::Symbol(s) = lit { // Symbol macro resolution let macro_result = self.try_resolve_symbol_macro_call(pipeline, s, expr.pos)?; if let Some(macro_result) = macro_result { self.compile_expr(pipeline, ¯o_result) } else { Ok(Expr::var(s, expr.pos)) } } else { Ok(Expr::from_ast_literal(lit, expr.pos)) } } } } pub fn compile_decl(&mut self, pipeline: &mut Pipeline, acc: &mut impl Extend, decl: &AST) -> Result<(), PError> { let vec: Vec<&AST> = DottedExpr::new(decl).try_into()?; if vec.is_empty() { return Err(PError::from(GDError::new(GDErrorF::UnknownDecl(decl.clone()), decl.pos))); } match &vec[0].value { ASTF::Atom(ASTLiteral::Symbol(s)) => { match s.borrow() { "sys/min-godot-version" => { // TODO In principle, this should be a macro that the user // can use directly (`ifc`), but that requires us to be // able to use macros in stdlib (#103), which is not // currently possible. So it's a sys/ directive for now. Expecting::at_least(1).validate("sys/min-godot-version", decl.pos, &vec[1..])?; let min_version = ExpectedShape::extract_i32("sys/min-godot-version", vec[1].clone())?; let actual_version = pipeline.config().godot_version.version; if actual_version.into_i32() >= min_version { for decl in &vec[2..] { self.compile_decl(pipeline, acc, decl)?; } } Ok(()) } "defn" => { Expecting::at_least(2).validate("defn", decl.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defn", vec[1].clone())?; let args: Vec<_> = DottedExpr::new(vec[2]).try_into()?; let args = ArgList::parse(args, decl.pos)?; let (mods, body) = modifier::function::parser().parse(&vec[3..])?; let body = body.iter().map(|expr| self.compile_expr(pipeline, expr)).collect::, _>>()?; let mut decl = decl::FnDecl { visibility: Visibility::FUNCTION, call_magic: None, name: name, args: args, body: Expr::progn(body, vec[0].pos), }; for m in mods { m.apply(&mut decl); } acc.extend(One(Decl::new(DeclF::FnDecl(decl), vec[0].pos))); Ok(()) } "defmacro" => { Expecting::at_least(2).validate("defmacro", decl.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defmacro", vec[1].clone())?; let args: Vec<_> = DottedExpr::new(vec[2]).try_into()?; let args = ArgList::parse(args, decl.pos)?; let (mods, body) = modifier::macros::parser().parse(&vec[3..])?; let body = body.iter().map(|expr| self.compile_expr(pipeline, expr)).collect::, _>>()?; let mut decl = decl::MacroDecl { visibility: Visibility::MACRO, name: name, args: args, body: Expr::progn(body, vec[0].pos), }; for m in mods { m.apply(&mut decl); } acc.extend(One(Decl::new(DeclF::MacroDecl(decl), vec[0].pos))); Ok(()) } "define-symbol-macro" => { Expecting::at_least(2).validate("define-symbol-macro", decl.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("define-symbol-macro", vec[1].clone())?; let value = self.compile_expr(pipeline, vec[2])?; let (mods, body) = modifier::macros::parser().parse(&vec[3..])?; ExpectedShape::validate_end_of_list("define-symbol-macro", body, decl.pos)?; let mut decl = decl::SymbolMacroDecl { visibility: Visibility::SYMBOL_MACRO, name: name, body: value, }; for m in mods { m.apply_to_symbol_macro(&mut decl); } acc.extend(One(Decl::new(DeclF::SymbolMacroDecl(decl), vec[0].pos))); Ok(()) } "defconst" => { Expecting::at_least(2).validate("defconst", decl.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defconst", vec[1].clone())?; let value = self.compile_expr(pipeline, vec[2])?; let (mods, body) = modifier::constant::parser().parse(&vec[3..])?; ExpectedShape::validate_end_of_list("defconst", body, decl.pos)?; let mut decl = decl::ConstDecl { visibility: Visibility::CONST, name, value }; for m in mods { m.apply(&mut decl); } acc.extend(One(Decl::new(DeclF::ConstDecl(decl), vec[0].pos))); Ok(()) } "defclass" => { Expecting::at_least(2).validate("defclass", decl.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defclass", vec[1].clone())?; let superclass = match &vec[2].value { ASTF::Atom(ASTLiteral::Nil) => library::REFERENCE_NAME.to_owned(), ASTF::Cons(car, cdr) => match (&car.value, &cdr.value) { (ASTF::Atom(ASTLiteral::Symbol(superclass_name)), ASTF::Atom(ASTLiteral::Nil)) => superclass_name.to_owned(), (_, ASTF::Atom(ASTLiteral::Nil)) => return Err(PError::from(GDError::new(GDErrorF::BadExtendsClause, vec[2].pos))), _ => return Err(PError::from(GDError::new(GDErrorF::BadExtendsClause, vec[2].pos))), }, _ => return Err(PError::from(GDError::new(GDErrorF::BadExtendsClause, decl.pos))), }; let (mods, decl_body) = modifier::class::parser().parse(&vec[3..])?; let mut class = decl::ClassDecl::new(name, superclass); for m in mods { m.apply(&mut class); } for decl in decl_body { self.compile_class_inner_decl(pipeline, &mut class, decl)?; } acc.extend(One(Decl::new(DeclF::ClassDecl(class), vec[0].pos))); Ok(()) } "defenum" => { Expecting::at_least(1).validate("defenum", decl.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defenum", vec[1].clone())?; let (mods, body) = modifier::enums::parser().parse(&vec[2..])?; let clauses = body.iter().map(|clause| { let clause = match &clause.value { ASTF::Atom(ASTLiteral::Symbol(_)) => AST::dotted_list(vec!((*clause).clone()), AST::nil(clause.pos)), _ => (*clause).clone(), }; let clause = Vec::try_from(DottedExpr::new(&clause))?; let (name, value) = match &clause[..] { [name] => (name, None), [name, value] => (name, Some(value)), _ => return Err(PError::from(GDError::new(GDErrorF::BadEnumClause, decl.pos))), }; let name = ExpectedShape::extract_symbol("defenum", (*name).to_owned())?; let value = value.map(|v| self.compile_expr(pipeline, v)).transpose()?; Ok((name, value)) }).collect::>()?; let mut enum_decl = decl::EnumDecl { visibility: Visibility::ENUM, name, clauses }; for m in mods { m.apply(&mut enum_decl); } acc.extend(One(Decl::new(DeclF::EnumDecl(enum_decl), vec[0].pos))); Ok(()) } "sys/declare" => { // (sys/declare value name) // (sys/declare function name (args...)) Expecting::at_least(2).validate("sys/declare", decl.pos, &vec[1..])?; let value_type = ExpectedShape::extract_symbol("sys/declare", vec[1].clone())?; let (mut declare, body) = match &*value_type { "value" | "constant" | "superglobal" => { let (name, target_name) = IncCompiler::get_declare_decl_name(vec[2])?; let declare_type = match &*value_type { "value" => decl::DeclareType::Value, "constant" => decl::DeclareType::Constant, "superglobal" => decl::DeclareType::Superglobal, _ => unreachable!(), }; let decl = decl::DeclareDecl { visibility: Visibility::DECLARE, declare_type, name, target_name, }; (decl, &vec[3..]) } "function" | "superfunction" => { Expecting::at_least(3).validate("sys/declare", decl.pos, &vec[1..])?; let (name, target_name) = IncCompiler::get_declare_decl_name(vec[2])?; let args: Vec<_> = DottedExpr::new(vec[3]).try_into()?; let args = ArgList::parse(args, decl.pos)?; let declare_type = if value_type == "superfunction" { decl::DeclareType::SuperglobalFn(args) } else { decl::DeclareType::Function(args) }; let decl = decl::DeclareDecl { visibility: Visibility::DECLARE, declare_type, name, target_name, }; (decl, &vec[4..]) } value_type => { return Err(PError::from(GDError::new(GDErrorF::BadSysDeclare(value_type.to_owned()), decl.pos))) } }; let (mods, body) = modifier::declare::parser().parse(body)?; ExpectedShape::validate_end_of_list("sys/declare", body, decl.pos)?; for m in mods { m.apply(&mut declare); } acc.extend(One(Decl::new(DeclF::DeclareDecl(declare), vec[0].pos))); Ok(()) } "sys/bootstrap" => { // (sys/bootstrap directive) Expecting::exactly(1).validate("sys/bootstrap", decl.pos, &vec[1..])?; let directive = ExpectedShape::extract_symbol("sys/bootstrap", vec[1].clone())?; compile_bootstrapping_decl(self, pipeline, acc, &directive, decl.pos)?; Ok(()) } _ => { Err(PError::from(GDError::new(GDErrorF::UnknownDecl(decl.clone()), decl.pos))) } } } _ => { Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("(declaration)"), vec[0].clone(), ExpectedShape::Symbol), decl.pos))) } } } // TODO Do we need to take two tables here (static and non-static) like we do in compile? pub fn compile_class_inner_decl(&mut self, pipeline: &mut Pipeline, acc: &mut decl::ClassDecl, curr: &AST) -> Result<(), PError> { // Deal with macros let mut candidate: Option; let mut curr = curr; while let Some(ast) = self.try_resolve_macro_call(pipeline, curr)? { candidate = Some(ast); curr = candidate.as_ref().unwrap(); } let vec = Vec::try_from(DottedExpr::new(curr)).map_err(|x| GDError::from_value(x, curr.pos))?; if vec.is_empty() { return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("(declaration)"), curr.clone(), ExpectedShape::NonemptyList), curr.pos))); } if let Some(s) = vec[0].as_symbol_ref() { match s { "progn" => { // Top-level magic progn for d in &vec[1..] { self.compile_class_inner_decl(pipeline, acc, d)?; } Ok(()) } "defconst" => { // TODO Combine this with the other defconst in a helper function Expecting::exactly(2).validate("defconst", curr.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defconst", vec[1].clone())?; let value = self.compile_expr(pipeline, vec[2])?; acc.decls.push(decl::ClassInnerDecl::new(decl::ClassInnerDeclF::ClassConstDecl(decl::ConstDecl { visibility: Visibility::CONST, name, value }), vec[0].pos)); Ok(()) } "defvar" => { Expecting::at_least(1).validate("defvar", curr.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defvar", vec[1].clone())?; // Parse value and export let mut value = None; let mut export = None; let mut idx = 2; // Value if let Some(v) = vec.get(idx) { if !(matches!(&v.value, ASTF::Cons(car, _) if car.value == ASTF::symbol("export"))) { let e = self.compile_expr(pipeline, v)?; value = Some(e); idx += 1; } }; // Export if let Some(v) = vec.get(idx) { if let Ok(elements) = Vec::<&AST>::try_from(DottedExpr::new(v)) { if elements.get(0).map(|x| &x.value) == Some(&ASTF::symbol("export")) { let args = elements[1..].iter().map(|x| self.compile_expr(pipeline, x)).collect::>()?; export = Some(decl::Export { args }); idx += 1; } } } // Modifiers let (mods, body) = modifier::var::parser().parse(&vec[idx..])?; // (Extra) ExpectedShape::validate_end_of_list("defvar", body, curr.pos)?; // Exports are only allowed on the main class if export.is_some() && !acc.main_class { return Err(PError::from(GDError::new(GDErrorF::ExportOnInnerClassVar(name), curr.pos))); } let decl = { let mut decl = decl::ClassVarDecl { export, name, value, init_time: InitTime::default() }; for m in mods { m.apply(&mut decl); } decl }; acc.decls.push(decl::ClassInnerDecl::new(decl::ClassInnerDeclF::ClassVarDecl(decl), vec[0].pos)); Ok(()) } "defn" => { // TODO Unify some of this with the equivalent code from compile_decl? Expecting::at_least(2).validate("defn", curr.pos, &vec[1..])?; if let Some(fname) = InstanceFunctionName::parse(vec[1]) { let args: Vec<_> = DottedExpr::new(vec[2]).try_into()?; let args = arglist::parser::parse(args)?; // Determine if static let (mods, body) = modifier::instance_method::parser().parse(&vec[3..])?; // Check for super call (only valid in constructor, but we // run the check unconditionally) let (super_call, body) = classification::detect_super(body); let super_call = match super_call { None => None, Some(super_call) => { let call = super_call.arguments.iter() .map(|expr| self.compile_expr(pipeline, expr)) .collect::, _>>()?; Some(decl::SuperCall { call, pos: super_call.pos }) } }; let body = body.iter().map(|expr| self.compile_expr(pipeline, expr)).collect::, _>>()?; if fname.is_constructor_function() { // Constructor // There can only be one constructor defined in the class if acc.constructor.is_some() { return Err(PError::from(GDError::new(GDErrorF::DuplicateConstructor, vec[0].pos))); } let args = ConstructorArgList::try_from(args).map_err(|err| { ArgListParseError::new(err, vec[0].pos) })?; let super_call = super_call.unwrap_or_else(|| decl::SuperCall::empty(vec[0].pos)); let mut constructor = decl::ConstructorDecl { args, super_call, body: Expr::progn(body, vec[0].pos) }; for m in mods { m.apply_to_constructor(&mut constructor)?; } acc.constructor = Some(constructor); } else { // Ordinary functions cannot have init super calls (The // `(super:foo)` syntax is handled separately). let args = SimpleArgList::try_from(args).map_err(|err| { ArgListParseError::new(err, vec[0].pos) })?; if let Some(super_call) = &super_call { return Err(PError::from(GDError::new(GDErrorF::BadSuperCall(String::from("(init)")), super_call.pos))); } let mut decl = decl::ClassFnDecl { is_static: Static::NonStatic, is_nullargs: false, name: fname, args, body: Expr::progn(body, vec[0].pos), }; for m in mods { m.apply(&mut decl); } acc.decls.push(decl::ClassInnerDecl::new(decl::ClassInnerDeclF::ClassFnDecl(decl), vec[0].pos)); } Ok(()) } else { Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("defn"), vec[1].clone(), ExpectedShape::InstanceFnName), vec[1].pos))) } } "defsignal" => { Expecting::between(1, 2).validate("defsignal", curr.pos, &vec[1..])?; let name = ExpectedShape::extract_symbol("defsignal", vec[1].clone())?; let nil = AST::nil(vec[0].pos); let args = vec.get(2).map_or(&nil, |x| *x); let args_pos = args.pos; let args: Vec<_> = DottedExpr::new(args).try_into()?; let args = SimpleArgList::parse(args, args_pos)?; acc.decls.push(decl::ClassInnerDecl::new(decl::ClassInnerDeclF::ClassSignalDecl(decl::ClassSignalDecl { name, args }), vec[0].pos)); Ok(()) } "sys/bootstrap" => { // (sys/bootstrap directive) Expecting::exactly(1).validate("sys/bootstrap", curr.pos, &vec[1..])?; let directive = ExpectedShape::extract_symbol("sys/bootstrap", vec[1].clone())?; compile_bootstrapping_class_inner_decl(self, pipeline, acc, &directive, curr.pos)?; Ok(()) } _ => { Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("(declaration)"), vec[0].clone(), ExpectedShape::Symbol), curr.pos))) } } } else { Err(PError::from(GDError::new(GDErrorF::UnknownDecl(curr.clone()), curr.pos))) } } pub fn compile_import(&mut self, curr: &AST) -> Result, GDError> { if let Ok(vec) = Vec::try_from(DottedExpr::new(curr)) { if !vec.is_empty() && vec[0].value == ASTF::symbol("use") { return ImportDecl::parse(&vec[1..]).map_err(|x| GDError::from_value(x, curr.pos)).map(Some); } } Ok(None) } fn import_macros_from(&mut self, unit: &TranslationUnit, import: &ImportDecl) { for imp in import.names(&unit.exports) { let ImportName { namespace: namespace, in_name: import_name, out_name: export_name } = imp; if let Some(data) = unit.macros.get(&*Id::build(namespace, &export_name)) { self.macros.insert(Id::new(namespace, import_name), data.to_imported()); }; } } fn compile_decl_or_expr(&mut self, pipeline: &mut Pipeline, main: &mut Vec, curr: &AST) -> Result<(), PError> { let mut candidate: Option; // Just need somewhere to store the intermediate. let mut curr = curr; // Change lifetime :) while let Some(ast) = self.try_resolve_macro_call(pipeline, curr)? { candidate = Some(ast); curr = candidate.as_ref().unwrap(); } // Check if we're looking at a top-level progn. if let Ok(vec) = Vec::try_from(DottedExpr::new(curr)) { if !vec.is_empty() && vec[0].value == ASTF::symbol("progn") { for inner in &vec[1..] { self.compile_decl_or_expr(pipeline, main, inner)?; } return Ok(()); } } if let Some(imp) = self.compile_import(curr)? { // TODO Consider doing the is_decl thing with imports so we have a nice, pure function called is_import to call here instead? let res_type = ResourceType::from(&imp); if res_type.can_have_macros() { let file = pipeline.load_file(imp.filename.path(), curr.pos)?; self.import_macros_from(file, &imp); } self.imports.push(imp); } else if classification::is_decl(curr) { let mut new_decls: Vec = Vec::new(); // Most declaration-level constructs produce exactly one // declaration, but there are a handful (mainly bootstrapping // primitives) that can produce several. self.compile_decl(pipeline, &mut new_decls, curr)?; for d in new_decls { self.table.add_unless_exists(d.clone())?; if let DeclF::MacroDecl(mdecl) = d.value { self.bind_macro(pipeline, mdecl, d.pos, false, Namespace::Function)?; } else if let DeclF::SymbolMacroDecl(mdecl) = d.value { let mdecl = decl::MacroDecl::from(mdecl); self.bind_macro(pipeline, mdecl, d.pos, true, Namespace::Value)?; } } } else { main.push(self.compile_expr(pipeline, curr)?); } Ok(()) } pub fn compile_toplevel(mut self, pipeline: &mut Pipeline, body: &AST, main_function_handler: &impl MainFunctionHandler) -> Result<(decl::TopLevel, HashMap), PError> { let body: Result, TryFromDottedExprError> = DottedExpr::new(body).try_into(); let body: Vec<_> = body?; // *sigh* Sometimes the type checker just doesn't get it ... // File-level modifiers let (mods, body) = modifier::file::parser().parse(&body)?; for m in mods { m.apply(&mut self); } self.bind_builtin_macros(pipeline); // No-op if minimalist is true. // Compile let mut main: Vec = Vec::new(); for curr in body { self.compile_decl_or_expr(pipeline, &mut main, curr)?; } main_function_handler.handle_main(&mut self, &main)?; Ok(self.into()) } pub fn bind_macro(&mut self, pipeline: &mut Pipeline, mut decl: decl::MacroDecl, pos: SourceOffset, generate_name: bool, namespace: Namespace) -> Result<(), PError> { let orig_name = decl.name.to_owned(); let tmp_name = if generate_name { self.names.generate_with("_macro") } else { orig_name.to_owned() }; // bind_macro is a no-op in a minimalist compile if self.minimalist { let id = pipeline.get_server_mut().add_reserved_macro(names::lisp_to_gd(&orig_name)); self.macros.insert(Id::new(namespace, orig_name), MacroData { id, specs: FnSpecs::from(decl.args), imported: false }); return Ok(()); } let translation_names = self.imports.iter().map(|import| { let res_type = ResourceType::from(import); if res_type.can_have_macros() { let unit = pipeline.load_file(&import.filename.path(), pos)?; Ok(import.names(&unit.exports)) } else { Ok(vec!()) } }).collect::, PError>>()?; let mut imported_names: HashSet<_> = translation_names.into_iter().flatten().map(ImportName::into_imported_id).collect(); // Now add any ambient symbols (most commonly, names defined in // prior lines in a REPL) to the known imports. for (name, _) in self.ambient_symbols.vars() { imported_names.insert(Id::new(Namespace::Value, name.to_owned())); } for (name, _, _) in self.ambient_symbols.fns() { imported_names.insert(Id::new(Namespace::Function, name.to_owned())); } // If we're generating a name, then we need to modify the symbol // table to reflect that name. decl.name = tmp_name.to_owned(); let table = if generate_name { let mut table = self.table.clone(); match namespace { Namespace::Function => { table.add_unless_exists(Decl::new(DeclF::MacroDecl(decl.to_owned()), pos))?; } Namespace::Value => { let symdecl = decl::SymbolMacroDecl { visibility: decl.visibility, name: decl.name.to_owned(), body: decl.body.clone() }; table.add_unless_exists(Decl::new(DeclF::SymbolMacroDecl(symdecl), pos))?; } } Cow::Owned(table) } else { Cow::Borrowed(&self.table) }; // Now we need to find the dependencies and spawn up the // server for the macro itself. let mut deps = Dependencies::identify(table.borrow(), &imported_names, &*Id::build(namespace, &tmp_name), pos); deps.purge_unknowns(library::all_builtin_names(self.minimalist).iter().map(|x| x as &dyn IdLike)); let runtime_name = if namespace == Namespace::Value { metadata::symbol_macro(&tmp_name) } else { tmp_name }; // Aside from built-in functions, it must be the case that // all referenced functions are already defined. let names = deps.try_into_knowns()?; let tmpfile = macros::create_macro_file(pipeline, self.imports.clone(), table.borrow(), names, &self.ambient_symbols, pos, self.minimalist)?; let m_id = pipeline.get_server_mut().stand_up_macro(runtime_name, tmpfile).map_err(|err| IOError::new(err, pos))?; self.macros.insert(Id::new(namespace, orig_name), MacroData { id: m_id, specs: FnSpecs::from(decl.args), imported: false }); Ok(()) } pub fn locally_save_macro(&mut self, name: &K, func: impl FnOnce(&mut Self) -> B) -> B where Id : Borrow, K : Hash + Eq + ToOwned + ?Sized { let saved_value = self.macros.remove(name); let result = func(self); if let Some(saved_value) = saved_value { self.macros.insert(name.to_owned(), saved_value); } result } pub fn has_macro(&self, name: &K) -> bool where Id : Borrow, K : Hash + Eq + ToOwned + ?Sized { self.macros.contains_key(name) } pub fn unbind_macro(&mut self, name: &K) where Id : Borrow, K : Hash + Eq + ToOwned + ?Sized { self.macros.remove(name); } pub fn bind_builtin_macros(&mut self, pipeline: &mut Pipeline) { if !self.minimalist { library::bind_builtin_macros(&mut self.macros, pipeline); } } pub fn bind_macros_from(&mut self, existing_macros: impl IntoIterator) { self.macros.extend(existing_macros.into_iter()) } pub fn declaration_table(&mut self) -> &mut DeclarationTable { &mut self.table } pub fn mark_as_minimalist(&mut self) { self.minimalist = true; } pub fn name_generator(&mut self) -> &mut FreshNameGenerator { &mut self.names } /// The name of the declaration in a `sys/declare` form can take one /// of two forms. It can either be a single symbol literal or a list /// of two symbol literals. In the former case, the symbol is /// considered to be both the GDLisp name and the target GDScript /// name, with the latter being escaped using `lisp_to_gd`. In the /// latter case, the first symbol is the GDLisp name, and the second /// is the target GDScript name, escaped with `lisp_to_gd_bare` /// (note that we use the "bare" variant in this case, to allow /// explicit overriding of GDScript identifiers in this case). /// /// This function returns the GDLisp name and, if present, the /// GDScript target name. No escaping is done by this function. fn get_declare_decl_name(form: &AST) -> Result<(String, Option), GDError> { fn inner(form: &AST) -> Option<(String, Option)> { if let Some(name) = form.as_symbol_ref() { // Single symbol; target GDScript name should be determined // automatically. Some((name.to_owned(), None)) } else { let list: Vec<&AST> = DottedExpr::new(form).try_into().ok()?; Expecting::exactly(2).validate("sys/declare", form.pos, &list).ok()?; let (name, target_name) = args::two(list); if let (Some(name), Some(target_name)) = (name.as_symbol_ref(), target_name.as_symbol_ref()) { Some((name.to_owned(), Some(target_name.to_owned()))) } else { None } } } // Replace any errors with the expected shape, since that will be // more helpful in this case than a vague "unexpected dotted list" // or something. inner(form).ok_or_else(|| { GDError::new( GDErrorF::InvalidArg(String::from("sys/declare"), form.to_owned(), ExpectedShape::SymbolOrPairOfSymbols), form.pos, ) }) } } impl From for (decl::TopLevel, HashMap) { fn from(compiler: IncCompiler) -> (decl::TopLevel, HashMap) { let toplevel = decl::TopLevel { imports: compiler.imports, decls: compiler.table.into(), minimalist_flag: compiler.minimalist, }; let macros: HashMap<_, _> = compiler.macros.into_iter().filter(|(_, x)| !x.imported).collect(); (toplevel, macros) } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::Pipeline; use crate::pipeline::config::ProjectConfig; use crate::pipeline::resolver::PanickingNameResolver; use crate::runner::version::VersionInfo; use std::path::PathBuf; // TODO More tests fn dummy_config() -> ProjectConfig { ProjectConfig { root_directory: PathBuf::from(r"."), optimizations: false, godot_version: VersionInfo::default(), } } fn dummy_pipeline() -> Pipeline { Pipeline::with_resolver(dummy_config(), Box::new(PanickingNameResolver)) } #[test] fn bad_call_test() { let mut icompiler = IncCompiler::new(vec!()); icompiler.mark_as_minimalist(); let mut pipeline = dummy_pipeline(); let ast = AST::list( vec!( AST::from_value(5, SourceOffset(500)), AST::from_value(6, SourceOffset(600)), ), SourceOffset(0), ); assert_eq!( icompiler.compile_expr(&mut pipeline, &ast), Err(PError::from(GDError::new(GDErrorF::CannotCall(AST::from_value(5, SourceOffset(500))), SourceOffset(500)))), ); } } ================================================ FILE: src/ir/literal.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! GDLisp literal values. use ordered_float::OrderedFloat; /// A GDLisp literal value. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Literal { Nil, Int(i32), Float(OrderedFloat), String(String), Symbol(String), Bool(bool), } impl From for Literal { fn from(value: i32) -> Literal { Literal::Int(value) } } impl From> for Literal { fn from(value: OrderedFloat) -> Literal { Literal::Float(value) } } impl From for Literal { fn from(value: String) -> Literal { Literal::String(value) } } impl<'a> From<&'a str> for Literal { fn from(value: &'a str) -> Literal { Literal::String(String::from(value)) } } impl From for Literal { fn from(value: bool) -> Literal { Literal::Bool(value) } } ================================================ FILE: src/ir/loops/error.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Error types during loop primitive validation. use crate::pipeline::source::{Sourced, SourceOffset}; use std::fmt; use std::error::Error; /// A `LoopPrimitiveErrorF` indicates that a looping construct, either /// `break` or `continue`, was found outside of the scope of a loop. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LoopPrimitiveErrorF { /// The primitive construct that was invalid. pub primitive: LoopPrimitive, /// If this is true, then the error occurred inside the lexical /// scope of a loop, but there was a closure in between the loop and /// the primitive. This is used to produce a better error message in /// this specific situation. pub is_in_closure: bool, } /// A [`LoopPrimitiveErrorF`] together with [`SourceOffset`] data. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LoopPrimitiveError { pub value: LoopPrimitiveErrorF, pub pos: SourceOffset, } #[derive(Clone, Debug, Copy, PartialEq, Eq)] pub enum LoopPrimitive { Break, Continue, } impl LoopPrimitiveError { pub fn new(value: LoopPrimitiveErrorF, pos: SourceOffset) -> LoopPrimitiveError { LoopPrimitiveError { value, pos } } pub fn break_error(pos: SourceOffset) -> LoopPrimitiveError { LoopPrimitiveError::new(LoopPrimitiveErrorF { primitive: LoopPrimitive::Break, is_in_closure: false, }, pos) } pub fn continue_error(pos: SourceOffset) -> LoopPrimitiveError { LoopPrimitiveError::new(LoopPrimitiveErrorF { primitive: LoopPrimitive::Continue, is_in_closure: false, }, pos) } pub fn in_closure(mut self) -> Self { self.value.is_in_closure = true; self } } impl Sourced for LoopPrimitiveError { type Item = LoopPrimitiveErrorF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &LoopPrimitiveErrorF { &self.value } } impl fmt::Display for LoopPrimitiveError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let LoopPrimitiveErrorF { primitive, is_in_closure } = &self.value; write!(f, "Loop primitive '{}' is not allowed outside of loops", primitive)?; if *is_in_closure { write!(f, " (Note: '{}' was found inside of a lambda or anonymous class that is nested inside of a loop; this nesting is currently not allowed)", primitive)?; } Ok(()) } } impl fmt::Display for LoopPrimitive { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { LoopPrimitive::Break => write!(f, "break"), LoopPrimitive::Continue => write!(f, "continue"), } } } impl Error for LoopPrimitiveError {} ================================================ FILE: src/ir/loops/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Functions for validating the use of looping primitives, //! specifically `break` and `continue`. pub mod error; use super::decl::TopLevel; use super::expr::{Expr, ExprF, AssignTarget, LambdaClass, CallTarget}; use error::{LoopPrimitiveError, LoopPrimitive, LoopPrimitiveErrorF}; use crate::pipeline::source::SourceOffset; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] enum LoopWalker { /// `LoopWalker::None` is the default state. If there are no loops /// that the current expression is inside, either directly or /// indirectly, then we are in this state. #[default] None, /// The state of expressions that are inside a loop, and for which /// there are no closures strictly between the current expression /// and the innermost enclosing loop. This is the only state in /// which it is permitted to use loop primitives. Within, /// The state of expressions that are inside a loop, but for which /// there is a closure strictly between the current expression and /// the innermost enclosing loop. Due to limitations in GDScript, we /// are not capable of compiling loop primitives in this state. ClosureWithin, } pub fn check_expr(expr: &Expr) -> Result<(), LoopPrimitiveError> { LoopWalker::new().check(expr) } pub fn check_all_exprs(toplevel: &TopLevel) -> Result<(), LoopPrimitiveError> { for expr in toplevel.inner_exprs() { check_expr(expr)?; } Ok(()) } impl LoopWalker { fn new() -> Self { Self::default() } fn enter_loop(self) -> Self { LoopWalker::Within } fn enter_closure(self) -> Self { if self == LoopWalker::None { // There's no loop at all, so don't change that fact. LoopWalker::None } else { LoopWalker::ClosureWithin } } fn check(self, expr: &Expr) -> Result<(), LoopPrimitiveError> { match &expr.value { ExprF::BareName(_) => {} ExprF::Literal(_) => {} ExprF::Progn(exprs) => { for inner in exprs { self.check(inner)?; } } ExprF::CondStmt(clauses) => { for (cond, body) in clauses { self.check(cond)?; if let Some(body) = body { self.check(body)?; } } } ExprF::WhileStmt(cond, body) => { // Note: Both the condition *and* the body count as being // inside the loop. We *can* break inside of a `while` // condition; we'll just end up compiling to the "full" while // loop, where the condition is a sequence of statements in a // `while True`. let walker = self.enter_loop(); walker.check(cond)?; walker.check(body)?; } ExprF::ForStmt(_, iter, body) => { // Contrary to `while`, the iteratee of a `for` loop is // *outside* the loop. A `break` in the iteratee expression is // meaningless. self.check(iter)?; self.enter_loop().check(body)?; } ExprF::Call(object, _, args) => { match object { CallTarget::Scoped | CallTarget::Super | CallTarget::Atomic => {} CallTarget::Object(inner) => { self.check(inner)?; } } for inner in args { self.check(inner)?; } } ExprF::Let(clauses, body) => { for clause in clauses { self.check(&clause.value)?; } self.check(body)?; } ExprF::FunctionLet(_, clauses, body) => { let closure_walker = self.enter_closure(); for clause in clauses { closure_walker.check(&clause.body)?; } self.check(body)?; } ExprF::Lambda(_, body) => { self.enter_closure().check(body)?; } ExprF::FuncRef(_) => {} ExprF::Assign(lhs, rhs) => { match lhs { AssignTarget::Variable(_, _) => {} AssignTarget::InstanceField(_, lhs, _) => { self.check(lhs)?; } } self.check(rhs)?; } ExprF::Quote(_) => {} ExprF::FieldAccess(lhs, _) => { self.check(lhs)?; } ExprF::LambdaClass(class) => { let walker = self.enter_closure(); let LambdaClass { extends: _, args, constructor, decls } = class.as_ref(); for inner in args { // Not inside the lambda class yet. self.check(inner)?; } if let Some(constructor) = constructor { for inner in &constructor.super_call.call { walker.check(inner)?; } walker.check(&constructor.body)?; } for expr in decls.iter().flat_map(|d| d.inner_exprs()) { walker.check(expr)?; } } ExprF::Yield(args) => { if let Some((a, b)) = args { self.check(a)?; self.check(b)?; } } ExprF::Assert(a, b) => { self.check(a)?; if let Some(b) = b { self.check(b)?; } } ExprF::Return(arg) => { self.check(arg)?; } ExprF::Break => { self.check_loop_primitive(LoopPrimitive::Break, expr.pos)?; } ExprF::Continue => { self.check_loop_primitive(LoopPrimitive::Continue, expr.pos)?; } ExprF::SpecialRef(_) => {} ExprF::ContextualFilename(_) => {} ExprF::Split(_, expr) => { self.check(expr)?; } ExprF::Preload(_) => {} }; Ok(()) } /// This function is called when the walker encounters a loop /// primitive, either `break` or `continue`. For a loop primitive to /// succeed, `self` *must* be [`LoopWalker::Within`]. If it's in /// either of the other states, then an appropriate error is issued. fn check_loop_primitive(self, primitive: LoopPrimitive, pos: SourceOffset) -> Result<(), LoopPrimitiveError> { match self { LoopWalker::None => { Err(LoopPrimitiveError::new( LoopPrimitiveErrorF { primitive, is_in_closure: false }, pos, )) } LoopWalker::Within => { // Everything is fine :) Ok(()) } LoopWalker::ClosureWithin => { Err(LoopPrimitiveError::new( LoopPrimitiveErrorF { primitive, is_in_closure: true }, pos, )) } } } } ================================================ FILE: src/ir/macros.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::compile::Compiler; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::body::builder::CodeBuilder; use crate::compile::body::class_scope::OutsideOfClass; use crate::compile::symbol_table::SymbolTable; use crate::compile::symbol_table::function_call::FnSpecs; use crate::gdscript::library; use crate::gdscript::class_extends::ClassExtends; use crate::runner::into_gd_file::IntoGDFile; use crate::runner::macro_server::named_file_server::MacroID; use crate::pipeline::error::{PError, IOError}; use crate::pipeline::Pipeline; use crate::pipeline::can_load::CanLoad; use crate::pipeline::source::SourceOffset; use crate::ir; use crate::ir::import::ImportDecl; use crate::ir::decl::TopLevel; use crate::ir::identifier::Id; use crate::ir::declaration_table::DeclarationTable; use tempfile::{NamedTempFile, Builder}; use serde::{Serialize, Deserialize}; use std::io::{self, Write}; use std::collections::HashSet; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MacroData { pub id: MacroID, pub specs: FnSpecs, pub imported: bool, } fn make_tmp() -> io::Result { Builder::new() .prefix("__gdlisp_macro") .suffix(".gd") .rand_bytes(5) .tempfile() } impl MacroData { pub fn to_imported(&self) -> MacroData { let mut result = self.clone(); result.imported = true; result } } pub fn create_macro_file(pipeline: &mut Pipeline, imports: Vec, src_table: &DeclarationTable, names: HashSet, existing_symbols: &SymbolTable, pos: SourceOffset, minimalist: bool) -> Result { let mut table = existing_symbols.clone(); library::bind_builtins(&mut table, minimalist); let current_filename = pipeline.current_filename(); let mut tmp_file = make_tmp().map_err(|err| IOError::new(err, pos))?; let mut resolver = pipeline.make_preload_resolver(); // Replace the current file name with the macro file name. resolver.insert(current_filename.into_path(), tmp_file.path().to_owned()); let mut compiler = Compiler::new(FreshNameGenerator::new(vec!()), Box::new(resolver), minimalist); let decls = Vec::from(src_table.filter(|d| names.contains(&*d.id_like()))); let toplevel = { let mut toplevel = TopLevel { imports, decls, minimalist_flag: minimalist }; // Strip main class qualifier; we don't need or want it during macro expansion. for d in &mut toplevel.decls { if let ir::decl::DeclF::ClassDecl(cdecl) = &mut d.value { cdecl.main_class = false; } } toplevel }; let mut builder = CodeBuilder::new(ClassExtends::SimpleIdentifier("Node".to_owned())); compiler.frame(pipeline, &mut builder, &mut table, &mut OutsideOfClass).compile_toplevel(&toplevel)?; let result = builder.build(); result.write_to_gd(&mut tmp_file).map_err(|err| IOError::new(err, pos))?; tmp_file.flush().map_err(|err| IOError::new(err, pos))?; Ok(tmp_file) } ================================================ FILE: src/ir/main_function.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Handlers for the main function in a GDLisp source file. use super::incremental::IncCompiler; use super::arglist::ordinary::ArgList; use super::expr::Expr; use super::decl::{Decl, DeclF, FnDecl}; use super::export::Visibility; use crate::pipeline::source::SourceOffset; use crate::compile::error::{GDError, GDErrorF}; /// A [`MainFunctionHandler`] which errs out if there are any /// top-level expressions in the file. This handler performs no action /// if the expression slice is empty. #[derive(Clone, Debug, Copy)] pub struct DisallowMainFunctionHandler; /// A [`MainFunctionHandler`] implementing the original behavior of /// GDLisp, accumulating all of the top-level expressions into a /// static function with the given name. /// /// This handler is not used in GDLisp production but is used in the /// testing scaffolding in order to be able to run expressions /// alongside declarations. It may be added back into GDLisp proper in /// the future. #[derive(Clone, Debug)] pub struct StaticMainFunctionHandler { pub function_name: String, } /// When the incremental compiler compiles a source file, it accepts /// top-level expressions and declarations. The declarations are /// handled by the incremental compiler, and the expressions are /// passed on to the `MainFunctionHandler` to determine what to do /// with them. /// /// The current default behavior in GDLisp is to err out if there are /// any expressions at the top-level, leaving the behavior available /// in future implementations. This implementation is provided by /// [`DisallowMainFunctionHandler`]. pub trait MainFunctionHandler { fn handle_main(&self, icompiler: &mut IncCompiler, main: &[Expr]) -> Result<(), GDError>; } impl MainFunctionHandler for DisallowMainFunctionHandler { fn handle_main(&self, _icompiler: &mut IncCompiler, main: &[Expr]) -> Result<(), GDError> { if main.is_empty() { Ok(()) } else { Err(GDError::new(GDErrorF::ExprAtTopLevel(main[0].clone()), main[0].pos)) } } } impl StaticMainFunctionHandler { pub fn new(function_name: String) -> Self { StaticMainFunctionHandler { function_name } } } impl MainFunctionHandler for StaticMainFunctionHandler { fn handle_main(&self, icompiler: &mut IncCompiler, main: &[Expr]) -> Result<(), GDError> { // Note: main_decl is synthesized from the file itself, so // SourceOffset(0) isn't just a cop-out here; it's the actual // right answer. let pos = SourceOffset(0); let main_decl = DeclF::FnDecl(FnDecl { visibility: Visibility::FUNCTION, call_magic: None, name: self.function_name.to_owned(), args: ArgList::empty(), body: Expr::progn(main.to_vec(), pos), }); let main_decl = Decl::new(main_decl, pos); icompiler.declaration_table().add(main_decl); Ok(()) } } ================================================ FILE: src/ir/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . // Intermediate representation used between the AST Lisp syntax and // the GDScript syntax. This representation shares all of the nice // expression semantics of Lisp but resolves any special forms to // something easily recognizable. // // NOTE: IR still exclusively uses the GDLisp names. Generating // GDScript names is the responsibility of the next compilation step. pub mod expr; pub mod decl; pub mod arglist; pub mod literal; pub mod special_form; pub mod declaration_table; pub mod incremental; pub mod depends; pub mod macros; pub mod quasiquote; pub mod call_name; pub mod import; pub mod export; pub mod identifier; pub mod modifier; pub mod access_type; pub mod closure_names; pub mod special_ref; pub mod scope; pub mod bootstrapping; pub mod loops; pub mod main_function; pub mod classification; use decl::Decl; use macros::MacroData; use identifier::Id; use main_function::MainFunctionHandler; use crate::sxp::ast::AST; use crate::compile::error::GDError; use crate::pipeline::error::PError; use crate::pipeline::Pipeline; use std::collections::HashMap; pub fn compile_toplevel(pipeline: &mut Pipeline, body: &AST, main_function_handler: &impl MainFunctionHandler) -> Result<(decl::TopLevel, HashMap), PError> { let compiler = incremental::IncCompiler::new(body.all_symbols()); compiler.compile_toplevel(pipeline, body, main_function_handler) } pub fn compile_and_check(pipeline: &mut Pipeline, body: &AST, main_function_handler: &impl MainFunctionHandler) -> Result<(decl::TopLevel, HashMap), PError> { let (ir, macros) = compile_toplevel(pipeline, body, main_function_handler)?; check_ir(&ir)?; Ok((ir, macros)) } pub fn check_ir(ir: &decl::TopLevel) -> Result<(), GDError> { scope::check_scopes(ir)?; loops::check_all_exprs(ir)?; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::ir::literal::Literal; use crate::ir::expr::{Expr, ExprF, AssignTarget}; use crate::ir::decl::DeclF; use crate::ir::arglist::ordinary::ArgList; use crate::ir::export::Visibility; use crate::pipeline::Pipeline; use crate::pipeline::config::ProjectConfig; use crate::pipeline::source::SourceOffset; use crate::runner::version::VersionInfo; use crate::sxp::ast::ASTF; use std::path::PathBuf; fn int(n: i32) -> AST { AST::new(ASTF::int(n), SourceOffset::default()) } fn symbol(s: &str) -> AST { AST::new(ASTF::symbol(s), SourceOffset::default()) } #[allow(dead_code)] fn string(s: &str) -> AST { AST::new(ASTF::string(s), SourceOffset::default()) } fn nil() -> AST { AST::nil(SourceOffset::default()) } #[allow(dead_code)] fn cons(a: AST, b: AST) -> AST { AST::new(ASTF::cons(a, b), SourceOffset::default()) } fn list(data: Vec) -> AST { AST::dotted_list(data, nil()) } fn call(name: &str, args: Vec) -> Expr { Expr::call(String::from(name), args, SourceOffset::default()) } fn literal(literal: Literal) -> Expr { Expr::literal(literal, SourceOffset::default()) } fn progn(body: Vec) -> Expr { Expr::progn(body, SourceOffset::default()) } fn compile_expr(pipeline: &mut Pipeline, expr: &AST) -> Result { let mut compiler = incremental::IncCompiler::new(expr.all_symbols()); compiler.bind_builtin_macros(pipeline); compiler.compile_expr(pipeline, expr) } fn compile_decl(pipeline: &mut Pipeline, decl: &AST) -> Result { let mut vec: Vec = Vec::new(); let mut compiler = incremental::IncCompiler::new(decl.all_symbols()); compiler.bind_builtin_macros(pipeline); compiler.compile_decl(pipeline, &mut vec, decl)?; assert_eq!(vec.len(), 1); Ok(vec.remove(0)) } fn do_compile_expr(expr: &AST) -> Result { let mut pipeline = Pipeline::new(ProjectConfig { root_directory: PathBuf::from("."), optimizations: false, godot_version: VersionInfo::default() }); compile_expr(&mut pipeline, &expr) } fn do_compile_decl(decl: &AST) -> Result { let mut pipeline = Pipeline::new(ProjectConfig { root_directory: PathBuf::from("."), optimizations: false, godot_version: VersionInfo::default() }); compile_decl(&mut pipeline, &decl) } #[test] fn compile_call() { let ast = list(vec!(symbol("foobar"), int(10))); let expected = call("foobar", vec!(literal(Literal::Int(10)))); let actual = do_compile_expr(&ast).unwrap(); assert_eq!(actual, expected); } // These used to compile to different IR but they don't anymore. // Test is still here because meh. #[test] fn compile_builtin() { let ast = list(vec!(symbol("cons"), int(10))); let expected = call("cons", vec!(literal(Literal::Int(10)))); let actual = do_compile_expr(&ast).unwrap(); assert_eq!(actual, expected); } #[test] fn compile_int() { assert_eq!(do_compile_expr(&int(99)).unwrap(), literal(Literal::Int(99))); assert_eq!(do_compile_expr(&int(-10)).unwrap(), literal(Literal::Int(-10))); } #[test] fn compile_nil() { assert_eq!(do_compile_expr(&nil()).unwrap(), literal(Literal::Nil)); } #[test] fn compile_progn() { assert_eq!(do_compile_expr(&list(vec!(symbol("progn")))).unwrap(), progn(vec!())); assert_eq!(do_compile_expr(&list(vec!(symbol("progn"), int(1)))).unwrap(), progn(vec!(literal(Literal::Int(1))))); assert_eq!(do_compile_expr(&list(vec!(symbol("progn"), int(1), int(2)))).unwrap(), progn(vec!(literal(Literal::Int(1)), literal(Literal::Int(2))))); } #[test] fn compile_defn() { assert_eq!(do_compile_decl(&list(vec!(symbol("defn"), symbol("foobar"), list(vec!(symbol("a"), symbol("b"))), int(20)))).unwrap(), Decl::new(DeclF::FnDecl(decl::FnDecl { visibility: Visibility::FUNCTION, call_magic: None, name: "foobar".to_owned(), args: ArgList::required(vec!("a".to_owned(), "b".to_owned())), body: progn(vec!(literal(Literal::Int(20)))), }), SourceOffset::default())); } #[test] fn compile_defmacro() { assert_eq!(do_compile_decl(&list(vec!(symbol("defmacro"), symbol("foobar"), list(vec!(symbol("a"), symbol("b"))), int(20)))).unwrap(), Decl::new(DeclF::MacroDecl(decl::MacroDecl { visibility: Visibility::MACRO, name: "foobar".to_owned(), args: ArgList::required(vec!("a".to_owned(), "b".to_owned())), body: progn(vec!(literal(Literal::Int(20)))), }), SourceOffset::default())); } #[test] fn compile_set() { assert_eq!(do_compile_expr(&list(vec!(symbol("set"), symbol("foobar"), int(1)))).unwrap(), Expr::new(ExprF::Assign(AssignTarget::Variable(SourceOffset::default(), String::from("foobar")), Box::new(literal(Literal::Int(1)))), SourceOffset::default())); } } ================================================ FILE: src/ir/modifier/class.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parse rule for modifiers which apply to class declarations. use crate::ir::decl::ClassDecl; use crate::ir::export::Visibility; use super::{ParseRule, Several, Constant}; use super::visibility; /// A modifier which can be applied to a [`ClassDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ClassMod { /// The literal `main` symbol parses as `ClassMod::Main`, which /// makes the class into the unique main class for the file. Main, /// A visibility modifier. See [`super::visibility`]. Visibility(Visibility), } impl ClassMod { /// Apply the modifier to `decl`. pub fn apply(&self, decl: &mut ClassDecl) { match self { ClassMod::Main => { decl.main_class = true; } ClassMod::Visibility(vis) => { decl.visibility = *vis; } } } } /// A parse rule for class declarations. pub fn parser() -> impl ParseRule { Several::new(vec!( Box::new(Constant::new("main", ClassMod::Main).unique()), Box::new(visibility::parser().map(ClassMod::Visibility)), )) } ================================================ FILE: src/ir/modifier/constant.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parse rule for modifiers that can be applied to constant declarations. use crate::ir::decl::ConstDecl; use crate::ir::export::Visibility; use super::ParseRule; use super::visibility; /// A modifier which applies to a [`ConstDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ConstMod { /// A visibility modifier, as per [`super::visibility`]. Visibility(Visibility), } impl ConstMod { /// Apply the modifier to `decl`. pub fn apply(&self, decl: &mut ConstDecl) { match self { ConstMod::Visibility(vis) => { decl.visibility = *vis; } } } } /// A parse rule for constant declarations. pub fn parser() -> impl ParseRule { visibility::parser().map(ConstMod::Visibility) } ================================================ FILE: src/ir/modifier/declare.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! A parse rule for modifiers to `sys/declare` declarations. use crate::ir::decl::DeclareDecl; use crate::ir::export::Visibility; use super::ParseRule; use super::visibility; /// Modifiers to compile-time declaration directives. #[derive(Clone, Debug, PartialEq, Eq)] pub enum DeclareMod { /// A visibility modifier. See [`super::visibility`]. Visibility(Visibility), } impl DeclareMod { /// Apply the modifier to `decl`. pub fn apply(&self, decl: &mut DeclareDecl) { match self { DeclareMod::Visibility(vis) => { decl.visibility = *vis; } } } } /// A parse rule for [`DeclareDecl`]. pub fn parser() -> impl ParseRule { visibility::parser().map(DeclareMod::Visibility) } ================================================ FILE: src/ir/modifier/enums.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parse rule for modifiers to `enum` declarations. use crate::ir::decl::EnumDecl; use crate::ir::export::Visibility; use super::ParseRule; use super::visibility; /// Modifiers which apply to [`EnumDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum EnumMod { /// A visibility modifier. See [`super::visibility`]. Visibility(Visibility), } impl EnumMod { /// Apply the modifier. pub fn apply(&self, decl: &mut EnumDecl) { match self { EnumMod::Visibility(vis) => { decl.visibility = *vis; } } } } /// A parse rule for modifiers which apply to [`EnumDecl`]. pub fn parser() -> impl ParseRule { visibility::parser().map(EnumMod::Visibility) } ================================================ FILE: src/ir/modifier/file.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Modifiers that have to do with an entire GDLisp file. use crate::sxp::ast::AST; use crate::sxp::dotted::DottedExpr; use crate::ir::incremental::IncCompiler; use super::{ParseRule, ParseError, ParseErrorF}; use std::convert::TryInto; /// Modifier type for file-local modifiers. #[derive(Clone, Debug, PartialEq, Eq)] pub enum FileMod { /// This modifier (`(sys/nostdlib)`) should be applied to the /// standard library file to initiate the bootstrapping process. It /// instructs the compiler to run a minimal compilation, which skips /// over the usual steps of binding the standard library and also /// disables macro resolution. NoStdLib, } /// Custom parse rule which only successfully parses the literal value /// `(sys/nostdlib)` successfully. #[derive(Clone, Debug)] pub struct NoStdLibParser; impl FileMod { /// Apply `self` to the `IncCompiler` supplied. pub fn apply(&self, icompiler: &mut IncCompiler) { match self { FileMod::NoStdLib => { icompiler.mark_as_minimalist() } } } } impl ParseRule for NoStdLibParser { type Modifier = (); fn name(&self) -> &str { "NoStdLibParser" } fn parse_once(&mut self, ast: &AST) -> Result<(), ParseError> { let vec: Vec<_> = DottedExpr::new(ast).try_into().map_err(|_| file_error(ast))?; if vec.len() != 1 { return Err(file_error(ast)); } if let Some(sys_nostdlib) = vec[0].as_symbol_ref() { if sys_nostdlib == "sys/nostdlib" { return Ok(()); } } Err(file_error(ast)) } } fn file_error(ast: &AST) -> ParseError { ParseError::new(ParseErrorF::Expecting(String::from("(file-level declaration)"), ast.clone()), ast.pos) } /// Parse rule for modifiers to an entire file. pub fn parser() -> impl ParseRule { NoStdLibParser.unique().map(|_| FileMod::NoStdLib) } ================================================ FILE: src/ir/modifier/function.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Modifiers which apply to function declarations. //! //! This modifier type applies to standalone functions. For the //! modifier type which applies to instance methods, see //! [`super::instance_method`]. use crate::ir::decl::FnDecl; use crate::ir::export::Visibility; use super::{ParseRule, Several}; use super::visibility; use super::magic; /// Modifier type which applies to [`FnDecl`]. #[derive(Clone)] pub enum FnMod { /// Visibility modifier, as per [`super::visibility`]. Visibility(Visibility), /// A magic declaration, as per [`super::magic`]. Magic(String), } impl FnMod { /// Apply the modifier. pub fn apply(&self, decl: &mut FnDecl) { match self { FnMod::Visibility(vis) => { decl.visibility = *vis; } FnMod::Magic(m) => { decl.call_magic = Some(m.clone()); } } } } /// A parse rule for function modifiers. pub fn parser() -> impl ParseRule { Several::new(vec!( Box::new(visibility::parser().map(FnMod::Visibility)), Box::new(magic::parser().map(FnMod::Magic)), )) } ================================================ FILE: src/ir/modifier/instance_method.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! A parse rule for modifiers which apply specifically to instance //! methods. use crate::ir::decl::{ClassFnDecl, ConstructorDecl}; use crate::gdscript::decl::Static; use crate::compile::error::{GDError, GDErrorF}; use super::{ParseRule, Several, Constant}; /// Modifier for [`ClassFnDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum MethodMod { /// `static` declarations allow the instance method to be called /// without an instance of the class available. Static, /// `sys/nullargs` declarations force all formal arguments to the /// instance method to be given a default value of `null`. Nullargs, } impl MethodMod { /// Apply the modifier to an instance function declaration. pub fn apply(&self, decl: &mut ClassFnDecl) { match self { MethodMod::Static => { decl.is_static = Static::IsStatic; } MethodMod::Nullargs => { decl.is_nullargs = true; } } } /// Apply the modifier to a constructor. Some modifiers do not make /// sense applied to constructors and will trigger an error if an /// attempt is made to do so. pub fn apply_to_constructor(&self, decl: &mut ConstructorDecl) -> Result<(), GDError> { match self { MethodMod::Static => { Err(GDError::new(GDErrorF::StaticConstructor, decl.body.pos)) } MethodMod::Nullargs => { Err(GDError::new(GDErrorF::NullargsConstructor, decl.body.pos)) } } } } /// Parse rule for `MethodMod`. pub fn parser() -> impl ParseRule { Several::new(vec!( Box::new(Constant::new("static", MethodMod::Static).unique()), Box::new(Constant::new("sys/nullargs", MethodMod::Nullargs).unique()), )) } ================================================ FILE: src/ir/modifier/macros.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parse rule for modifiers which apply to macros and symbol macros. use crate::ir::decl::{MacroDecl, SymbolMacroDecl}; use crate::ir::export::Visibility; use super::ParseRule; use super::visibility; /// Modifier for [`MacroDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum MacroMod { Visibility(Visibility), } impl MacroMod { /// Apply the modifier. pub fn apply(&self, decl: &mut MacroDecl) { match self { MacroMod::Visibility(vis) => { decl.visibility = *vis; } } } /// Apply the modifier to a symbol macro. pub fn apply_to_symbol_macro(&self, decl: &mut SymbolMacroDecl) { match self { MacroMod::Visibility(vis) => { decl.visibility = *vis; } } } } /// Parse rule for macro modifiers. pub fn parser() -> impl ParseRule { visibility::parser().map(MacroMod::Visibility) } ================================================ FILE: src/ir/modifier/magic.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parser for call magic declarations. //! //! [Call magic](crate::compile::symbol_table::call_magic) can be used //! to force a sort of inlining on standard library calls. This //! modifier is responsible for parsing declarations that a function //! would like to exhibit call magic behavior. use crate::sxp::ast::AST; use crate::sxp::dotted::DottedExpr; use super::{ParseRule, ParseError, ParseErrorF}; use std::convert::TryInto; /// Parse rule for call magic. A `MagicParser` parses expressions of /// the form `(sys/call-magic name)` where `name` is an arbitrary /// symbol. On a successful parse, `name` is returned. #[derive(Clone, Debug)] pub struct MagicParser; impl ParseRule for MagicParser { type Modifier = String; fn name(&self) -> &str { "MagicParser" } fn parse_once(&mut self, ast: &AST) -> Result { let vec: Vec<_> = DottedExpr::new(ast).try_into().map_err(|_| magic_error(ast))?; if vec.len() != 2 { return Err(magic_error(ast)); } if let Some(sys_call_magic) = vec[0].as_symbol_ref() { if sys_call_magic == "sys/call-magic" { if let Some(name) = vec[1].as_symbol_ref() { return Ok(name.to_owned()); } } } Err(magic_error(ast)) } } fn magic_error(ast: &AST) -> ParseError { ParseError::new(ParseErrorF::Expecting(String::from("(magic declaration)"), ast.clone()), ast.pos) } /// Parser for call magic. pub fn parser() -> impl ParseRule { MagicParser } ================================================ FILE: src/ir/modifier/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Framework for parsing and applying modifiers to various types. //! //! There are a few situations in the compiler where we want to read //! an optional sequence of modifiers from a list and then return the //! rest of the list. The [`ParseRule`] trait captures this pattern. pub mod class; pub mod instance_method; pub mod function; pub mod macros; pub mod constant; pub mod enums; pub mod visibility; pub mod magic; pub mod file; pub mod declare; pub mod var; use crate::sxp::ast::AST; use crate::pipeline::source::{SourceOffset, Sourced}; use std::fmt; use std::error::Error; // TODO Can we use this for defvar export statements too? /// `Constant` is a [`ParseRule`] which looks for a /// [`Literal::Symbol`](crate::sxp::literal::Literal::Symbol) with a /// specific string value. If it finds it, it returns a preset value. /// The `M` type must implement [`Clone`] to be usable as a /// `ParseRule`. pub struct Constant { /// The symbol value to look for. pub symbol_value: String, /// The value to return on successful parse. pub result: M, } /// `Several` is a [`ParseRule`] which will attempt to parse all of /// the modifiers in its values field in order, taking the first one /// which succeeds. The `Several` instance will only fail if all /// constituent parsers fail on the same input. pub struct Several<'a, M> { /// The `Several` instance's name, used to generate friendly error /// messages. pub name: String, /// The parsers to try in order. All parse rules must have the same /// `Modifier` type. pub values: Vec + 'a>>, } /// `Map` is a [`ParseRule`] which will perform another parse rule and /// then apply a function to the result, effectively postprocessing /// the value. This rule is constructed using [`ParseRule::map`]. pub struct Map { rule: R, function: F, } /// `Unique` is a [`ParseRule`] which keeps track of whether or not /// its inner parse rule has been tripped before. If it has and it /// would trigger a second time successfully, an appropriate error is /// signaled. This rule is constructed using ParseRule::unique. pub struct Unique { triggered: bool, rule: R, } /// The type of errors that can occur during parsing modifiers. /// /// There are two broad categories of errors in parsing modifiers: /// fatal and non-fatal. A non-fatal error can be recovered by /// alternative cases, such as a [`Several`] instance with several /// options. However, if a fatal error occurs at any point during /// parsing, it is assumed to be critical and immediately fails the /// entire parse, regardless of alternatives. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParseErrorF { /// A `UniquenessError` occurs when a parser wrapped in [`Unique`] /// triggers successfully twice in the same parse. This error is /// fatal. UniquenessError(String), /// An error in which the parser was expecting something, but some /// non-matching `AST` was found instead. Expecting(String, AST), /// Generic error which is triggered when a [`Several`] exhausts its /// options. ExhaustedAlternatives, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParseError { pub value: ParseErrorF, pub pos: SourceOffset, } /// A [`ParseRule`] takes an [`AST`] and attempts to parse it as a /// particular modifier type. If successful, the modifier is returned. /// Otherwise, a [`ParseError`] is returned. pub trait ParseRule { /// The type of modifier to be returned. The interpretation of this /// type is entirely dependent on context, and no explicit /// requirements are placed on it. type Modifier; /// Attempts to parse the given AST using the parse rule. fn parse_once(&mut self, ast: &AST) -> Result; /// The name of a parse rule is used to generate more friendly error /// messages and does not affect the act of parsing itself. fn name(&self) -> &str; /// `parse` takes a slice of AST's and attempts to parse them each /// using the current parse rule. Whenever a parse fails, parsing /// stops. /// /// If the parse failed with a non-fatal (continuable) error, then /// the successfully parsed modifiers are returned, along with the /// rest of the slice that was not parsed successfully. If the parse /// failed with a fatal error, then that error is returned instead. fn parse<'a, 'b>(&mut self, args: &'a [&'b AST]) -> Result<(Vec, &'a [&'b AST]), ParseError> { let mut modifiers = Vec::new(); let mut position = 0; while position < args.len() { let curr = args[position]; match self.parse_once(curr) { Err(e) if e.is_fatal() => return Err(e), Err(_) => break, Ok(m) => modifiers.push(m), } position += 1; } Ok((modifiers, &args[position..])) } /// Produces a [`ParseRule`] which performs `self`, then applies `f` /// to the result. Errors (fatal and non-fatal alike) will be /// propagated through to the new parse rule. fn map(self, f: F) -> Map where Self : Sized, F : FnMut(Self::Modifier) -> N { Map { rule: self, function: f } } /// Produces a [`Unique`] parser representing `self`. A `Unique` /// parser will parse `self` successfully only once. If `self` /// parses successfully twice or more, a fatal /// [`ParseErrorF::UniquenessError`] will be signaled. fn unique(self) -> Unique where Self : Sized { Unique { triggered: false, rule: self } } } impl ParseError { pub fn new(value: ParseErrorF, pos: SourceOffset) -> ParseError { ParseError { value, pos } } /// Fatal errors should abort the entire parse process, not allowing /// any alternatives to run. Non-fatal errors allow alternative /// parse attempts to be run. pub fn is_fatal(&self) -> bool { match &self.value { ParseErrorF::UniquenessError(_) => true, ParseErrorF::Expecting(_, _) => false, ParseErrorF::ExhaustedAlternatives => false, } } } impl Sourced for ParseError { type Item = ParseErrorF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &ParseErrorF { &self.value } } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.value { ParseErrorF::UniquenessError(e) => write!(f, "Got duplicate modifiers for {}, expecting at most one", e), ParseErrorF::Expecting(expected, actual) => write!(f, "Expecting modifier '{}', found {}", expected, actual), ParseErrorF::ExhaustedAlternatives => write!(f, "Invalid modifier, no alternatives matched"), } } } impl Error for ParseError {} impl Constant { pub fn new(symbol_value: &str, result: M) -> Constant { Constant { symbol_value: String::from(symbol_value), result } } } impl ParseRule for Constant where M: Clone { type Modifier = M; fn parse_once(&mut self, ast: &AST) -> Result { if ast.as_symbol_ref() == Some(&self.symbol_value) { Ok(self.result.clone()) } else { Err(ParseError::new(ParseErrorF::Expecting(self.symbol_value.clone(), ast.clone()), ast.pos)) } } fn name(&self) -> &str { &self.symbol_value } } impl<'a, M> Several<'a, M> { pub fn new(values: Vec + 'a>>) -> Several<'a, M> { Several { name: String::from("(union parse rule)"), values } } /// Assigns a given name to `self` and returns it. pub fn named(mut self, name: &str) -> Self { self.name = String::from(name); self } } impl<'a, M> ParseRule for Several<'a, M> { type Modifier = M; fn parse_once(&mut self, ast: &AST) -> Result { for value in &mut self.values { match value.parse_once(ast) { Ok(modifier) => { return Ok(modifier); } Err(err) if err.is_fatal() => { return Err(err); } Err(_) => { // Continue with any other possible parses } } } Err(ParseError::new(ParseErrorF::ExhaustedAlternatives, ast.pos)) } fn name(&self) -> &str { &self.name } } impl ParseRule for Map where F : FnMut(M) -> N, R : ParseRule { type Modifier = N; fn parse_once(&mut self, ast: &AST) -> Result { let result = self.rule.parse_once(ast)?; Ok((self.function)(result)) } fn name(&self) -> &str { self.rule.name() } } impl ParseRule for Unique where R : ParseRule { type Modifier = R::Modifier; fn parse_once(&mut self, ast: &AST) -> Result { let result = self.rule.parse_once(ast)?; if self.triggered { Err(ParseError::new(ParseErrorF::UniquenessError(self.name().to_owned()), ast.pos)) } else { self.triggered = true; Ok(result) } } fn name(&self) -> &str { self.rule.name() } } ================================================ FILE: src/ir/modifier/var.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parse rule for modifiers that can be applied to instance variable //! declarations. use crate::ir::decl::ClassVarDecl; use crate::compile::body::class_initializer::InitTime; use super::{ParseRule, Constant}; /// A modifier which applies to a [`ClassVarDecl`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum VarMod { /// An "onready" modifier. Onready, } impl VarMod { /// Apply the modifier to `decl`. pub fn apply(&self, decl: &mut ClassVarDecl) { match self { VarMod::Onready => { decl.init_time = InitTime::Ready; } } } } /// A parse rule for constant declarations. pub fn parser() -> impl ParseRule { Constant::new("onready", VarMod::Onready).unique() } ================================================ FILE: src/ir/modifier/visibility.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parse rule for declaring [`Visibility`] of an identifier. use crate::ir::export::Visibility; use super::{ParseRule, Several, Constant}; /// A parse rule which checks for the literal symbols `public` or /// `private`, to return the appropriate [`Visibility`] values. The /// entire parse rule is wrapped in [`ParseRule::unique`], hence if /// the user ever supplies two visibility modifiers (whether they are /// the same or different), a fatal error will be issued. pub fn parser() -> impl ParseRule { Several::new(vec!( Box::new(Constant::new("public", Visibility::Public)), Box::new(Constant::new("private", Visibility::Private)), )).named("visibility").unique() } ================================================ FILE: src/ir/quasiquote.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::sxp::ast::{AST, ASTF}; use crate::ir::incremental::IncCompiler; use crate::compile::error::{GDError, GDErrorF}; use super::expr::Expr; use crate::pipeline::error::PError; use crate::pipeline::Pipeline; // TODO Where do I pick up PError in this file? Can we do all of this in GDError? /// A `QQSpliced` represents an expression within an `unquote` or /// `unquote-spliced` block. Specifically, a `QQSpliced` contains an /// expression that, when the quasiquote is evaluated, will have its /// value interpolated in some form or another into the surrounding /// expression. #[derive(Clone, Debug, PartialEq, Eq)] enum QQSpliced { /// An expression that will be interpreted as a single atomic value. /// The inside of an `unquote` block is parsed as this variant. Single(Expr), /// An expression that will be spliced into an array or list /// expression. The inside of an `unquote-spliced` block parses as /// this variant. /// /// It is an error if a `QQSpliced::Several` appears in a context /// other than a list or array, as it cannot be spliced. For /// example, it is an error if a `QQSpliced::Several` appears within /// a dictionary literal. Several(Expr), } /// A value within a quasiquoting expression. `UnquotedValue` always /// wraps a single [`AST`] and keeps track of what sort of expression /// it is, as far as the quasiquoting engine is concerned. #[derive(Clone, Debug, PartialEq, Eq)] enum UnquotedValue<'a> { /// A simple, literal value which is not anything special as far as /// the quasiquoting engine is concerned. SimpleValue(&'a AST), /// A nested `quasiquote` block. The [`AST`] is the *inside* of the /// `quasiquote`, excluding the block itself. Quasiquote(&'a AST), /// An `unquote` block. The [`AST`] is the *inside* of the /// `unquote`, excluding the block itself. Unquote(&'a AST), /// An `unquote-spliced` block. The [`AST`] is the *inside* of the /// `unquote-spliced`, excluding the block itself. UnquoteSpliced(&'a AST), } impl QQSpliced { /// Extracts the [`QQSpliced::Single`] value from `self`. If `self` /// is not a [`QQSpliced::Single`] then an error is returned instead. /// /// The [`AST`] argument should be the original S-expression from /// which the `QQSpliced` was produced. It will only be used in the /// case of an error in order to provide a more detailed diagnostic. fn into_single(self, ast: &AST) -> Result { match self { QQSpliced::Single(e) => Ok(e), QQSpliced::Several(_) => Err(GDError::new(GDErrorF::BadUnquoteSpliced(ast.clone()), ast.pos)), } } } impl<'a> UnquotedValue<'a> { /// Convenience constructor, delegates to /// [`UnquotedValue::SimpleValue`]. fn verbatim(arg: &'a AST) -> UnquotedValue<'a> { UnquotedValue::SimpleValue(arg) } /// Reads the top-level S-expressions and looks for any special /// quasiquote keywords. Specifically, if the S-expression is a /// proper list of two elements and the first element is one of the /// following symbols, then the function returns an appropriate /// value. /// /// * If the symbol is `quasiquote`, then an /// [`UnquotedValue::Quasiquote`] containing the second element of /// the list is returned. /// /// * If the symbol is `unquote`, then an [`UnquotedValue::Unquote`] /// containing the second element of the list is returned. /// /// * If the symbol is `unquote-spliced`, then an /// [`UnquotedValue::UnquoteSpliced`] containing the second element /// of the list is returned. /// /// If the symbol is anything else, or if the AST has any other /// shape, then this function returns an /// [`UnquotedValue::SimpleValue`] containing the entire AST. fn parse(arg: &'a AST) -> UnquotedValue<'a> { if let ASTF::Cons(car, cdr) = &arg.value { if let Some(name) = car.as_symbol_ref() { if let ASTF::Cons(cadr, cddr) = &cdr.value { if cddr.value == ASTF::NIL { if name == "quasiquote" { return UnquotedValue::Quasiquote(cadr); } else if name == "unquote" { return UnquotedValue::Unquote(cadr); } else if name == "unquote-spliced" { return UnquotedValue::UnquoteSpliced(cadr); } } } } } UnquotedValue::SimpleValue(arg) } } pub fn quasiquote(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, arg: &AST) -> Result { quasiquote_with_depth(icompiler, pipeline, arg, u32::MAX) } pub fn quasiquote_with_depth(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, arg: &AST, max_depth: u32) -> Result { let mut engine = QuasiquoteEngine::new(icompiler, pipeline, max_depth); engine.quasiquote_indexed(arg, 0, 0) } /// The quasiquoting engine is the internal class whose implementation /// recursively implements quasiquote parsing on an [`AST`]. /// /// A quasiquote engine requires mutable access to an [`IncCompiler`] /// and a [`Pipeline`], in order to call [`IncCompiler::compile_expr`] /// on unquoted values. Additionally, a quasiquote engine keeps track /// of how deep it is into a Godot expression. This is a workaround /// for a Godot parsing issue that manifests when an expression is /// nested too deeply. struct QuasiquoteEngine<'a, 'b> { icompiler: &'a mut IncCompiler, pipeline: &'b mut Pipeline, max_depth: u32, } impl<'a, 'b> QuasiquoteEngine<'a, 'b> { /// Given an incremental compiler and a pipeline, produce a /// [`QuasiquoteEngine`]. /// /// The `max_depth` argument indicates how deeply nested a generated /// expression can be before the engine will split it into multiple /// expressions with a temporary variable in between. For the /// rationale behind this argument (as well as a reasonable default /// value for it), see /// [`MAX_QUOTE_REIFY_DEPTH`](crate::compile::frame::MAX_QUOTE_REIFY_DEPTH). fn new(icompiler: &'a mut IncCompiler, pipeline: &'b mut Pipeline, max_depth: u32) -> Self { QuasiquoteEngine { icompiler, pipeline, max_depth } } // Note: nesting_depth is the number of nested quasiquotes we're in. // An unquote encountered when nesting_depth is positive simply // decreases that value rather than performing an actual unquote // operation. current_depth is how far down we are into our // structure and is used to determine when to insert ExprF::Split // calls to avoid Godot parsing issues. fn quasiquote_indexed(&mut self, arg: &AST, nesting_depth: u32, current_depth: u32) -> Result { let (needs_split_wrapper, current_depth) = if current_depth > self.max_depth { (true, 0) } else { (false, current_depth) }; self.quasiquote_spliced(arg, nesting_depth, current_depth).and_then(|qq| { let value = qq.into_single(arg)?; if needs_split_wrapper { let pos = value.pos; Ok(value.named_split("_quasiquote", pos)) } else { Ok(value) } }) } fn quasiquote_spliced(&mut self, arg: &AST, nesting_depth: u32, current_depth: u32) -> Result { let unquoted_value = UnquotedValue::parse(arg); // Deal with nesting issues let (unquoted_value, nesting_depth) = match unquoted_value { UnquotedValue::SimpleValue(_) => { (UnquotedValue::verbatim(arg), nesting_depth) } UnquotedValue::Quasiquote(_) => { (UnquotedValue::verbatim(arg), nesting_depth + 1) } UnquotedValue::Unquote(_) | UnquotedValue::UnquoteSpliced(_) => { if nesting_depth > 0 { // We're inside a nested quasiquote, so do NOT unquote the value. (UnquotedValue::verbatim(arg), nesting_depth - 1) } else { (unquoted_value, nesting_depth) } } }; match unquoted_value { UnquotedValue::Unquote(arg) => { self.icompiler.compile_expr(self.pipeline, arg).map(QQSpliced::Single) } UnquotedValue::UnquoteSpliced(arg) => { self.icompiler.compile_expr(self.pipeline, arg).map(QQSpliced::Several) } UnquotedValue::Quasiquote(_) => { // The above nesting handler should always eliminate // UnquotedValue::Quasiquote and convert it into // UnquotedValue::SimpleValue, so this should never happen. panic!("Internal error in quasiquote_spliced (impossible UnquotedValue::Quasiquote branch was reached)") } UnquotedValue::SimpleValue(arg) => { let body = match &arg.value { ASTF::Atom(lit) => { Expr::from_ast_literal(lit, arg.pos) } ASTF::Cons(car, cdr) => { let car = self.quasiquote_spliced(car, nesting_depth, current_depth + 1)?; let cdr = self.quasiquote_indexed(cdr, nesting_depth, current_depth + 1)?; match car { QQSpliced::Single(car) => { Expr::call(String::from("cons"), vec!(car, cdr), arg.pos) } QQSpliced::Several(car) => { let converted_car = Expr::call(String::from("sys/qq-smart-list"), vec!(car), arg.pos); Expr::call(String::from("append"), vec!(converted_car, cdr), arg.pos) } } } }; Ok(QQSpliced::Single(body)) } } } } ================================================ FILE: src/ir/scope/decl.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Functions for determining scope at a high-level. //! //! These functions deal with high-level concepts such as classes and //! declarations and are specifically *not* concerned with lower-level //! concepts like `let` blocks and local variables. use super::name_table::NameTable; use super::name_table::builder::NameTableBuilder; use super::error::ScopeError; use crate::util::extract_err; use crate::pipeline::source::SourceOffset; use crate::ir::identifier::{Namespace, ClassNamespace}; use crate::ir::decl::{Decl, DeclF, TopLevel, ClassDecl}; use crate::ir::expr::{Expr, ExprF, LambdaClass}; use crate::ir::literal::Literal; use crate::gdscript::library; use crate::optimize::ir::expr_walker::walk_exprs_in_decl; use std::hash::Hash; use std::convert::Infallible; /// Any type which implements [`DeclScope`] for the namespace /// [`Namespace`] can also correctly implement it for /// [`ClassNamespace`] via a simple embedding (`From::from`). The /// `ClassNamespaceAdaptor` type takes a value which implements /// `DeclScope` and provides a value which implements /// `DeclScope`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClassNamespaceAdaptor<'a, T>(pub &'a T); /// Trait for containers of declarations which can meaningfully /// enumerate the names declared in their scope. Implementors of this /// trait should only return a table of the names in the immediate /// scope and should specifically *not* recurse on inner declarations, /// such as the bodies of inner classes. pub trait DeclScope { /// Returns a table of all names, or an appropriate [`ScopeError`] /// if a problem occurs during enumeration. fn get_scope_names(&self) -> Result, ScopeError>; } impl<'a, T> DeclScope for ClassNamespaceAdaptor<'a, T> where T: DeclScope { fn get_scope_names(&self) -> Result, ScopeError> { self.0.get_scope_names() .map_err(ScopeError::from) .map(|table| table.map_ns(ClassNamespace::from)) } } impl DeclScope for TopLevel { /// Returns a table of all names, or an appropriate [`ScopeError`] /// if a problem occurs during enumeration. /// /// This method does *not* enumerate imported names. It only /// produces names for declarations which are actually defined in /// this current file, not those imported into scope. fn get_scope_names(&self) -> Result, ScopeError> { let mut builder = NameTableBuilder::new(); for decl in &self.decls { let namespace = decl.namespace(); let name = decl.name().to_owned(); builder.add_name(namespace, name, decl.pos)?; } Ok(builder.build()) } } impl DeclScope for ClassDecl { fn get_scope_names(&self) -> Result, ScopeError> { let mut builder = NameTableBuilder::new(); // Add the constructor as a special case if let Some(constructor) = &self.constructor { builder.add_name(ClassNamespace::Function, library::CONSTRUCTOR_NAME.to_owned(), constructor.body.pos)?; } for decl in &self.decls { let namespace = decl.namespace(); let name = decl.name().into_owned(); builder.add_name(namespace, name, decl.pos)?; } Ok(builder.build()) } } impl DeclScope for LambdaClass { fn get_scope_names(&self) -> Result, ScopeError> { let mut builder = NameTableBuilder::new(); // Add the constructor as a special case if let Some(constructor) = &self.constructor { builder.add_name(ClassNamespace::Function, library::CONSTRUCTOR_NAME.to_owned(), constructor.body.pos)?; } for decl in &self.decls { let namespace = decl.namespace(); let name = decl.name().into_owned(); builder.add_name(namespace, name, decl.pos)?; } Ok(builder.build()) } } impl<'a, T, NS: Hash + Eq + Clone> DeclScope for &'a T where T: DeclScope { fn get_scope_names(&self) -> Result, ScopeError> { (*self).get_scope_names() } } /// Return a vector of all of the declaration scopes in the given /// file, in an unspecified order. /// /// This function returns all of the following. /// * The toplevel scope itself. /// * Any class scopes introduced in the file, including the main class, which /// is considered distinct from the toplevel. /// * Any anonymous classes defined in the file. #[allow(clippy::vec_init_then_push)] // For style consistency pub fn get_all_decl_scopes<'a>(toplevel: &'a TopLevel) -> Vec + 'a>> { let mut acc: Vec + 'a>> = Vec::new(); // The toplevel scope acc.push(Box::new(ClassNamespaceAdaptor(toplevel))); // Any classes defined in the toplevel for decl in &toplevel.decls { match &decl.value { DeclF::ClassDecl(cls) => { acc.push(Box::new(cls)); } DeclF::FnDecl(_) | DeclF::MacroDecl(_) | DeclF::SymbolMacroDecl(_) | DeclF::ConstDecl(_) | DeclF::EnumDecl(_) | DeclF::DeclareDecl(_) => {} } } // Any anonymous classes declared in the file let err = on_each_lambda_class::(&toplevel.decls, |class| { // *sigh* What an unfortunate copy. But I don't see a way to // convince the borrow checker that this value isn't going to // disappear. (TODO Yeah...) acc.push(Box::new(class.clone())); Ok(()) }); extract_err(err); acc } /// Given a file, run the check for duplicate names, returning an /// error if any are found. If no errors occur, returns normally. pub fn check_all_decl_scopes(toplevel: &TopLevel) -> Result<(), ScopeError> { for scope in get_all_decl_scopes(toplevel) { // We don't need the resulting table; just any errors that occur // while trying to produce it. let _ = scope.get_scope_names()?; } Ok(()) } pub fn on_each_lambda_class(decls: &[Decl], mut block: F) -> Result<(), E> where F : FnMut(&LambdaClass) -> Result<(), E> { for decl in decls { walk_exprs_in_decl(decl, |expr| { if let ExprF::LambdaClass(cls) = &expr.value { block(cls)?; } // Note: We don't use this value, so if this string ever appears // in the output code, there's a problem. Ok(Expr::literal(Literal::from("UNUSED STRING FROM on_each_lambda_class"), SourceOffset(0))) })?; } Ok(()) } // TODO Tests ================================================ FILE: src/ir/scope/error.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`ScopeError`] type, which marks error during scope //! resolution. use crate::pipeline::source::SourceOffset; use crate::ir::identifier::{Namespace, ClassNamespace}; // TODO Encode the offset of both duplicate names in this error type. ///// Display and error /// The type of errors during scope resolution. #[derive(PartialEq, Eq, Debug)] pub enum ScopeError { /// The same name was declared twice in the same namespace and /// scope. DuplicateName(NS, String, SourceOffset), NameConflictWithMainClass(NS, String, SourceOffset), } impl From> for ScopeError { fn from(error: ScopeError) -> ScopeError { match error { ScopeError::DuplicateName(n, s, p) => ScopeError::DuplicateName(n.into(), s, p), ScopeError::NameConflictWithMainClass(n, s, p) => ScopeError::NameConflictWithMainClass(n.into(), s, p), } } } ================================================ FILE: src/ir/scope/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Functions for identifying and detailing which names are in scope //! in the IR. pub mod decl; pub mod name_table; pub mod error; use crate::ir::decl::{TopLevel, Decl, DeclF}; use crate::compile::error::GDError; use crate::ir::identifier::Namespace; use decl::{DeclScope, check_all_decl_scopes, ClassNamespaceAdaptor}; use name_table::NameTable; use name_table::builder::NameTableBuilder; use error::ScopeError; struct ConcreteNamesAdaptor<'a>(&'a TopLevel); /// Convenience method to perform all scope checking on an IR file /// representation. pub fn check_scopes(toplevel: &TopLevel) -> Result<(), GDError> { check_all_decl_scopes(toplevel).map_err(GDError::from)?; check_main_class_conflicts(toplevel) } /// Check for conflicts between names defined in the main class and /// names defined at the top level. There should be no overlap. /// Returns an appropriate error in case of overlap. /// /// If there is no main class, then this function simply returns /// `Ok(())` without performing any further checks. pub fn check_main_class_conflicts(toplevel: &TopLevel) -> Result<(), GDError> { let main_class_option = toplevel.find_main_class()?; if let Some(main_class) = main_class_option { // Get all toplevel names *except* `sys/declare` names. // `sys/declare` are specifically allowed to conflict with the // main class under this rule (this is necessary in the standard // library, where lots of supposedly module-level names are // `sys/declare` and are actually implemented on the main class // `GDLisp`). let concrete_names_adaptor = ConcreteNamesAdaptor(toplevel); let class_namespace_adaptor = ClassNamespaceAdaptor(&concrete_names_adaptor); let toplevel_names = class_namespace_adaptor.get_scope_names()?; let mut main_names = main_class.get_scope_names()?; main_names.retain(|ns, name, _| toplevel_names.has_name(ns, name)); // If there are any conflicts, just report the first one. let first_conflict = main_names.iter().next(); match first_conflict { None => Ok(()), Some((ns, name, pos)) => Err(GDError::from(ScopeError::NameConflictWithMainClass(ns, name.to_owned(), pos))), } } else { // No main class is declared, so conflict is impossible. Ok(()) } } impl<'a> DeclScope for ConcreteNamesAdaptor<'a> { /// As `DeclScope` on [`TopLevel`] but excludes `sys/declare` /// declarations specifically. See the implementation of /// [`check_main_class_conflicts`] for why we care to do this. fn get_scope_names(&self) -> Result, ScopeError> { let mut builder = NameTableBuilder::new(); for decl in &self.0.decls { if !is_declare_decl(decl) { let namespace = decl.namespace(); let name = decl.name().to_owned(); builder.add_name(namespace, name, decl.pos)?; } } Ok(builder.build()) } } fn is_declare_decl(decl: &Decl) -> bool { matches!(decl.value, DeclF::DeclareDecl(_)) } ================================================ FILE: src/ir/scope/name_table/builder.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Builder for a [`NameTable`](super::NameTable). use super::NameTable; use crate::ir::scope::error::ScopeError; use crate::pipeline::source::SourceOffset; use std::hash::Hash; /// A [`NameTableBuilder`] builds a [`NameTable`]. If it ever /// encounters a duplicate name or other error condition, it signals /// the error via [`ScopeError`]. #[derive(Debug)] pub struct NameTableBuilder { table: NameTable, } impl NameTableBuilder { /// A new, empty builder. pub fn new() -> NameTableBuilder { NameTableBuilder::default() } /// Adds a name to the builder. If the name is already present, an /// appropriate [`ScopeError::DuplicateName`] error is signaled. /// /// If this method signals an error, then the builder is left /// unmodified. pub fn add_name(&mut self, namespace: NS, name: String, pos: SourceOffset) -> Result<(), ScopeError> { // Note: We do the get_name check in advance and assert the result // of add_name to ensure that, if the name exists, then the // builder is left in a consistent state. If we used the result of // add_name to determine if an error had occurred, then we // couldn't guarantee that the builder remains in its prior state // in case of an error. if self.table.get_name(namespace.clone(), &name).is_some() { // Duplicate found Err(ScopeError::DuplicateName(namespace, name, pos)) } else { let result = self.table.add_name(namespace, name.clone(), pos); assert!(result.is_none(), "Internal error in add_name at {}", name); Ok(()) } } /// Build the [`NameTable`]. pub fn build(self) -> NameTable { self.table } } impl Default for NameTableBuilder { fn default() -> NameTableBuilder { NameTableBuilder { table: NameTable::default() } } } ================================================ FILE: src/ir/scope/name_table/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the [`NameTable`] type, for keeping track of names in a //! given scope and namespace. pub mod builder; use crate::pipeline::source::SourceOffset; use crate::ir::identifier::IdLike; use std::collections::HashMap; use std::hash::Hash; /// A name table stores an unordered collection of names, tagged with /// the namespace they belong to and the place in the source code that /// they were declared. #[derive(Clone, Debug, Eq, PartialEq)] pub struct NameTable { data: HashMap<(NS, String), SourceOffset>, } impl NameTable { /// A new, empty `NameTable`. pub fn new() -> NameTable { NameTable::default() } /// Given a namespace and a name, get the position in the source /// code where the declaration for that name appears, if one exists. pub fn get_name(&self, namespace: NS, name: &str) -> Option { let key = (namespace, name); self.data.get::>((&key) as &(dyn IdLike)).copied() } /// Returns whether or not this name table contains the given name /// in the given namespace. Equivalent to `self.get_name(namespace, /// name).is_some()`. pub fn has_name(&self, namespace: NS, name: &str) -> bool { self.get_name(namespace, name).is_some() } /// Adds a name to the name table, marking it as appearing at the /// given source position. If that name already appears as a /// declaration somewhere in the source code, this method overwrites /// the existing one and returns it. Otherwise, this method inserts /// the new name and returns `None`. pub fn add_name(&mut self, namespace: NS, name: String, pos: SourceOffset) -> Option { self.data.insert((namespace, name), pos) } /// The number of names which are known to this name table. pub fn len(&self) -> usize { self.data.len() } /// Whether the table is currently empty, i.e. contains no names. pub fn is_empty(&self) -> bool { self.data.is_empty() } /// Maps the namespace type of this name table, producing a new name /// table where the namespace of every known name has been /// transformed by the given function. /// /// This is mainly intended to be used with injective functions. If /// the function argument is injective, then the new name table will /// have no loss of information and will contain exactly as many /// names as the original. If the function is not injective, then it /// is unspecified which name will win out in the case of collisions /// in the resulting name table. pub fn map_ns(self, mut function: impl FnMut(NS) -> NS1) -> NameTable { let mut result: NameTable = NameTable::new(); for ((ns, name), pos) in self.data { result.add_name(function(ns), name, pos); } result } pub fn retain(&mut self, mut f: F) where F: FnMut(NS, &str, SourceOffset) -> bool { self.data.retain(|k, v| f(k.0.clone(), &k.1, *v)); } pub fn iter(&self) -> impl Iterator { self.data.iter().map(|(k, v)| (k.0.clone(), &*k.1, *v)) } } impl Default for NameTable { fn default() -> NameTable { NameTable { data: HashMap::default() } } } #[cfg(test)] mod tests { use super::*; use crate::ir::identifier::{Namespace, ClassNamespace}; #[test] fn empty_table() { let table: NameTable = NameTable::new(); assert_eq!(table.len(), 0); } #[test] fn table_insert_names() { let mut table: NameTable = NameTable::new(); assert_eq!(table.add_name(Namespace::Value, String::from("VALUE"), SourceOffset(20)), None); assert_eq!(table.add_name(Namespace::Function, String::from("FUNCTION"), SourceOffset(10)), None); assert_eq!(table.get_name(Namespace::Value, "VALUE"), Some(SourceOffset(20))); assert_eq!(table.get_name(Namespace::Function, "FUNCTION"), Some(SourceOffset(10))); assert_eq!(table.len(), 2); } #[test] fn table_crossed_namespaces() { let mut table: NameTable = NameTable::new(); assert_eq!(table.add_name(Namespace::Value, String::from("VALUE"), SourceOffset(20)), None); assert_eq!(table.add_name(Namespace::Function, String::from("FUNCTION"), SourceOffset(10)), None); assert_eq!(table.get_name(Namespace::Function, "VALUE"), None); assert_eq!(table.get_name(Namespace::Value, "FUNCTION"), None); assert_eq!(table.len(), 2); } #[test] fn table_duplicate_name_different_namespace() { let mut table: NameTable = NameTable::new(); assert_eq!(table.add_name(Namespace::Value, String::from("NAME"), SourceOffset(20)), None); assert_eq!(table.add_name(Namespace::Function, String::from("NAME"), SourceOffset(10)), None); assert_eq!(table.len(), 2); } #[test] fn table_duplicate_name_same_namespace() { let mut table: NameTable = NameTable::new(); assert_eq!(table.add_name(Namespace::Value, String::from("NAME"), SourceOffset(20)), None); assert_eq!(table.add_name(Namespace::Value, String::from("NAME"), SourceOffset(10)), Some(SourceOffset(20))); assert_eq!(table.len(), 1); } #[test] fn table_map_ns() { let mut original_table: NameTable = NameTable::new(); assert_eq!(original_table.add_name(Namespace::Value, String::from("VALUE"), SourceOffset(20)), None); assert_eq!(original_table.add_name(Namespace::Function, String::from("FUNCTION"), SourceOffset(10)), None); let mut new_table: NameTable = NameTable::new(); assert_eq!(new_table.add_name(ClassNamespace::Value, String::from("VALUE"), SourceOffset(20)), None); assert_eq!(new_table.add_name(ClassNamespace::Function, String::from("FUNCTION"), SourceOffset(10)), None); let mapped_table = original_table.map_ns(ClassNamespace::from); assert_eq!(mapped_table, new_table); } #[test] fn table_map_ns_collision() { let mut original_table: NameTable = NameTable::new(); assert_eq!(original_table.add_name(Namespace::Value, String::from("A"), SourceOffset(20)), None); assert_eq!(original_table.add_name(Namespace::Function, String::from("A"), SourceOffset(10)), None); let mapped_table = original_table.map_ns(|_| Namespace::Value); // Cause the two names to collide. // It is unspecified which name will win out, but there should // only be one name in the resulting table. assert_eq!(mapped_table.len(), 1); } } ================================================ FILE: src/ir/special_form/access_slot.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::sxp::ast::AST; use crate::sxp::dotted::DottedExpr; use crate::compile::error::{GDError, GDErrorF}; use crate::compile::args::{Expecting, ExpectedShape}; use crate::pipeline::source::SourceOffset; use std::convert::TryInto; #[derive(Clone, Debug)] pub struct AccessSlotSyntax<'a> { pub object: &'a AST, pub slot_name: String, } pub const ACCESS_SLOT_FORM_NAME: &str = "access-slot"; impl<'a> AccessSlotSyntax<'a> { pub fn parse_ast(ast: &'a AST) -> Result { let args: Vec<_> = DottedExpr::new(ast).try_into()?; if args.is_empty() { return Err(GDError::new(GDErrorF::InvalidArg(String::from(ACCESS_SLOT_FORM_NAME), ast.clone(), ExpectedShape::NonemptyList), ast.pos)); } let head = ExpectedShape::extract_symbol(ACCESS_SLOT_FORM_NAME, args[0].clone())?; if head != ACCESS_SLOT_FORM_NAME { return Err(GDError::new(GDErrorF::InvalidArg(String::from(ACCESS_SLOT_FORM_NAME), ast.clone(), ExpectedShape::AccessSlotName), ast.pos)); } Self::parse_from_tail(&args[1..], ast.pos) } pub fn parse_from_tail(tail: &[&'a AST], pos: SourceOffset) -> Result { Expecting::exactly(2).validate(ACCESS_SLOT_FORM_NAME, pos, tail)?; let object = tail[0]; let slot_name = ExpectedShape::extract_symbol(ACCESS_SLOT_FORM_NAME, tail[1].clone())?; Ok(AccessSlotSyntax { object, slot_name }) } } ================================================ FILE: src/ir/special_form/assignment.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::ir::expr::{Expr, ExprF, AssignTarget}; use crate::pipeline::source::SourceOffset; // This enum represents the different ways a (set ...) assignment form // can expand. #[derive(Clone, Debug)] pub enum AssignmentForm { Simple(AssignTarget), SetterCall(String, Vec), } impl AssignmentForm { pub fn str_to_setter_prefix(name: &str) -> String { format!("set-{}", name) } pub fn into_expr(self, rhs: Expr, pos: SourceOffset) -> Expr { match self { AssignmentForm::Simple(target) => { Expr::new(ExprF::Assign(target, Box::new(rhs)), pos) } AssignmentForm::SetterCall(f, mut args) => { args.insert(0, rhs); Expr::call(f, args, pos) } } } } ================================================ FILE: src/ir/special_form/local_binding.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::ir::expr::{Expr, ExprF, LocalFnClause, FunctionBindingType}; use crate::pipeline::source::SourceOffset; // flet and labels use a lot of common code and only really differ in // the way in which bindings work. We factor out those differences // here so that all of the common code can be written only once over // in ir::special_form. // // TODO I think FunctionBindingType makes this irrelevant. These // differences can just go over there. pub struct FLetLocalBinding; pub struct LabelsLocalBinding; pub trait LocalBinding { fn function_binding_type(&self) -> FunctionBindingType; fn has_recursive_bindings(&self) -> bool; fn wrap_in_expr(&self, clauses: Vec, body: Box, pos: SourceOffset) -> Expr { Expr::new(ExprF::FunctionLet(self.function_binding_type(), clauses, body), pos) } } impl LocalBinding for FLetLocalBinding { fn function_binding_type(&self) -> FunctionBindingType { FunctionBindingType::OuterScoped } fn has_recursive_bindings(&self) -> bool { false } } impl LocalBinding for LabelsLocalBinding { fn function_binding_type(&self) -> FunctionBindingType { FunctionBindingType::Recursive } fn has_recursive_bindings(&self) -> bool { true } } ================================================ FILE: src/ir/special_form/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod access_slot; pub mod assignment; pub mod local_binding; use crate::sxp::ast::{AST, ASTF}; use crate::sxp::literal::Literal; use crate::sxp::dotted::DottedExpr; use super::expr::{ExprF, Expr, FuncRefTarget, AssignTarget, LambdaClass, LocalVarClause, LocalFnClause}; use super::special_ref::SpecialRef; use super::decl::{self, Decl, DeclF}; use super::arglist::ordinary::ArgList; use super::quasiquote::quasiquote_with_depth; use super::bootstrapping::compile_bootstrapping_expr; use crate::compile::error::{GDError, GDErrorF}; use crate::compile::args::{Expecting, ExpectedShape}; use crate::compile::frame::MAX_QUOTE_REIFY_DEPTH; use crate::ir::incremental::IncCompiler; use crate::ir::identifier::{Id, IdLike, Namespace}; use crate::ir::export::Visibility; use crate::ir::import::{ImportDecl, ImportDeclParseError}; use crate::pipeline::Pipeline; use crate::pipeline::source::SourceOffset; use crate::pipeline::error::PError; use local_binding::{FLetLocalBinding, LabelsLocalBinding, LocalBinding}; use assignment::AssignmentForm; use access_slot::AccessSlotSyntax; use std::convert::{TryFrom, TryInto}; pub fn dispatch_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, head: &str, tail: &[&AST], pos: SourceOffset) -> Result, PError> { match head { "progn" => progn_form(icompiler, pipeline, tail, pos).map(Some), "cond" => cond_form(icompiler, pipeline, tail, pos).map(Some), "while" => while_form(icompiler, pipeline, tail, pos).map(Some), "for" => for_form(icompiler, pipeline, tail, pos).map(Some), "let" => let_form(icompiler, pipeline, tail, pos).map(Some), "flet" => flet_form(icompiler, pipeline, tail, pos, FLetLocalBinding).map(Some), "labels" => flet_form(icompiler, pipeline, tail, pos, LabelsLocalBinding).map(Some), "lambda" => lambda_form(icompiler, pipeline, tail, pos).map(Some), "function" => function_form(tail, pos).map(Some), "set" => assign_form(icompiler, pipeline, tail, pos).map(Some), "quote" => quote_form(tail, pos).map(Some), "quasiquote" => quasiquote_form(icompiler, pipeline, tail, pos).map(Some), "unquote" => Err(PError::from(GDError::new(GDErrorF::UnquoteOutsideQuasiquote, pos))), "unquote-spliced" => Err(PError::from(GDError::new(GDErrorF::UnquoteSplicedOutsideQuasiquote, pos))), "access-slot" => access_slot_form(icompiler, pipeline, tail, pos).map(Some), "new" => new_form(icompiler, pipeline, tail, pos).map(Some), "yield" => yield_form(icompiler, pipeline, tail, pos).map(Some), "assert" => assert_form(icompiler, pipeline, tail, pos).map(Some), "return" => return_form(icompiler, pipeline, tail, pos).map(Some), "macrolet" => macrolet_form(icompiler, pipeline, tail, pos).map(Some), "symbol-macrolet" => symbol_macrolet_form(icompiler, pipeline, tail, pos).map(Some), "sys/special-ref" => special_ref_form(icompiler, pipeline, tail, pos).map(Some), "sys/context-filename" => context_filename_form(icompiler, pipeline, tail, pos).map(Some), "literally" => literally_form(tail, pos).map(Some), "sys/split" => split_form(icompiler, pipeline, tail, pos).map(Some), "preload" => preload_form(icompiler, pipeline, tail, pos).map(Some), "sys/bootstrap" => bootstrap_form(icompiler, pipeline, tail, pos).map(Some), "break" => break_form(icompiler, pipeline, tail, pos).map(Some), "continue" => continue_form(icompiler, pipeline, tail, pos).map(Some), _ => Ok(None), } } pub fn progn_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { let body = tail.iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(Expr::progn(body, pos)) } pub fn cond_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { let body = tail.iter().map(|clause| { let vec: Vec<&AST> = DottedExpr::new(clause).try_into().map_err(|x| GDError::from_value(x, pos))?; match vec.len() { 0 => { Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("cond"), (*clause).clone(), ExpectedShape::NonemptyList), pos))) } 1 => { let cond = icompiler.compile_expr(pipeline, vec[0])?; Ok((cond, None)) } _ => { let cond = icompiler.compile_expr(pipeline, vec[0])?; let inner = vec[1..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; // In this branch, vec.len() > 1, so vec[1] is safe Ok((cond, Some(Expr::progn(inner, vec[1].pos)))) } } }).collect::, _>>()?; Ok(Expr::new(ExprF::CondStmt(body), pos)) } pub fn while_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::at_least(1).validate("while", pos, tail)?; let cond = icompiler.compile_expr(pipeline, tail[0])?; let body = tail[1..].iter().map(|x| icompiler.compile_expr(pipeline, x)).collect::, _>>()?; Ok(Expr::while_stmt(cond, Expr::progn(body, pos), pos)) } pub fn for_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::at_least(2).validate("for", pos, tail)?; let name = ExpectedShape::extract_symbol("for", tail[0].clone())?; let iter = icompiler.compile_expr(pipeline, tail[1])?; let body = tail[2..].iter().map(|x| icompiler.compile_expr(pipeline, x)).collect::, _>>()?; Ok(Expr::for_stmt(name, iter, Expr::progn(body, pos), pos)) } pub fn let_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { // TODO Clean this up :( Expecting::at_least(1).validate("let", pos, tail)?; let vars: Vec<_> = DottedExpr::new(tail[0]).try_into()?; let var_clauses = vars.into_iter().map(|clause| { let var: Vec<_> = match DottedExpr::new(clause) { DottedExpr { elements, terminal: AST { value: ASTF::NIL, pos: _ } } if !elements.is_empty() => elements, DottedExpr { elements, terminal: tail@AST { value: ASTF::Atom(Literal::Symbol(_)), pos: _ } } if elements.is_empty() => vec!(tail), _ => return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("let"), (*clause).clone(), ExpectedShape::VarDecl), pos))) }; let result_value = var[1..].iter().map(|e| icompiler.compile_expr(pipeline, e)).collect::, _>>()?; let name = match &var[0].value { ASTF::Atom(Literal::Symbol(s)) => Ok(s.clone()), _ => Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("let"), (*clause).clone(), ExpectedShape::VarDecl), pos))), }?; Ok(LocalVarClause { name, value: Expr::progn(result_value, clause.pos) }) }).collect::, _>>()?; let var_names: Vec<_> = var_clauses.iter().map(|x| x.name.clone()).collect(); macrolet_unbind_macros(icompiler, pipeline, &mut var_names.iter().map(|x| (Namespace::Value, &**x)), |icompiler, pipeline| { let body = tail[1..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(Expr::new(ExprF::Let(var_clauses, Box::new(Expr::progn(body, pos))), pos)) }) } pub fn lambda_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::at_least(1).validate("lambda", pos, tail)?; let args: Vec<_> = DottedExpr::new(tail[0]).try_into()?; let args = ArgList::parse(args, pos)?; let body = tail[1..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(Expr::new(ExprF::Lambda(args, Box::new(Expr::progn(body, pos))), pos)) } pub fn function_form(tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("function", pos, tail)?; let s = ExpectedShape::extract_symbol("function", tail[0].clone())?; Ok(Expr::new(ExprF::FuncRef(FuncRefTarget::SimpleName(s)), pos)) } pub fn assign_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(2).validate("set", pos, tail)?; let assign_target = match &tail[0].value { ASTF::Atom(Literal::Symbol(s)) => { AssignmentForm::Simple(AssignTarget::Variable(tail[0].pos, s.to_owned())) } _ => { let x = tail[0]; let inner: Vec<_> = DottedExpr::new(x).try_into()?; if inner[0].value == ASTF::symbol(String::from("access-slot")) { let AccessSlotSyntax { object, slot_name } = AccessSlotSyntax::parse_from_tail(&inner[1..], pos)?; let object = icompiler.compile_expr(pipeline, object)?; AssignmentForm::Simple(AssignTarget::InstanceField(inner[0].pos, Box::new(object), slot_name)) } else { let s = ExpectedShape::extract_symbol("set", inner[0].clone())?; let head = AssignmentForm::str_to_setter_prefix(&s); let args = inner[1..].iter().map(|x| icompiler.compile_expr(pipeline, x)).collect::>()?; AssignmentForm::SetterCall(head, args) } } }; let value = icompiler.compile_expr(pipeline, tail[1])?; Ok(assign_target.into_expr(value, pos)) } pub fn flet_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset, binding_rule: impl LocalBinding) -> Result { // TODO This function is used for flet and labels, so using "flet" // in all of the errors is not strictly correct. Expecting::at_least(1).validate("flet", pos, tail)?; let fns: Vec<_> = DottedExpr::new(tail[0]).try_into()?; // Get all of the names of the declared functions let fn_names: Vec<_> = fns.iter().map(|clause| { let func: Vec<_> = DottedExpr::new(clause).try_into()?; if func.len() < 2 { return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("flet"), (*clause).clone(), ExpectedShape::FnDecl), pos))); } let result = ExpectedShape::extract_symbol("flet", func[0].clone())?; Ok(result) }).collect::>()?; // If the recursive binding rule is in effect (i.e. for labels), // then we want to bind all of the names before compiling anything. // If the recursive binding rule is not in effect (i.e. for flet), // then we want to bind the names after compiling the clauses but // before compiling the body. let (pre_names, post_names) = if binding_rule.has_recursive_bindings() { (fn_names, vec!()) } else { (vec!(), fn_names) }; macrolet_unbind_macros(icompiler, pipeline, &mut pre_names.iter().map(|x| (Namespace::Function, &**x)), |icompiler, pipeline| { let fn_clauses = fns.into_iter().map(|clause| { let func: Vec<_> = DottedExpr::new(clause).try_into()?; if func.len() < 2 { return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("flet"), clause.clone(), ExpectedShape::FnDecl), pos))); } let name = match &func[0].value { ASTF::Atom(Literal::Symbol(s)) => Ok(s.clone()), _ => Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("flet"), (*clause).clone(), ExpectedShape::FnDecl), pos))), }?; let args: Vec<_> = DottedExpr::new(func[1]).try_into()?; let args = ArgList::parse(args, pos)?; let body = func[2..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(LocalFnClause { name, args, body: Expr::progn(body, clause.pos) }) }).collect::, _>>()?; macrolet_unbind_macros(icompiler, pipeline, &mut post_names.iter().map(|x| (Namespace::Function, &**x)), |icompiler, pipeline| { let body = tail[1..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(binding_rule.wrap_in_expr(fn_clauses, Box::new(Expr::progn(body, pos)), pos)) }) }) } pub fn quote_form(tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("quote", pos, tail)?; Ok(Expr::new(ExprF::Quote(tail[0].clone()), pos)) } pub fn quasiquote_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("quasiquote", pos, tail)?; quasiquote_with_depth(icompiler, pipeline, tail[0], MAX_QUOTE_REIFY_DEPTH) } pub fn access_slot_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { let AccessSlotSyntax { object, slot_name } = AccessSlotSyntax::parse_from_tail(tail, pos)?; let object = icompiler.compile_expr(pipeline, object)?; Ok(Expr::new(ExprF::FieldAccess(Box::new(object), slot_name), pos)) } pub fn new_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::at_least(1).validate("new", pos, tail)?; let super_call = match &tail[0].value { ASTF::Atom(Literal::Symbol(_)) => AST::dotted_list(vec!((*tail[0]).clone()), AST::nil(tail[0].pos)), _ => tail[0].clone(), }; let super_call = Vec::try_from(DottedExpr::new(&super_call))?; if super_call.is_empty() { return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("new"), tail[0].clone(), ExpectedShape::SuperclassDecl), pos))); } let superclass = match &super_call[0].value { ASTF::Atom(Literal::Symbol(superclass_name)) => superclass_name.to_owned(), _ => return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("new"), tail[0].clone(), ExpectedShape::SuperclassDecl), pos))), }; let super_args = super_call[1..].iter().map(|arg| icompiler.compile_expr(pipeline, arg)).collect::, _>>()?; let mut cls = decl::ClassDecl::new(String::from("(local anonymous class)"), superclass); for decl in &tail[1..] { icompiler.compile_class_inner_decl(pipeline, &mut cls, decl)?; } let lambda_class = LambdaClass::from((cls, super_args)); Ok(Expr::new(ExprF::LambdaClass(Box::new(lambda_class)), pos)) } pub fn yield_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::between(0, 2).validate("yield", pos, tail)?; match tail.len() { 0 => { Ok(Expr::yield_none(pos)) } 1 => { // TODO yield() is weird in that it's the only thing in GDLisp // right now that takes a non-interval number of arguments. // Every other function or special form can be broadly described // as taking [a, b] or [a, infinity) arguments, where a and b // are integers. yield() takes {0, 2}. // // It would be nice to *define* some semantics for yield(x), for // consistency, but I can't think of what that would mean. OTOH // how do we correctly report errors for this relatively bizarre // special form, since WrongNumberArgs assumes an interval as // expected argument count? Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("yield"), AST::nil(SourceOffset(0)), ExpectedShape::YieldArg), pos))) } 2 => { let lhs = icompiler.compile_expr(pipeline, tail[0])?; let rhs = icompiler.compile_expr(pipeline, tail[1])?; Ok(Expr::yield_some(lhs, rhs, pos)) } _ => { unreachable!() } } } pub fn assert_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::between(1, 2).validate("assert", pos, tail)?; match tail.len() { 1 => { let cond = icompiler.compile_expr(pipeline, tail[0])?; Ok(Expr::assert_expr(cond, None, pos)) } 2 => { let cond = icompiler.compile_expr(pipeline, tail[0])?; let message = icompiler.compile_expr(pipeline, tail[1])?; Ok(Expr::assert_expr(cond, Some(message), pos)) } _ => { unreachable!() } } } pub fn return_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("return", pos, tail)?; let expr = icompiler.compile_expr(pipeline, tail[0])?; Ok(Expr::new(ExprF::Return(Box::new(expr)), pos)) } pub fn macrolet_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::at_least(1).validate("macrolet", pos, tail)?; let fns: Vec<_> = DottedExpr::new(tail[0]).try_into()?; let fn_clauses = fns.into_iter().map(|clause| { let func: Vec<_> = DottedExpr::new(clause).try_into()?; if func.len() < 2 { return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("macrolet"), clause.clone(), ExpectedShape::MacroDecl), pos))); } let name = match &func[0].value { ASTF::Atom(Literal::Symbol(s)) => Ok(s.clone()), _ => Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("macrolet"), (*clause).clone(), ExpectedShape::MacroDecl), pos))), }?; let args: Vec<_> = DottedExpr::new(func[1]).try_into()?; let args = ArgList::parse(args, pos)?; let body = func[2..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(decl::MacroDecl { visibility: Visibility::MACRO, name, args, body: Expr::progn(body, clause.pos) }) }).collect::, _>>()?; let mut fn_clauses = fn_clauses.into_iter().map(|x| (Namespace::Function, x)); macrolet_bind_locals(icompiler, pipeline, &mut fn_clauses, pos, |icompiler, pipeline| { let body = tail[1..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(Expr::progn(body, pos)) }) } pub fn symbol_macrolet_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::at_least(1).validate("symbol-macrolet", pos, tail)?; let vars: Vec<_> = DottedExpr::new(tail[0]).try_into()?; let var_clauses = vars.into_iter().map(|clause| { let var: Vec<_> = DottedExpr::new(clause).try_into()?; if var.len() != 2 { return Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("symbol-macrolet"), clause.clone(), ExpectedShape::MacroDecl), pos))); } let name = match &var[0].value { ASTF::Atom(Literal::Symbol(s)) => Ok(s.clone()), _ => Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("symbol-macrolet"), (*clause).clone(), ExpectedShape::MacroDecl), pos))), }?; let body = icompiler.compile_expr(pipeline, var[1])?; Ok(decl::MacroDecl { visibility: Visibility::MACRO, name, args: ArgList::empty(), body: body }) }).collect::, _>>()?; let mut var_clauses = var_clauses.into_iter().map(|x| (Namespace::Value, x)); macrolet_bind_locals(icompiler, pipeline, &mut var_clauses, pos, |icompiler, pipeline| { let body = tail[1..].iter().map(|expr| icompiler.compile_expr(pipeline, expr)).collect::, _>>()?; Ok(Expr::progn(body, pos)) }) } pub fn special_ref_form(_icompiler: &mut IncCompiler, _pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("sys/special-ref", pos, tail)?; if let Some(sym) = tail[0].as_symbol_ref() { match sym { "this-file" => Ok(Expr::from_value(SpecialRef::ThisFile, pos)), "this-filename" => Ok(Expr::from_value(SpecialRef::ThisFileName, pos)), "this-true-filename" => Ok(Expr::from_value(SpecialRef::ThisTrueFileName, pos)), "godot-version" => Ok(Expr::from_value(SpecialRef::GodotVersion, pos)), _ => Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("sys/special-ref"), tail[0].clone(), ExpectedShape::SpecialRefValue), pos))), } } else { Err(PError::from(GDError::new(GDErrorF::InvalidArg(String::from("sys/special-ref"), tail[0].clone(), ExpectedShape::SpecialRefValue), pos))) } } // This one requires a bit of explanation. (sys/context-filename path) // is a compiler hook designed to assist with macro expansion. It's // much easier to explain *how* it works than to explain when you'd // need it. For the latter, see the expansion for `deflazy`, which // uses this trick. // // path must be a literal string. (sys/context-filename path) plugs // path into the *current* preload resolver during the compilation // stage and produces a new literal string consisting of the result. // Obviously, path must be a literal string which parses as a resource // path (i.e. "res://*"). This is basically the same transformation // (use ...) directives undergo, but I'm exposing it as an // expression-level special form so that macros can exploit it. // // Note that (sys/context-filename ...) is *not* involved in // dependency resolution, so macros won't detect it as a dependency // like they will a (use ...) directive. Thus, it's basically only // safe to use this directive in situations where the relevant file is // guaranteed to *already* be loaded by a prior (use ...) in the same // file. pub fn context_filename_form(_icompiler: &mut IncCompiler, _pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("sys/context-filename", pos, tail)?; let s = ExpectedShape::extract_string("sys/context-filename", tail[0].clone())?; let path = ImportDecl::parse_path_param(&s).ok_or_else(|| { let err = ImportDeclParseError::InvalidPath(s.clone()); PError::from(GDError::from_value(err, pos)) })?; Ok(Expr::new(ExprF::ContextualFilename(path), pos)) } pub fn literally_form(tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("literally", pos, tail)?; let name = ExpectedShape::extract_symbol("literally", tail[0].clone())?; Ok(Expr::atomic_var(name, pos)) } pub fn split_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("sys/split", pos, tail)?; let expr = icompiler.compile_expr(pipeline, tail[0])?; Ok(expr.split(pos)) } fn macrolet_bind_locals(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, macros: &mut I, pos: SourceOffset, func: F) -> Result where E : From, F : FnOnce(&mut IncCompiler, &mut Pipeline) -> Result, I : Iterator { match macros.next() { None => func(icompiler, pipeline), #[allow(clippy::redundant_clone)] // Clippy thinks this clone is // redundant but it doesn't // compile without it. Some((namespace, m)) => icompiler.locally_save_macro(&*Id::build(namespace, &m.name), |icompiler| { let name = m.name.to_string(); icompiler.bind_macro(pipeline, m.to_owned(), pos, true, namespace)?; let old_symbol_value = { let table = icompiler.declaration_table(); table.get(&*Id::build(namespace, &name)).cloned() }; match namespace { Namespace::Function => { icompiler.declaration_table().add(Decl::new(DeclF::MacroDecl(m.clone()), pos)); } Namespace::Value => { let symbol_macro = decl::SymbolMacroDecl { visibility: m.visibility, name: m.name.to_owned(), body: m.body.clone() }; icompiler.declaration_table().add(Decl::new(DeclF::SymbolMacroDecl(symbol_macro), pos)); } }; let result = macrolet_bind_locals(icompiler, pipeline, macros, pos, func); if let Some(old_symbol_value) = old_symbol_value { let table = icompiler.declaration_table(); table.add(old_symbol_value); } else { let table = icompiler.declaration_table(); table.del(&*Id::build(namespace, &name)); } icompiler.unbind_macro(&*Id::build(namespace, &name)); result }), } } fn macrolet_unbind_macros<'a, B, E, F, I>(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, macros: &mut I, func: F) -> Result where E : From, F : FnOnce(&mut IncCompiler, &mut Pipeline) -> Result, I : Iterator { match macros.next() { None => func(icompiler, pipeline), Some(name) => { let name: &(dyn IdLike + 'a) = &name; if icompiler.has_macro(name) { icompiler.locally_save_macro(name, |icompiler| { let old_symbol_value = { let table = icompiler.declaration_table(); table.get(name).cloned() }; icompiler.unbind_macro(name); let result = macrolet_unbind_macros(icompiler, pipeline, macros, func); if let Some(old_symbol_value) = old_symbol_value { let table = icompiler.declaration_table(); table.add(old_symbol_value); } else { let table = icompiler.declaration_table(); table.del(name); } result }) } else { macrolet_unbind_macros(icompiler, pipeline, macros, func) } } } } pub fn preload_form(_icompiler: &mut IncCompiler, _pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("preload", pos, tail)?; let s = ExpectedShape::extract_string("preload", tail[0].clone())?; Ok(Expr::new(ExprF::Preload(s), pos)) } pub fn bootstrap_form(icompiler: &mut IncCompiler, pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::exactly(1).validate("sys/bootstrap", pos, tail)?; let s = ExpectedShape::extract_symbol("sys/bootstrap", tail[0].clone())?; compile_bootstrapping_expr(icompiler, pipeline, &s, pos).map_err(PError::from) } pub fn break_form(_icompiler: &mut IncCompiler, _pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::NONE.validate("break", pos, tail)?; Ok(Expr::new(ExprF::Break, pos)) } pub fn continue_form(_icompiler: &mut IncCompiler, _pipeline: &mut Pipeline, tail: &[&AST], pos: SourceOffset) -> Result { Expecting::NONE.validate("continue", pos, tail)?; Ok(Expr::new(ExprF::Continue, pos)) } ================================================ FILE: src/ir/special_ref.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`SpecialRef`] type and associated values. use super::expr::ExprF; /// A `SpecialRef` is a type of [`Expr`](super::expr::Expr) which is /// superficially a literal form but which acquires some state from /// its surrounding environment or context. That is, a special /// reference can be thought of as a 0-ary function which is not, in /// the mathematical sense, a pure function. /// /// Special references are converted to [`Expr`](super::expr::Expr) /// either via the explicit constructor /// [`ExprF::SpecialRef`](super::expr::ExprF::SpecialRef) or via the /// general-purpose helper /// [`Expr::from_value`](super::expr::Expr::from_value). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SpecialRef { /// `ThisFile` will compile into a `load` expression which loads and /// returns a reference to the current file, as a GDScript script /// object. ThisFile, /// `ThisFileName` will compile into a string literal expression /// which refers to the current (GDScript) filename. If called /// during a macro expansion, this will expand to the virtual name /// of the macro file. ThisFileName, /// `ThisTrueFileName` will compile into a string literal expression /// which refers to the current (GDScript) filename. If called /// during macro expansion, this will expand into the *true* final /// name of the file, not the name of the virtual macro file. ThisTrueFileName, /// `GodotVersion` returns the current Godot version, as an integer /// such that later Godot versions compare greater than earlier /// ones. GodotVersion, } impl From for ExprF { fn from(special_ref: SpecialRef) -> ExprF { ExprF::SpecialRef(special_ref) } } ================================================ FILE: src/lib.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . #![allow(clippy::redundant_field_names)] #![allow(non_shorthand_field_patterns)] #[macro_use] extern crate lalrpop_util; #[macro_use] extern crate lazy_static; pub mod sxp; pub mod compile; pub mod ir; pub mod gdscript; pub mod runner; pub mod graph; pub mod util; pub mod command_line; pub mod pipeline; pub mod optimize; pub mod repl; mod parser_test; lalrpop_mod!(#[allow(clippy::all)] pub parser); lazy_static! { pub static ref AST_PARSER: parser::ASTParser = parser::ASTParser::new(); pub static ref SOME_AST_PARSER: parser::SomeASTParser = parser::SomeASTParser::new(); } ================================================ FILE: src/main.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate lalrpop_util; extern crate walkdir; extern crate gdlisp; /* pub mod sxp; pub mod compile; pub mod gdscript; mod parser_test; lalrpop_mod!(pub parser); */ use gdlisp::gdscript::{decl, library}; use gdlisp::command_line::{parse_args, show_help_message}; use gdlisp::repl::Repl; use gdlisp::pipeline::Pipeline; use gdlisp::pipeline::config::ProjectConfig; use gdlisp::pipeline::source::{SourcedValue, SourceOffset}; use gdlisp::runner::version::{VersionInfo, get_godot_version_as_string}; use walkdir::WalkDir; use std::io::{self, BufRead, Write}; use std::env; use std::path::{PathBuf, Path}; use std::str::FromStr; use std::thread::sleep; use std::time::Duration; // Legacy REPL fn run_pseudo_repl(godot_version: VersionInfo) { let stdin = io::stdin(); let config = ProjectConfig { root_directory: PathBuf::from_str(".").unwrap(), // Infallible optimizations: true, godot_version, }; let mut pipeline = Pipeline::new(config); for line in stdin.lock().lines() { let line = line.unwrap(); match pipeline.compile_code("./REPL.lisp", &line) { Err(err) => { let err = SourcedValue::new(&err, &line); println!("Error: {}", err); } Ok(trans) => { let cls = decl::TopLevelClass::from(trans); print!("{}", cls.to_gd()); } } } } fn run_repl(godot_version: VersionInfo) { fn prompt() -> Option { let mut line = String::new(); let stdin = io::stdin(); let mut stdout = io::stdout(); print!("> "); stdout.flush().expect("Failed to flush stdout"); let bytes = stdin.read_line(&mut line).expect("Could not read from stdin; broken pipe?"); if bytes == 0 { None } else { Some(line) } } let config = ProjectConfig { root_directory: PathBuf::from_str(".").unwrap(), // Infallible optimizations: true, godot_version, }; let mut repl = Repl::new(config); repl.force_load(); while let Some(line) = prompt() { let result = repl.parse_and_run_code(&line); match result { Err(err) => { let err = SourcedValue::new(&err, &line); println!("Error: {}", err); } Ok(result) => { println!("{}", result); } } // Check and see if the Godot process has terminated. If the user // calls `get_tree().quit()`, then the process will terminate at // the end of the current frame. Godot's default FPS is 60, so // this should take 1/60th of a second, or 16 milliseconds. Out of // an abundance of caution, we pause for 100 milliseconds here. sleep(Duration::from_millis(100)); if !repl.is_running() { break; } } } fn compile_file

+ ?Sized>(input: &P, godot_version: VersionInfo) { let input = input.as_ref(); let mut pipeline = Pipeline::new(ProjectConfig { root_directory: input.parent().unwrap_or(input).to_owned(), optimizations: true, godot_version }); match pipeline.load_file(input.file_name().unwrap(), SourceOffset(0)) { Err(err) => { let err = SourcedValue::from_file(&err, input).unwrap(); println!("Error: {}", err); } Ok(_unit) => {} } } fn compile_all_files

+ ?Sized>(input: &P, godot_version: VersionInfo) { let input = input.as_ref(); let mut pipeline = Pipeline::new(ProjectConfig { root_directory: input.to_owned(), optimizations: true, godot_version }); for entry in WalkDir::new(input).into_iter().filter_map(|e| e.ok()) { if entry.path().is_file() && entry.path().extension() == Some("lisp".as_ref()) { println!("Compiling {} ...", entry.path().to_string_lossy()); let path = entry.path().strip_prefix(&pipeline.config().root_directory).expect("Non-local file load detected"); match pipeline.load_file(path, SourceOffset(0)) { Err(err) => { let err = SourcedValue::from_file(&err, entry.path()).unwrap(); println!("Error in {}: {}", entry.path().to_string_lossy(), err); } Ok(_unit) => {} } } } } fn main() { let args: Vec<_> = env::args().collect(); let program_name = &args[0]; let parsed_args = parse_args(&args[1..]); if parsed_args.help_message { show_help_message(program_name); } else { let version = get_godot_version_as_string(); match &version { Ok(version) => { println!("GDLisp v1.0.0"); println!("Running under Godot {}", version); } Err(err) => { eprintln!("Warning: `godot` is not on your path. A significant portion of the GDLisp compiler depends on the Godot engine. It is strongly recommended that you add `godot` to your path before continuing."); eprintln!("Warning: While looking for Godot, the error I got was: {}", err); } } // If Godot isn't on the path, then any version checks will return // 0.0.0 since we can't get that information accurately. let version = version.unwrap_or_default(); if parsed_args.compile_stdlib_flag { library::load_stdlib_to_file(); println!("Stdlib compiled successfully.") } else if let Some(input) = parsed_args.input_file { let input: &Path = input.as_ref(); if input.is_dir() { compile_all_files(input, VersionInfo::parse(&version)); } else { compile_file(input, VersionInfo::parse(&version)); } } else if parsed_args.legacy_repl_flag { run_pseudo_repl(VersionInfo::parse(&version)); } else { run_repl(VersionInfo::parse(&version)); } } } ================================================ FILE: src/optimize/gdscript/assignment.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::op; use crate::gdscript::stmt::{Stmt, StmtF}; use crate::gdscript::expr::{Expr, ExprF}; #[derive(Clone, Debug)] pub struct AssignmentStmt<'a> { pub assign_type: AssignType, pub var_name: &'a str, pub expr: &'a Expr, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum AssignType { VarDecl, Assignment(op::AssignOp), } impl<'a> AssignmentStmt<'a> { pub fn match_stmt(stmt: &'a Stmt) -> Option> { match &stmt.value { StmtF::VarDecl(var_name, expr) => { Some(AssignmentStmt { assign_type: AssignType::VarDecl, var_name, expr }) } StmtF::Assign(var_name, op, expr) => { if let ExprF::Var(var_name) = &var_name.value { Some(AssignmentStmt { assign_type: AssignType::Assignment(*op), var_name, expr }) } else { None } } _ => { None } } } } impl AssignType { pub fn ensure_eq(self) -> Self { match self { AssignType::VarDecl => AssignType::VarDecl, AssignType::Assignment(_) => AssignType::Assignment(op::AssignOp::Eq), } } } impl<'a> From> for Stmt { fn from(stmt: AssignmentStmt) -> Stmt { let AssignmentStmt { assign_type, var_name, expr } = stmt; let var_name = var_name.to_owned(); let expr = expr.clone(); let pos = expr.pos; match assign_type { AssignType::VarDecl => Stmt::var_decl(var_name, expr, pos), AssignType::Assignment(op) => Stmt::new(StmtF::Assign(Box::new(Expr::var(&var_name, expr.pos)), op, Box::new(expr)), pos), } } } ================================================ FILE: src/optimize/gdscript/basic_math_ops.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::ExpressionLevelPass; use crate::gdscript::expr::{self, Expr, ExprF}; use crate::gdscript::op; use crate::compile::error::GDError; use super::constant; pub struct BasicMathOps; impl ExpressionLevelPass for BasicMathOps { fn run_on_expr(&self, expr: &Expr) -> Result { let mut expr = expr.clone(); // (TODO Expand this) // Negation on constant if let ExprF::Unary(op::UnaryOp::Not, inner) = &expr.value { if let Some(b) = constant::deduce_bool(inner) { expr = Expr::from_value(!b, expr.pos); } } // Ternary if on constant if let ExprF::TernaryIf(if_expr) = &expr.value { let expr::TernaryIf { true_case, cond, false_case } = if_expr.clone(); if let Some(b) = constant::deduce_bool(&cond) { expr = if b { *true_case } else { *false_case }; } } // Double negation elimination (TODO Maybe only in if statements; // it does technically Booleanize at least) if let ExprF::Unary(op::UnaryOp::Not, outer) = &expr.value { if let ExprF::Unary(op::UnaryOp::Not, inner) = &outer.value { expr = (**inner).clone(); } } Ok(expr) } } ================================================ FILE: src/optimize/gdscript/constant.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::expr::{self, Expr, ExprF}; use crate::gdscript::stmt::{Stmt, StmtF}; use crate::gdscript::literal::Literal; // Attempt to determine whether a value is true or false by looking at // the expression that produces it. If we can't tell or if the // expression is stateful, then we return None. pub fn deduce_bool(expr: &Expr) -> Option { match &expr.value { ExprF::Literal(lit) => { match lit { Literal::Int(n) => Some(*n != 0), Literal::Float(f) => Some(**f != 0.0), Literal::String(s) => Some(!s.is_empty()), Literal::NodeLiteral(_) => None, // Truthy iff the node exists; can't determine at compile-time Literal::NodePathLiteral(s) => Some(!s.is_empty()), Literal::Null => Some(false), Literal::Bool(b) => Some(*b), } } _ => None, } } // Attempt to discern whether or not a statement or expression has any // side effects. If in doubt, assume it does. pub fn stmt_has_side_effects(stmt: &Stmt) -> bool { match &stmt.value { StmtF::Expr(e) => { expr_has_side_effects(e) } StmtF::PassStmt => false, StmtF::IfStmt(_) | StmtF::ForLoop(_) | StmtF::WhileLoop(_) | StmtF::MatchStmt(_, _) => true, // TODO These StmtF::BreakStmt | StmtF::ContinueStmt | StmtF::VarDecl(_, _) | StmtF::ReturnStmt(_) | StmtF::Assign(_, _, _) => true, } } pub fn expr_has_side_effects(expr: &Expr) -> bool { match &expr.value { ExprF::Var(_) => false, ExprF::Literal(_) => false, // Subscript is a bit questionable, because it can't call // arbitrary methods (only works in a predefined, simple way), but // it can err out if out of bounds. ExprF::Subscript(a, b) => expr_has_side_effects(a) || expr_has_side_effects(b), // Attribute is questionable as well because setget can do method // calls. ExprF::Attribute(a, _) => expr_has_side_effects(a), ExprF::Call(_, _, _) => true, ExprF::SuperCall(_, _) => true, ExprF::Unary(_, e) => expr_has_side_effects(e), ExprF::Binary(a, _, b) => expr_has_side_effects(a) || expr_has_side_effects(b), ExprF::TernaryIf(expr::TernaryIf { true_case: a, cond: b, false_case: c }) => expr_has_side_effects(a) || expr_has_side_effects(b) || expr_has_side_effects(c), ExprF::ArrayLit(es) => es.iter().any(expr_has_side_effects), ExprF::DictionaryLit(es) => es.iter().any(|(k, v)| expr_has_side_effects(k) || expr_has_side_effects(v)), } } // A constant expression neither reads nor writes to any local or // global state. If a variable has a constant value, any references to // that variable can be safely replaced with the value without // changing the behavior of the code. pub fn expr_is_constant(expr: &Expr) -> bool { match &expr.value { ExprF::Var(_) => false, ExprF::Literal(_) => true, ExprF::Subscript(_, _) => false, ExprF::Attribute(_, _) => false, ExprF::Call(_, _, _) => false, ExprF::SuperCall(_, _) => false, ExprF::Unary(_, e) => expr_is_constant(e), ExprF::Binary(a, _, b) => expr_is_constant(a) && expr_is_constant(b), ExprF::TernaryIf(expr::TernaryIf { true_case: a, cond: b, false_case: c }) => expr_is_constant(a) && expr_is_constant(b) && expr_is_constant(c), // These two produce mutable values that cannot be blindly substituted ExprF::ArrayLit(_) => false, ExprF::DictionaryLit(_) => false, } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } #[test] fn literals_as_bool_test() { assert_eq!(deduce_bool(&e(ExprF::from(0))), Some(false)); assert_eq!(deduce_bool(&e(ExprF::from(1))), Some(true)); assert_eq!(deduce_bool(&e(ExprF::Literal(Literal::Null))), Some(false)); assert_eq!(deduce_bool(&e(ExprF::from(true))), Some(true)); assert_eq!(deduce_bool(&e(ExprF::from(false))), Some(false)); assert_eq!(deduce_bool(&e(ExprF::from("A"))), Some(true)); assert_eq!(deduce_bool(&e(ExprF::from(""))), Some(false)); assert_eq!(deduce_bool(&e(ExprF::Literal(Literal::NodeLiteral(String::from("foobar"))))), None); assert_eq!(deduce_bool(&e(ExprF::Literal(Literal::NodePathLiteral(String::from("A"))))), Some(true)); assert_eq!(deduce_bool(&e(ExprF::Literal(Literal::NodePathLiteral(String::from(""))))), Some(false)); } #[test] fn arbitrary_expr_as_bool_test() { assert_eq!(deduce_bool(&Expr::var("some-variable", SourceOffset::default())), None); } } ================================================ FILE: src/optimize/gdscript/constant_conditional_branch.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::{self, Stmt, StmtF}; use crate::compile::error::GDError; use crate::pipeline::source::SourceOffset; use super::StatementLevelPass; use super::constant; pub struct ConstantConditionalBranch; // Eliminate the "if" part of the conditional fn kill_if_branch(if_stmt: &stmt::IfStmt, pos: SourceOffset) -> Vec { if if_stmt.elif_clauses.is_empty() { // No elif, so the else clause becomes the whole conditional if_stmt.else_clause.clone().unwrap_or_default() } else { // Use the first elif let new_if_stmt = stmt::IfStmt { if_clause: if_stmt.elif_clauses[0].clone(), elif_clauses: if_stmt.elif_clauses[1..].to_vec(), else_clause: if_stmt.else_clause.clone(), }; vec!(Stmt::new(StmtF::IfStmt(new_if_stmt), pos)) } } impl ConstantConditionalBranch { pub fn run_on_stmt_acc(&self, stmts: &[Stmt]) -> Result, GDError> { let mut result = Vec::new(); for stmt in stmts { result.extend(self.run_on_stmt(stmt)?); } Ok(result) } } impl StatementLevelPass for ConstantConditionalBranch { fn run_on_stmt(&self, stmt: &Stmt) -> Result, GDError> { // Check for obviously true or false cases if let StmtF::IfStmt(if_stmt) = &stmt.value { // If branch if let Some(b) = constant::deduce_bool(&if_stmt.if_clause.0) { if b { // Definitely true case return self.run_on_stmt_acc(&if_stmt.if_clause.1); } else { // Definitely false case let rest_of_stmt = kill_if_branch(if_stmt, stmt.pos); return self.run_on_stmt_acc(&rest_of_stmt); } } // Elif branches // If any are definitely false, we can run_on_stmt them let mut v = if_stmt.elif_clauses.clone(); v.retain(|(e, _)| constant::deduce_bool(e) != Some(false)); if v != if_stmt.elif_clauses.clone() { let mut new_if_stmt = if_stmt.clone(); new_if_stmt.elif_clauses = v; return self.run_on_stmt(&Stmt::new(StmtF::IfStmt(new_if_stmt), stmt.pos)); } // If any are definitely true, then they become the else branch let match_index = if_stmt.elif_clauses.iter().position(|(e, _)| constant::deduce_bool(e) == Some(true)); if let Some(match_index) = match_index { let mut new_if_stmt = if_stmt.clone(); new_if_stmt.elif_clauses = if_stmt.elif_clauses[0..match_index].to_vec(); new_if_stmt.else_clause = Some(if_stmt.elif_clauses[match_index].1.clone()); return self.run_on_stmt(&Stmt::new(StmtF::IfStmt(new_if_stmt), stmt.pos)); } } Ok(vec!(stmt.clone())) } } ================================================ FILE: src/optimize/gdscript/dead_code_elimination.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::{Stmt, StmtF}; use crate::compile::error::GDError; use super::StatementLevelPass; use super::noop; pub struct DeadCodeElimination; // TODO Apply to code after a return impl StatementLevelPass for DeadCodeElimination { fn run_on_stmt(&self, stmt: &Stmt) -> Result, GDError> { let mut stmt = stmt.clone(); // If the statement itself has no effect, then omit it entirely if noop::is_code_noop(&stmt) { return Ok(vec!()); } // Check for empty else clause if let StmtF::IfStmt(if_stmt) = &mut stmt.value { if let Some(else_clause) = &if_stmt.else_clause { if noop::is_code_seq_noop(else_clause) { if_stmt.else_clause = None; } } } Ok(vec!(stmt)) } } #[cfg(test)] mod tests { use super::*; use crate::optimize::gdscript::FunctionOptimization; use crate::gdscript::stmt; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::decl; use crate::gdscript::arglist::ArgList; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } #[test] fn else_do_not_run_on_stmt() { /* (Change nothing) * if 0: * return 1 * else: * return 2 */ let stmt = stmt::if_else(e(ExprF::from(0)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(1))))), vec!(s(StmtF::ReturnStmt(e(ExprF::from(2))))), SourceOffset::default()); let decl = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: vec!(stmt) }; let mut transformed_decl = decl.clone(); DeadCodeElimination.run_on_function(&mut transformed_decl).unwrap(); assert_eq!(decl, transformed_decl); } #[test] fn else_run_on_stmt() { /* (Eliminate spurious else) * if 0: * return 1 * else: * pass */ let stmt0 = stmt::if_else(e(ExprF::from(0)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(1))))), vec!(s(StmtF::PassStmt)), SourceOffset::default()); let decl0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: vec!(stmt0) }; let stmt1 = stmt::if_then(e(ExprF::from(0)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(1))))), SourceOffset::default()); let decl1 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: vec!(stmt1) }; let mut transformed_decl = decl0.clone(); DeadCodeElimination.run_on_function(&mut transformed_decl).unwrap(); assert_eq!(decl1, transformed_decl); } } ================================================ FILE: src/optimize/gdscript/dead_decl_elimination.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::decl::{self, DeclF}; use crate::compile::error::GDError; use super::{FileOptimization, on_each_decl}; /// `DeadDeclElimination` simply removes all "pass" declarations. /// These can be produced as a compilation artifact of other /// optimization passes and of certain types of declarations in the /// language. They're unnecessary in the resulting output and can /// always be safely removed. pub struct DeadDeclElimination; // TODO In the future, this will expand to remove private declarations // that are unused (such as lambda classes which have been optimized // out by other passes). For now, it just runs on "pass" decls. impl FileOptimization for DeadDeclElimination { fn run_on_file(&self, file: &mut decl::TopLevelClass) -> Result<(), GDError> { file.body = on_each_decl(&file.body, |d| { if let DeclF::PassDecl = &d.value { Ok(vec!()) } else { Ok(vec!(d.clone())) } })?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::decl::{self, Decl, DeclF}; use crate::gdscript::class_extends::ClassExtends; use crate::pipeline::source::SourceOffset; fn d(decl: DeclF) -> Decl { Decl::new(decl, SourceOffset::default()) } #[test] fn eliminate_pass() { /* (Eliminate the pass decl) * pass */ let decls = vec!( d(DeclF::PassDecl), ); let mut toplevel = decl::TopLevelClass { name: None, extends: ClassExtends::SimpleIdentifier(String::from("Reference")), body: decls, }; DeadDeclElimination.run_on_file(&mut toplevel).unwrap(); assert_eq!(toplevel.name, None); assert_eq!(toplevel.extends, ClassExtends::SimpleIdentifier(String::from("Reference"))); assert_eq!(toplevel.body, vec!()); } } ================================================ FILE: src/optimize/gdscript/dead_var_elimination.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::{Stmt, StmtF}; use crate::gdscript::decl; use crate::compile::error::GDError; use super::FunctionOptimization; use super::stmt_walker; use super::variables::get_variable_info; pub struct DeadVarElimination; impl DeadVarElimination { fn run_on_body(&self, stmts: &mut Vec) { let vars = get_variable_info(stmts); *stmts = stmt_walker::walk_stmts_ok(stmts, stmt_walker::on_each_stmt_ok(|stmt| { if let StmtF::VarDecl(var_name, expr) = &stmt.value { if let Some(info) = vars.get(var_name) { if !info.is_ever_used() { return vec!(Stmt::expr(expr.clone())); } } } vec!(stmt.clone()) })); } } /* * If a variable is never used, then we can omit it entirely. */ impl FunctionOptimization for DeadVarElimination { fn run_on_function(&self, function: &mut decl::FnDecl) -> Result<(), GDError> { self.run_on_body(&mut function.body); Ok(()) } fn run_on_init_function(&self, function: &mut decl::InitFnDecl) -> Result<(), GDError> { self.run_on_body(&mut function.body); Ok(()) } } ================================================ FILE: src/optimize/gdscript/direct_var_substitute.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::expr::ExprF; use crate::gdscript::stmt::Stmt; use crate::gdscript::decl; use crate::compile::error::GDError; use super::FunctionOptimization; use super::constant; use super::expr_walker; use super::variables::get_variable_info; pub struct DirectVarSubstitute; // NOTE: This optimization assumes that a local variable is only // declared once in a given function. I don't know Godot's exact // scoping rules, but I know the GDLisp compiler ensures uniqueness of // names in local variables. If we ever stop doing so, we need to // alter this a bit. impl DirectVarSubstitute { fn run_on_body(&self, stmts: &mut Vec) { let vars = get_variable_info(stmts); *stmts = expr_walker::walk_exprs_ok(stmts, |var_expr| { if let ExprF::Var(var_name) = &var_expr.value { if let Some(info) = vars.get(var_name) { if info.is_read_only() && constant::expr_is_constant(&info.value) { return info.value.clone(); } } } var_expr.clone() }); } } /* * If a variable is declared and assigned a constant value, and if the * variable is never written to again, then we can simply use the * constant value anywhere we would use the variable. */ impl FunctionOptimization for DirectVarSubstitute { fn run_on_function(&self, function: &mut decl::FnDecl) -> Result<(), GDError> { self.run_on_body(&mut function.body); Ok(()) } fn run_on_init_function(&self, function: &mut decl::InitFnDecl) -> Result<(), GDError> { self.run_on_body(&mut function.body); Ok(()) } } ================================================ FILE: src/optimize/gdscript/else_then_if_fold.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::{Stmt, StmtF}; use crate::compile::error::GDError; use super::StatementLevelPass; pub struct ElseThenIfFold; impl StatementLevelPass for ElseThenIfFold { fn run_on_stmt(&self, stmt: &Stmt) -> Result, GDError> { // If we have an else whose body is an if, we can flatten it. // This comes up when compiling cond sometimes. if let StmtF::IfStmt(if_stmt) = &stmt.value { if let Some(else_stmt) = &if_stmt.else_clause { if let [Stmt { value: StmtF::IfStmt(inner_if_stmt), pos: _ }] = &else_stmt[..] { let mut new_if_stmt = if_stmt.clone(); new_if_stmt.elif_clauses.push(inner_if_stmt.if_clause.clone()); for elif in &inner_if_stmt.elif_clauses { new_if_stmt.elif_clauses.push(elif.clone()); } new_if_stmt.else_clause = inner_if_stmt.else_clause.clone(); return Ok(vec!(Stmt::new(StmtF::IfStmt(new_if_stmt), stmt.pos))); } } } Ok(vec!(stmt.clone())) } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::stmt; use crate::gdscript::decl; use crate::gdscript::arglist::ArgList; use crate::optimize::gdscript::FunctionOptimization; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } #[test] fn else_then_if_optimize_test() { /* (Merge else and if) * if 1: * return 1 * else: * if 2: * return 2 */ let inner_stmt = stmt::if_then(e(ExprF::from(2)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(2))))), SourceOffset::default()); let stmt = stmt::if_else(e(ExprF::from(1)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(1))))), vec!(inner_stmt), SourceOffset::default()); let mut decl = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: vec!(stmt) }; let desired_stmt = s(StmtF::IfStmt(stmt::IfStmt { if_clause: (e(ExprF::from(1)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(1)))))), elif_clauses: vec!((e(ExprF::from(2)), vec!(s(StmtF::ReturnStmt(e(ExprF::from(2))))))), else_clause: None, })); let desired_decl = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: vec!(desired_stmt) }; ElseThenIfFold.run_on_function(&mut decl).unwrap(); assert_eq!(decl, desired_decl); } } ================================================ FILE: src/optimize/gdscript/expr_walker.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::stmt_walker; use crate::gdscript::stmt::{self, Stmt, StmtF}; use crate::gdscript::expr::{self, Expr, ExprF}; use crate::util::extract_err; // Post-order traversal pub fn walk_expr<'a, E>(stmt: &Stmt, mut walker: impl FnMut(&Expr) -> Result + 'a) -> Result, E> { stmt_walker::walk_stmt(stmt, stmt_walker::on_each_stmt(|s| walk_impl(&mut walker, s))) } pub fn walk_exprs<'a, E>(stmts: &[Stmt], mut walker: impl FnMut(&Expr) -> Result + 'a) -> Result, E> { stmt_walker::walk_stmts(stmts, stmt_walker::on_each_stmt(|s| walk_impl(&mut walker, s))) } pub fn walk_expr_ok<'a>(stmt: &Stmt, mut walker: impl FnMut(&Expr) -> Expr + 'a) -> Vec { let result = walk_expr(stmt, move |x| Ok(walker(x))); extract_err(result) } pub fn walk_exprs_ok<'a>(stmts: &[Stmt], mut walker: impl FnMut(&Expr) -> Expr + 'a) -> Vec { let result = walk_exprs(stmts, move |x| Ok(walker(x))); extract_err(result) } fn walk_impl_expr<'a, E>(walker: &mut (impl FnMut(&Expr) -> Result + 'a), expr: &Expr) -> Result { let mut expr = expr.clone(); match &mut expr.value { ExprF::Subscript(a, b) => { **a = walk_impl_expr(walker, a)?; **b = walk_impl_expr(walker, b)?; } ExprF::Attribute(a, _) => { **a = walk_impl_expr(walker, a)?; } ExprF::Call(lhs, _, args) => { if let Some(lhs) = lhs.as_mut() { **lhs = walk_impl_expr(walker, lhs)?; } for arg in args.iter_mut() { *arg = walk_impl_expr(walker, arg)?; } } ExprF::SuperCall(_, args) => { for arg in args.iter_mut() { *arg = walk_impl_expr(walker, arg)?; } } ExprF::Unary(_, a) => { **a = walk_impl_expr(walker, a)?; } ExprF::Binary(a, _, b) => { **a = walk_impl_expr(walker, a)?; **b = walk_impl_expr(walker, b)?; } ExprF::TernaryIf(expr::TernaryIf { true_case: a, cond: b, false_case: c }) => { **a = walk_impl_expr(walker, a)?; **b = walk_impl_expr(walker, b)?; **c = walk_impl_expr(walker, c)?; } ExprF::ArrayLit(args) => { for arg in args.iter_mut() { *arg = walk_impl_expr(walker, arg)?; } } ExprF::DictionaryLit(args) => { for (k, v) in args.iter_mut() { *k = walk_impl_expr(walker, k)?; *v = walk_impl_expr(walker, v)?; } } ExprF::Var(_) | ExprF::Literal(_) => {} } walker(&expr) } fn walk_impl<'a, E>(walker: &mut (impl FnMut(&Expr) -> Result + 'a), stmt: &Stmt) -> Result, E> { let new_stmt = match &stmt.value { StmtF::Expr(e) => { StmtF::Expr(walk_impl_expr(walker, e)?) } StmtF::IfStmt(stmt::IfStmt { if_clause, elif_clauses, else_clause }) => { let new_if_stmt = stmt::IfStmt { if_clause: (walk_impl_expr(walker, &if_clause.0)?, if_clause.1.clone()), elif_clauses: (elif_clauses.iter().map(|(e, s)| Ok((walk_impl_expr(walker, e)?, s.clone()))).collect::>()?), else_clause: else_clause.clone(), }; StmtF::IfStmt(new_if_stmt) } StmtF::ForLoop(stmt::ForLoop { iter_var, collection, body }) => { let new_for_loop = stmt::ForLoop { iter_var: iter_var.clone(), collection: walk_impl_expr(walker, collection)?, body: body.clone(), }; StmtF::ForLoop(new_for_loop) } StmtF::WhileLoop(stmt::WhileLoop { condition, body }) => { let new_while_loop = stmt::WhileLoop { condition: walk_impl_expr(walker, condition)?, body: body.clone(), }; StmtF::WhileLoop(new_while_loop) } StmtF::MatchStmt(expr, clauses) => { StmtF::MatchStmt(walk_impl_expr(walker, expr)?, clauses.clone()) } StmtF::VarDecl(name, expr) => { StmtF::VarDecl(name.clone(), walk_impl_expr(walker, expr)?) } StmtF::ReturnStmt(expr) => { StmtF::ReturnStmt(walk_impl_expr(walker, expr)?) } StmtF::Assign(lhs, op, rhs) => { let lhs = walk_impl_expr(walker, lhs)?; let rhs = walk_impl_expr(walker, rhs)?; StmtF::Assign(Box::new(lhs), *op, Box::new(rhs)) } StmtF::PassStmt | StmtF::BreakStmt | StmtF::ContinueStmt => { stmt.value.clone() } }; Ok(vec!(Stmt::new(new_stmt, stmt.pos))) } // TODO Test me :) ================================================ FILE: src/optimize/gdscript/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod noop; pub mod stmt_walker; pub mod expr_walker; pub mod constant; pub mod variables; pub mod assignment; pub mod dead_code_elimination; pub mod dead_decl_elimination; pub mod constant_conditional_branch; pub mod else_then_if_fold; pub mod basic_math_ops; pub mod redundant_assignment_elimination; pub mod direct_var_substitute; pub mod dead_var_elimination; pub mod ternary_if_fold; use crate::gdscript::decl::{self, Decl, DeclF}; use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::compile::error::GDError; // Note: If optimization results in an error, the code is guaranteed // to be in a valid, correct state. It may or may not be rolled back // to the way it started, but it should perform equivalently at // runtime. pub trait ExpressionLevelPass { fn run_on_expr(&self, expr: &Expr) -> Result; } pub trait StatementLevelPass { fn run_on_stmt(&self, stmt: &Stmt) -> Result, GDError>; } pub trait FunctionOptimization { fn run_on_function(&self, function: &mut decl::FnDecl) -> Result<(), GDError>; fn run_on_init_function(&self, function: &mut decl::InitFnDecl) -> Result<(), GDError>; } pub trait FileOptimization { fn run_on_file(&self, file: &mut decl::TopLevelClass) -> Result<(), GDError>; } // TODO Note that expression-level optimizations won't run on // ConstDecl, VarDecl, or EnumDecl expressions right now. Also on // super args in InitFnDecl. fn on_each_decl_impl(decls: &[Decl], acc: &mut Vec, block: &mut impl FnMut(&Decl) -> Result, GDError>) -> Result<(), GDError> { for decl in decls { let mut new_decls = block(decl)?; for new_decl in &mut new_decls { match &mut new_decl.value { DeclF::FnDecl(_, _) | DeclF::InitFnDecl(_) | DeclF::VarDecl(_) | DeclF::ConstDecl(_, _) | DeclF::SignalDecl(_, _) | DeclF::EnumDecl(_) | DeclF::PassDecl => { // Nothing to recurse on, so pass. } DeclF::ClassDecl(cdecl) => { let mut inner_acc: Vec = Vec::new(); on_each_decl_impl(&cdecl.body, &mut inner_acc, block)?; cdecl.body = inner_acc; } } } acc.append(&mut new_decls); } Ok(()) } /// Runs the block for each declaration, including those nested inside /// of classes. This is a pre-order traversal, so the block will be /// run on the outer declaration first and then on any inner /// declarations in the result. /// /// For each declaration, the block is free to return zero or more /// declarations in its place. The resulting declarations will be /// concatenated together, with the original order preserved. Any /// errors during the block invocation are propagated to the caller of /// `on_each_decl`. pub fn on_each_decl(decls: &[Decl], mut block: impl FnMut(&Decl) -> Result, GDError>) -> Result, GDError> { let mut acc: Vec = Vec::new(); on_each_decl_impl(decls, &mut acc, &mut block)?; Ok(acc) } fn on_decl(opt: &impl FunctionOptimization, decl: &Decl) -> Result, GDError> { match &decl.value { DeclF::FnDecl(s, fndecl) => { let mut fndecl = fndecl.clone(); opt.run_on_function(&mut fndecl)?; Ok(vec!(Decl::new(DeclF::FnDecl(*s, fndecl), decl.pos))) } DeclF::InitFnDecl(fndecl) => { let mut fndecl = fndecl.clone(); opt.run_on_init_function(&mut fndecl)?; Ok(vec!(Decl::new(DeclF::InitFnDecl(fndecl), decl.pos))) } DeclF::ClassDecl(_) | DeclF::VarDecl(_) | DeclF::ConstDecl(_, _) | DeclF::SignalDecl(_, _) | DeclF::EnumDecl(_) | DeclF::PassDecl => { Ok(vec!(decl.clone())) } } } // Every FunctionOptimization is a FileOptimization by applying it to // each function in the file. impl FileOptimization for T where T : FunctionOptimization { fn run_on_file(&self, file: &mut decl::TopLevelClass) -> Result<(), GDError> { file.body = on_each_decl(&file.body, |d| on_decl(self, d))?; Ok(()) } } // A StatementLevelPass is just a local FunctionOptimization impl FunctionOptimization for T where T : StatementLevelPass { fn run_on_function(&self, function: &mut decl::FnDecl) -> Result<(), GDError> { function.body = stmt_walker::walk_stmts(&function.body, stmt_walker::on_each_stmt(|x| self.run_on_stmt(x)))?; Ok(()) } fn run_on_init_function(&self, function: &mut decl::InitFnDecl) -> Result<(), GDError> { function.body = stmt_walker::walk_stmts(&function.body, stmt_walker::on_each_stmt(|x| self.run_on_stmt(x)))?; Ok(()) } } // An ExpressionLevelPass can easily be realized as a StatementLevelPass impl StatementLevelPass for T where T : ExpressionLevelPass { fn run_on_stmt(&self, stmt: &Stmt) -> Result, GDError> { expr_walker::walk_expr(stmt, |e| self.run_on_expr(e)) } } // TODO We'll refine this a lot. Right now, it's hard coded. pub fn run_standard_passes(file: &mut decl::TopLevelClass) -> Result<(), GDError> { // Run thrice, for good measure :) for _ in 0..3 { // Fold trivial if statements containing assignments into the variable ternary_if_fold::TernaryIfFold.run_on_file(file)?; redundant_assignment_elimination::RedundantAssignmentElimination.run_on_file(file)?; dead_var_elimination::DeadVarElimination.run_on_file(file)?; dead_code_elimination::DeadCodeElimination.run_on_file(file)?; // Simplify anything we can at the expression level. basic_math_ops::BasicMathOps.run_on_file(file)?; // Eliminate and fold conditionals else_then_if_fold::ElseThenIfFold.run_on_file(file)?; constant_conditional_branch::ConstantConditionalBranch.run_on_file(file)?; dead_code_elimination::DeadCodeElimination.run_on_file(file)?; // Get rid of unnecessary assignments redundant_assignment_elimination::RedundantAssignmentElimination.run_on_file(file)?; // Eliminate constant variables direct_var_substitute::DirectVarSubstitute.run_on_file(file)?; dead_var_elimination::DeadVarElimination.run_on_file(file)?; // Another conditional and dead code pass else_then_if_fold::ElseThenIfFold.run_on_file(file)?; constant_conditional_branch::ConstantConditionalBranch.run_on_file(file)?; dead_code_elimination::DeadCodeElimination.run_on_file(file)?; // Eliminate dead variables dead_var_elimination::DeadVarElimination.run_on_file(file)?; dead_code_elimination::DeadCodeElimination.run_on_file(file)?; dead_decl_elimination::DeadDeclElimination.run_on_file(file)?; } Ok(()) } ================================================ FILE: src/optimize/gdscript/noop.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . // Helper functions for checking whether code is a noop use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use super::constant; pub fn is_code_seq_noop(stmts: &[Stmt]) -> bool { stmts.iter().all(is_code_noop) } pub fn is_code_noop(stmt: &Stmt) -> bool { !constant::stmt_has_side_effects(stmt) } pub fn is_expr_noop(expr: &Expr) -> bool { !constant::expr_has_side_effects(expr) } #[cfg(test)] mod tests { use super::*; use crate::gdscript::op; use crate::gdscript::expr::ExprF; use crate::gdscript::stmt::StmtF; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } #[test] fn expr_noop() { // True assert!(is_expr_noop(&e(ExprF::Var(String::from("example_variable"))))); assert!(is_expr_noop(&e(ExprF::from(1)))); assert!(is_expr_noop(&e(ExprF::Unary(op::UnaryOp::Negate, Box::new(e(ExprF::from(1))))))); assert!(is_expr_noop(&e(ExprF::ArrayLit(vec!())))); assert!(is_expr_noop(&e(ExprF::ArrayLit(vec!(e(ExprF::from(1)), e(ExprF::from(2)), e(ExprF::from(3))))))); assert!(is_expr_noop(&e(ExprF::Subscript(Box::new(e(ExprF::from(1))), Box::new(e(ExprF::from(2))))))); assert!(is_expr_noop(&e(ExprF::Attribute(Box::new(e(ExprF::from(1))), String::from("attribute_name"))))); // False let call = e(ExprF::Call(None, String::from("function_name"), vec!())); assert!(!is_expr_noop(&call)); assert!(!is_expr_noop(&e(ExprF::Unary(op::UnaryOp::Negate, Box::new(call.clone()))))); assert!(!is_expr_noop(&e(ExprF::Binary(Box::new(e(ExprF::from(2))), op::BinaryOp::Add, Box::new(call.clone()))))); } #[test] fn stmt_noop() { // True assert!(is_code_noop(&Stmt::expr(e(ExprF::from(1))))); assert!(is_code_noop(&s(StmtF::PassStmt))); // False let call = e(ExprF::Call(None, String::from("function_name"), vec!())); assert!(!is_code_noop(&Stmt::expr(call))); assert!(!is_code_noop(&s(StmtF::BreakStmt))); assert!(!is_code_noop(&s(StmtF::ContinueStmt))); } } ================================================ FILE: src/optimize/gdscript/redundant_assignment_elimination.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::Stmt; use crate::gdscript::expr::Expr; use crate::gdscript::decl; use crate::gdscript::op; use crate::compile::error::GDError; use super::FunctionOptimization; use super::constant; use super::stmt_walker; use super::assignment::{AssignmentStmt, AssignType}; pub struct RedundantAssignmentElimination; /* * This catches situations like * * var example = 0 * example = 10 * * and replaces them with * * var example = 10 * * Specifically, any variable declaration or (possibly compound) * assignment (where the expression is non-stateful), followed by zero * or more non-stateful statements, followed by another assignment, is * considered redundant and can be culled down to one declaration / * assignment. */ impl RedundantAssignmentElimination { fn match_first_assign<'a>(&self, stmt: &'a Stmt) -> Option> { AssignmentStmt::match_stmt(stmt) } // The second assignment (the one that makes the first redundant) // must be a Stmt::Assign. If it's a Stmt::VarDecl, then it's a // Godot error (I think? Might be a shadowed variable? Either way, // not our problem) fn match_second_assign<'a, 'b, 'c>(&'a self, name: &'b str, stmt: &'c Stmt) -> Option<&'c Expr> { if let Some(AssignmentStmt { assign_type, var_name, expr }) = AssignmentStmt::match_stmt(stmt) { if assign_type == AssignType::Assignment(op::AssignOp::Eq) && name == var_name { return Some(expr); } } None } fn rebuild_assignment(&self, assign_type: AssignType, var_name: &str, expr: &Expr) -> Stmt { AssignmentStmt { assign_type, var_name, expr }.into() } pub fn run_on_stmts(&self, stmts: &[Stmt]) -> Result, GDError> { // Look for something to eliminate. If we find it, do the // elimination and call the function again. If not, we're done. for (index, stmt1) in stmts.iter().enumerate() { if let Some(AssignmentStmt { assign_type, var_name: name, expr: expr1 }) = self.match_first_assign(stmt1) { if !constant::expr_has_side_effects(expr1) { // Found a match. Keep going. for jndex in index+1..stmts.len() { let stmt2 = &stmts[jndex]; if let Some(expr2) = self.match_second_assign(name, stmt2) { // Redundant assignment; cull let new_stmt = self.rebuild_assignment(assign_type.ensure_eq(), name, expr2); let mut new_stmts = stmts.to_vec(); new_stmts.splice(index..=jndex, vec!(new_stmt).into_iter()); return self.run_on_stmts(&new_stmts); } else if constant::stmt_has_side_effects(stmt2) { // If it's stateful, then we can't do anything break; } } } } } // Found nothing to do, so just pass through Ok(stmts.to_vec()) } } impl FunctionOptimization for RedundantAssignmentElimination { fn run_on_function(&self, function: &mut decl::FnDecl) -> Result<(), GDError> { function.body = stmt_walker::walk_stmts(&function.body, |x| self.run_on_stmts(x))?; Ok(()) } fn run_on_init_function(&self, function: &mut decl::InitFnDecl) -> Result<(), GDError> { function.body = stmt_walker::walk_stmts(&function.body, |x| self.run_on_stmts(x))?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::gdscript::arglist::ArgList; use crate::gdscript::expr::ExprF; use crate::gdscript::stmt::StmtF; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } #[test] fn redundant_assign_test_1() { /* (Eliminate after decl) * var foo = 1 * foo = 2 * foo = 3 */ let body0 = vec!( s(StmtF::VarDecl(String::from("foo"), e(ExprF::from(1)))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(3))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0, }; let body1 = vec!( s(StmtF::VarDecl(String::from("foo"), e(ExprF::from(3)))), ); let func1 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body1, }; RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_2() { /* (Eliminate after decl) * var foo = 1 * "string" * null * foo = 2 */ let body0 = vec!( s(StmtF::VarDecl(String::from("foo"), e(ExprF::from(1)))), s(StmtF::Expr(Expr::from_value("string", SourceOffset::default()))), s(StmtF::Expr(Expr::null(SourceOffset::default()))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0, }; let body1 = vec!( s(StmtF::VarDecl(String::from("foo"), e(ExprF::from(2)))), ); let func1 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body1, }; RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_3() { /* (Eliminate after assign) * foo = 1 * foo = 2 */ let body0 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0, }; let body1 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let func1 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body1, }; RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_4() { /* (Eliminate after assign (compound)) * foo += 1 * foo = 2 */ let body0 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Add, Box::new(e(ExprF::from(1))))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0, }; let body1 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(Expr::from_value(2, SourceOffset::default())))), ); let func1 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body1, }; RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_no_trigger_1() { /* (Do not eliminate if stateful in between) * foo = 1 * fn() * foo = 2 */ let body0 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))), s(StmtF::Expr(e(ExprF::Call(None, String::from("fn"), vec!())))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0.clone(), }; let func1 = func0.clone(); RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_no_trigger_2() { /* (Do not eliminate if stateful on assign) * foo = fn() * foo = 2 */ let body0 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::Call(None, String::from("fn"), vec!()))))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0.clone(), }; let func1 = func0.clone(); RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_no_trigger_3() { /* (Do not eliminate if different variables) * foo = 1 * bar = 2 */ let body0 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))), s(StmtF::Assign(Box::new(Expr::var("bar", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0.clone(), }; let func1 = func0.clone(); RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } #[test] fn redundant_assign_test_no_trigger_4() { /* (Do not eliminate if second assignment is compound) * foo = 1 * foo += 2 */ let body0 = vec!( s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))), s(StmtF::Assign(Box::new(Expr::var("foo", SourceOffset::default())), op::AssignOp::Add, Box::new(e(ExprF::from(2))))), ); let mut func0 = decl::FnDecl { name: String::from("example"), args: ArgList::empty(), body: body0.clone(), }; let func1 = func0.clone(); RedundantAssignmentElimination.run_on_function(&mut func0).unwrap(); assert_eq!(func0, func1); } } ================================================ FILE: src/optimize/gdscript/stmt_walker.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides mechanisms for walking GDScript //! [`Stmt`](crate::gdscript::stmt::Stmt) statements or sequences //! thereof. //! //! The primary endpoints of this module are [`walk_stmt`] and //! [`walk_stmts`] for traversing, respectively, a single statement or //! a slice of statements. use crate::gdscript::stmt::{self, Stmt, StmtF}; use crate::util::extract_err; struct StmtWalker<'a, E> { imp: Box>, } type WalkFn<'a, E> = dyn FnMut(&[Stmt]) -> Result, E> + 'a; impl<'a, E> StmtWalker<'a, E> { fn new(function: impl FnMut(&[Stmt]) -> Result, E> + 'a) -> StmtWalker<'a, E> { StmtWalker { imp: Box::new(function) } } fn walk_stmts(&mut self, stmts: &[Stmt]) -> Result, E> { let mut result = Vec::new(); for stmt in stmts { result.extend(self.walk_stmt(stmt)?); } (self.imp)(&result) } fn walk_stmt(&mut self, stmt: &Stmt) -> Result, E> { let new_stmt = match &stmt.value { StmtF::Expr(_) | StmtF::PassStmt | StmtF::BreakStmt | StmtF::ContinueStmt | StmtF::VarDecl(_, _) | StmtF::ReturnStmt(_) | StmtF::Assign(_, _, _) => stmt.value.clone(), StmtF::IfStmt(stmt::IfStmt { if_clause, elif_clauses, else_clause }) => { let if_clause = (if_clause.0.clone(), self.walk_stmts(&if_clause.1)?); let elif_clauses = elif_clauses.iter().map(|(e, s)| self.walk_stmts(s).map(|s| (e.clone(), s))).collect::>()?; let else_clause = else_clause.as_ref().map(|s| self.walk_stmts(s)).transpose()?; StmtF::IfStmt(stmt::IfStmt { if_clause, elif_clauses, else_clause }) } StmtF::ForLoop(stmt::ForLoop { iter_var, collection, body }) => { let body = self.walk_stmts(body)?; StmtF::ForLoop(stmt::ForLoop { iter_var: iter_var.clone(), collection: collection.clone(), body: body, }) } StmtF::WhileLoop(stmt::WhileLoop { condition, body }) => { let body = self.walk_stmts(body)?; StmtF::WhileLoop(stmt::WhileLoop { condition: condition.clone(), body: body, }) } StmtF::MatchStmt(expr, clauses) => { let clauses = clauses.iter().map(|(p, s)| self.walk_stmts(s).map(|s| (p.clone(), s))).collect::>()?; StmtF::MatchStmt(expr.clone(), clauses) } }; Ok(vec!(Stmt::new(new_stmt, stmt.pos))) } } /// Takes a callable designed to operate on one `&Stmt` and returns a /// callable which operates on each of a slice `&[Stmt]` of statements /// individually, effectively mapping the callable over the slice. /// /// The resulting vectors from each call to `walker` are concatenated /// together into the final vector result. pub fn on_each_stmt<'a, E>(mut walker: impl FnMut(&Stmt) -> Result, E> + 'a) -> impl FnMut(&[Stmt]) -> Result, E> + 'a { move |stmts| { let mut result = Vec::new(); for stmt in stmts { result.extend(walker(stmt)?); } Ok(result) } } /// As [`on_each_stmt`] but with no error type. pub fn on_each_stmt_ok<'a>(mut walker: impl FnMut(&Stmt) -> Vec + 'a) -> impl FnMut(&[Stmt]) -> Vec + 'a { let mut new_walker = on_each_stmt(move |x| Ok(walker(x))); move |stmts| { let result = new_walker(stmts); extract_err(result) } } /// Walks the statement `stmt`, calling `walker` on each sequence of /// statements in `stmt` (including the sequence consisting of `stmt` /// itself). The calls are done in postorder, so changes made on the /// inner structure of the statement during the walk will be reflected /// in the later calls. pub fn walk_stmt<'a, E>(stmt: &Stmt, walker: impl FnMut(&[Stmt]) -> Result, E> + 'a) -> Result, E> { let stmts = vec!(stmt.clone()); walk_stmts(&stmts[..], walker) } /// Walks the sequence of statements `stmts`, calling `walker` on each /// sequence in `stmts` (including `stmts` itself). The calls are done /// in postorder, so changes made on the inner structure during the /// walk will be reflected in later calls. pub fn walk_stmts<'a, E>(stmts: &[Stmt], walker: impl FnMut(&[Stmt]) -> Result, E> + 'a) -> Result, E> { let mut walker = StmtWalker::new(walker); walker.walk_stmts(stmts) } /// As [`walk_stmt`] but with no error type. pub fn walk_stmt_ok<'a>(stmt: &Stmt, mut walker: impl FnMut(&[Stmt]) -> Vec + 'a) -> Vec { let result = walk_stmt(stmt, move |x| Ok(walker(x))); extract_err(result) } /// As [`walk_stmts`] but with no error type. pub fn walk_stmts_ok<'a>(stmts: &[Stmt], mut walker: impl FnMut(&[Stmt]) -> Vec + 'a) -> Vec { let result = walk_stmts(stmts, move |x| Ok(walker(x))); extract_err(result) } #[cfg(test)] mod tests { use super::*; use crate::gdscript::expr::{Expr, ExprF}; use crate::pipeline::source::SourceOffset; // TODO More test coverage #[derive(Copy, Clone, Debug, Eq, PartialEq)] struct SampleError; fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } #[test] fn test_walk_simple_stmt() { let stmt = s(StmtF::PassStmt); let mut call_result = Vec::new(); walk_stmt_ok(&stmt, |x| { call_result.push(x.to_vec()); x.to_vec() }); assert_eq!(call_result, vec!(vec!(s(StmtF::PassStmt)))); } #[test] fn test_walk_simple_stmt_seq() { let stmts = vec!(s(StmtF::PassStmt), s(StmtF::BreakStmt)); let mut call_result = Vec::new(); walk_stmts_ok(&stmts, |x| { call_result.push(x.to_vec()); x.to_vec() }); assert_eq!(call_result, vec!(vec!(s(StmtF::PassStmt), s(StmtF::BreakStmt)))); } #[test] fn test_walk_complex_stmt() { let stmt = stmt::if_else(e(ExprF::from(1)), vec!(s(StmtF::PassStmt)), vec!(s(StmtF::BreakStmt)), SourceOffset::default()); let mut call_result = Vec::new(); walk_stmt_ok(&stmt, |x| { call_result.push(x.to_vec()); x.to_vec() }); assert_eq!(call_result, vec!( vec!(s(StmtF::PassStmt)), vec!(s(StmtF::BreakStmt)), vec!(stmt), )); } #[test] fn test_walk_complex_stmt_seq() { let stmt = stmt::if_else(e(ExprF::from(1)), vec!(s(StmtF::PassStmt)), vec!(s(StmtF::BreakStmt)), SourceOffset::default()); let stmts = vec!(stmt.clone(), s(StmtF::PassStmt)); let mut call_result = Vec::new(); walk_stmts_ok(&stmts, |x| { call_result.push(x.to_vec()); x.to_vec() }); assert_eq!(call_result, vec!( vec!(s(StmtF::PassStmt)), vec!(s(StmtF::BreakStmt)), vec!(stmt, s(StmtF::PassStmt)), )); } #[test] fn test_walk_simple_error_1() { let stmts = vec!(s(StmtF::PassStmt)); let result = walk_stmts(&stmts, on_each_stmt(|_| Err(SampleError))); assert_eq!(result, Err(SampleError)); } #[test] fn test_walk_simple_error_2() { let stmts = vec!(); let result = walk_stmts(&stmts, on_each_stmt(|_| Err(SampleError))); assert_eq!(result, Ok(vec!())); } } ================================================ FILE: src/optimize/gdscript/ternary_if_fold.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::gdscript::stmt::{self, Stmt, StmtF}; use crate::gdscript::expr::{self, Expr, ExprF}; use crate::gdscript::op; use crate::compile::error::GDError; use super::StatementLevelPass; pub struct TernaryIfFold; /* * Factor out common assignment in an if-statement. Specifically, if * we have a statement of the form * * if a: * example = b * elif c: * example = c * ... * else: * example = z * * Where every branch is a simple assignment to a common variable and * none of the conditions depend on that variable in any way. */ impl TernaryIfFold { fn fold_into_ternary(acc: Expr, next: (Expr, Expr)) -> Expr { let pos = acc.pos; Expr::from_value( expr::TernaryIf { true_case: Box::new(next.1), cond: Box::new(next.0), false_case: Box::new(acc), }, pos, ) } fn match_simple_assign<'a>(&self, stmts: &'a [Stmt]) -> Option<(&'a str, &'a Expr)> { if let [Stmt { value: StmtF::Assign(lhs, op::AssignOp::Eq, rhs), pos: _ }] = stmts { if let ExprF::Var(var_name) = &lhs.value { return Some((var_name, rhs)); } } None } fn try_to_run(&self, if_stmt: &stmt::IfStmt) -> Option { let stmt::IfStmt { if_clause, elif_clauses, else_clause } = if_stmt; // Check if if let Some((var_name, if_expr)) = self.match_simple_assign(&if_clause.1) { let mut clauses = vec!((if_clause.0.clone(), if_expr.clone())); // Check elif for (elif_cond, elif_stmts) in elif_clauses { if let Some((var_name_elif, elif_expr)) = self.match_simple_assign(elif_stmts) { if var_name_elif == var_name { clauses.push((elif_cond.clone(), elif_expr.clone())); continue; } } return None; } // Check else (Note: Don't fire if we're missing the else clause) let (else_var_name, else_body) = self.match_simple_assign(else_clause.as_ref()?)?; if else_var_name != var_name { return None; } let else_body = else_body.clone(); let final_expr = clauses.into_iter().rev().fold(else_body, TernaryIfFold::fold_into_ternary); let final_expr_pos = final_expr.pos; return Some(Stmt::simple_assign(Expr::var(var_name, final_expr.pos), final_expr, final_expr_pos)); } None } } impl StatementLevelPass for TernaryIfFold { fn run_on_stmt(&self, stmt: &Stmt) -> Result, GDError> { if let StmtF::IfStmt(if_stmt) = &stmt.value { if let Some(stmt) = self.try_to_run(if_stmt) { return Ok(vec!(stmt)); } } Ok(vec!(stmt.clone())) } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; fn e(expr: ExprF) -> Expr { Expr::new(expr, SourceOffset::default()) } fn s(stmt: StmtF) -> Stmt { Stmt::new(stmt, SourceOffset::default()) } #[test] fn ternary_if_fold_test_1() { /* * if a: * x = 1 * else: * x = 2 */ let stmt1 = s(StmtF::IfStmt(stmt::IfStmt { if_clause: (Expr::var("a", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))))), elif_clauses: vec!(), else_clause: Some(vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2))))))), })); let stmt2 = s(StmtF::Assign( Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::TernaryIf(expr::TernaryIf { true_case: Box::new(e(ExprF::from(1))), cond: Box::new(Expr::var("a", SourceOffset::default())), false_case: Box::new(e(ExprF::from(2))), }))), )); assert_eq!(TernaryIfFold.run_on_stmt(&stmt1).unwrap(), vec!(stmt2)); } #[test] fn ternary_if_fold_test_2() { /* * if a: * x = 1 * elif b: * x = 2 * else: * x = 3 */ let stmt1 = s(StmtF::IfStmt(stmt::IfStmt { if_clause: (Expr::var("a", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))))), elif_clauses: vec!((Expr::var("b", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2)))))))), else_clause: Some(vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(3))))))), })); let stmt2 = s(StmtF::Assign( Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::TernaryIf(expr::TernaryIf { true_case: Box::new(e(ExprF::from(1))), cond: Box::new(Expr::var("a", SourceOffset::default())), false_case: Box::new(e(ExprF::TernaryIf(expr::TernaryIf { true_case: Box::new(e(ExprF::from(2))), cond: Box::new(Expr::var("b", SourceOffset::default())), false_case: Box::new(e(ExprF::from(3))), }))), }))), )); assert_eq!(TernaryIfFold.run_on_stmt(&stmt1).unwrap(), vec!(stmt2)); } #[test] fn ternary_if_fold_test_no_trigger_1() { /* (Different variables) * if a: * x = 1 * elif b: * y = 2 * else: * x = 3 */ let stmt1 = s(StmtF::IfStmt(stmt::IfStmt { if_clause: (Expr::var("a", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))))), elif_clauses: vec!((Expr::var("b", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("y", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2)))))))), else_clause: Some(vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(3))))))), })); assert_eq!(TernaryIfFold.run_on_stmt(&stmt1).unwrap(), vec!(stmt1)); } #[test] fn ternary_if_fold_test_no_trigger_2() { /* (No else) * if a: * x = 1 * elif b: * x = 2 */ let stmt1 = s(StmtF::IfStmt(stmt::IfStmt { if_clause: (Expr::var("a", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(1))))))), elif_clauses: vec!((Expr::var("b", SourceOffset::default()), vec!(s(StmtF::Assign(Box::new(Expr::var("x", SourceOffset::default())), op::AssignOp::Eq, Box::new(e(ExprF::from(2)))))))), else_clause: None, })); assert_eq!(TernaryIfFold.run_on_stmt(&stmt1).unwrap(), vec!(stmt1)); } } ================================================ FILE: src/optimize/gdscript/variables.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . // Helper for getting information about variable usage in a block of code use crate::gdscript::expr::{Expr, ExprF}; use crate::gdscript::stmt::{Stmt, StmtF}; use super::stmt_walker; use super::expr_walker; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] pub struct VarInfo { pub value: Expr, pub read_count: u32, pub write_count: u32, } impl VarInfo { pub fn new(expr: Expr) -> VarInfo { VarInfo { value: expr, read_count: 0, write_count: 0 } } pub fn is_ever_used(&self) -> bool { self.read_count > 0 || self.write_count > 0 } pub fn is_read_only(&self) -> bool { self.write_count == 0 } } pub fn get_variable_info(stmts: &[Stmt]) -> HashMap { let mut map = HashMap::new(); // Read declarations stmt_walker::walk_stmts_ok(stmts, stmt_walker::on_each_stmt_ok(|stmt| { if let StmtF::VarDecl(s, e) = &stmt.value { map.insert(s.to_owned(), VarInfo::new(e.clone())); } vec!(stmt.clone()) // Pass through })); // Read modifications stmt_walker::walk_stmts_ok(stmts, stmt_walker::on_each_stmt_ok(|stmt| { if let StmtF::Assign(s, _, _) = &stmt.value { if let ExprF::Var(s) = &s.value { map.entry(s.to_owned()).and_modify(|v| v.write_count += 1); } } vec!(stmt.clone()) // Pass through })); // Read accesses expr_walker::walk_exprs_ok(stmts, |expr| { if let ExprF::Var(s) = &expr.value { map.entry(s.to_owned()).and_modify(|v| v.read_count += 1); } expr.clone() // Pass through }); map } ================================================ FILE: src/optimize/ir/expr_walker.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::ir::expr::{self, Expr, ExprF}; use crate::ir::decl::{self, Decl, DeclF}; use crate::util::extract_err; // Post-order traversal struct ExprWalker<'a, E> { imp: Box>, } type WalkFn<'a, E> = dyn (FnMut(&Expr) -> Result) + 'a; impl<'a, E> ExprWalker<'a, E> { fn new(function: impl FnMut(&Expr) -> Result + 'a) -> ExprWalker<'a, E> { ExprWalker { imp: Box::new(function) } } fn walk_exprs(&mut self, exprs: &[Expr]) -> Result, E> { exprs.iter().map(|e| { self.walk_expr(e) }).collect() } fn walk_expr(&mut self, expr: &Expr) -> Result { let new_expr = match &expr.value { ExprF::BareName(_) => { expr.value.clone() } ExprF::Literal(_) => { expr.value.clone() } ExprF::Quote(_) => { expr.value.clone() } ExprF::SpecialRef(_) => { expr.value.clone() } ExprF::ContextualFilename(_) => { expr.value.clone() } ExprF::Progn(body) => { ExprF::Progn(self.walk_exprs(body)?) } ExprF::CondStmt(options) => { ExprF::CondStmt(options.iter().map(|(cond, body)| { Ok((self.walk_expr(cond)?, body.as_ref().map(|b| self.walk_expr(b)).transpose()?)) }).collect::, _>>()?) } ExprF::WhileStmt(cond, body) => { ExprF::WhileStmt( Box::new(self.walk_expr(cond)?), Box::new(self.walk_expr(body)?), ) } ExprF::ForStmt(name, iter, body) => { ExprF::ForStmt( name.clone(), Box::new(self.walk_expr(iter)?), Box::new(self.walk_expr(body)?), ) } ExprF::Call(object, name, body) => { let object = match object { expr::CallTarget::Scoped => expr::CallTarget::Scoped, expr::CallTarget::Super => expr::CallTarget::Super, expr::CallTarget::Atomic => expr::CallTarget::Atomic, expr::CallTarget::Object(inner) => expr::CallTarget::Object(Box::new(self.walk_expr(inner)?)), }; ExprF::Call( object, name.clone(), self.walk_exprs(body)?, ) } ExprF::Let(clauses, body) => { let clauses = clauses.iter().map(|clause| { Ok(expr::LocalVarClause { name: clause.name.clone(), value: self.walk_expr(&clause.value)?, }) }).collect::, _>>()?; ExprF::Let( clauses, Box::new(self.walk_expr(body)?), ) } ExprF::FunctionLet(binding_type, clauses, body) => { let clauses = clauses.iter().map(|clause| { Ok(expr::LocalFnClause { name: clause.name.clone(), args: clause.args.clone(), body: self.walk_expr(&clause.body)?, }) }).collect::, _>>()?; ExprF::FunctionLet( *binding_type, clauses, Box::new(self.walk_expr(body)?), ) } ExprF::Lambda(args, body) => { ExprF::Lambda( args.clone(), Box::new(self.walk_expr(body)?), ) } ExprF::FuncRef(expr::FuncRefTarget::SimpleName(s)) => { ExprF::FuncRef( expr::FuncRefTarget::SimpleName(s.clone()), ) } ExprF::Assign(target, rhs) => { let target = match target { expr::AssignTarget::Variable(_, _) => { target.clone() } expr::AssignTarget::InstanceField(pos, inner, name) => { expr::AssignTarget::InstanceField( *pos, Box::new(self.walk_expr(inner)?), name.clone(), ) } }; ExprF::Assign( target, Box::new(self.walk_expr(rhs)?), ) } ExprF::FieldAccess(lhs, name) => { ExprF::FieldAccess( Box::new(self.walk_expr(lhs)?), name.clone(), ) } ExprF::LambdaClass(cls) => { ExprF::LambdaClass( Box::new(self.walk_lambda_class(cls)?), ) } ExprF::Yield(None) => { ExprF::Yield(None) } ExprF::Yield(Some((a, b))) => { ExprF::Yield(Some(( Box::new(self.walk_expr(a)?), Box::new(self.walk_expr(b)?), ))) } ExprF::Assert(cond, message) => { let cond = self.walk_expr(cond)?; let message = message.as_ref().map(|x| self.walk_expr(x)).transpose()?; ExprF::Assert(Box::new(cond), message.map(Box::new)) } ExprF::Return(v) => { ExprF::Return(Box::new(self.walk_expr(v)?)) } ExprF::Break => { ExprF::Break } ExprF::Continue => { ExprF::Continue } ExprF::Split(name, body) => { ExprF::Split( name.clone(), Box::new(self.walk_expr(body)?), ) } ExprF::Preload(name) => { ExprF::Preload(name.to_owned()) } }; let new_expr = Expr::new(new_expr, expr.pos); (self.imp)(&new_expr) } fn walk_lambda_class(&mut self, cls: &expr::LambdaClass) -> Result { // TODO Once we can walk declarations, unify parts of this with // that implementation. let extends = cls.extends.clone(); let args = self.walk_exprs(&cls.args)?; let constructor = cls.constructor.as_ref().map(|c| { let super_call = decl::SuperCall { call: self.walk_exprs(&c.super_call.call)?, pos: c.super_call.pos }; Ok(decl::ConstructorDecl { args: c.args.clone(), super_call: super_call, body: self.walk_expr(&c.body)?, }) }).transpose()?; let decls = cls.decls.iter().map(|d| { // TODO Technically ClassConstDecl and ClassVarDecl contain // expressions. We should walk those too. let new_decl = match &d.value { decl::ClassInnerDeclF::ClassSignalDecl(_) => d.value.clone(), decl::ClassInnerDeclF::ClassConstDecl(_) => d.value.clone(), decl::ClassInnerDeclF::ClassVarDecl(_) => d.value.clone(), decl::ClassInnerDeclF::ClassFnDecl(inner) => { decl::ClassInnerDeclF::ClassFnDecl(decl::ClassFnDecl { is_static: inner.is_static, is_nullargs: inner.is_nullargs, name: inner.name.clone(), args: inner.args.clone(), body: self.walk_expr(&inner.body)?, }) } }; Ok(decl::ClassInnerDecl { value: new_decl, pos: d.pos }) }).collect::, _>>()?; Ok(expr::LambdaClass { extends, args, constructor, decls }) } } pub fn walk_expr<'a, E>(expr: &Expr, walker: impl FnMut(&Expr) -> Result + 'a) -> Result { let mut walker = ExprWalker::new(walker); walker.walk_expr(expr) } pub fn walk_expr_ok<'a>(expr: &Expr, mut walker: impl FnMut(&Expr) -> Expr + 'a) -> Expr { let result = walk_expr(expr, move |x| Ok(walker(x))); extract_err(result) } pub fn walk_exprs_in_decl<'a, E>(decl: &Decl, walker: impl FnMut(&Expr) -> Result + 'a) -> Result { let new_decl = match &decl.value { DeclF::ConstDecl(d) => { DeclF::ConstDecl(decl::ConstDecl { visibility: d.visibility, name: d.name.clone(), value: walk_expr(&d.value, walker)?, }) } DeclF::FnDecl(d) => { DeclF::FnDecl(decl::FnDecl { visibility: d.visibility, call_magic: d.call_magic.clone(), name: d.name.clone(), args: d.args.clone(), body: walk_expr(&d.body, walker)?, }) } DeclF::MacroDecl(d) => { DeclF::MacroDecl(decl::MacroDecl { visibility: d.visibility, name: d.name.clone(), args: d.args.clone(), body: walk_expr(&d.body, walker)?, }) } DeclF::SymbolMacroDecl(d) => { DeclF::SymbolMacroDecl(decl::SymbolMacroDecl { visibility: d.visibility, name: d.name.clone(), body: walk_expr(&d.body, walker)?, }) } DeclF::EnumDecl(d) => { let mut walker = walker; let clauses = d.clauses.iter().map(|(s, e)| { Ok((s.clone(), e.as_ref().map(|e1| walk_expr(e1, |x| walker(x))).transpose()?)) }).collect::, _>>()?; DeclF::EnumDecl(decl::EnumDecl { visibility: d.visibility, name: d.name.clone(), clauses: clauses, }) } DeclF::DeclareDecl(d) => { DeclF::DeclareDecl(d.clone()) } DeclF::ClassDecl(d) => { let mut walker = walker; let mut walker = ExprWalker::new(|x| walker(x)); let constructor = d.constructor.as_ref().map(|c| { let super_call = decl::SuperCall { call: walker.walk_exprs(&c.super_call.call)?, pos: c.super_call.pos }; Ok(decl::ConstructorDecl { args: c.args.clone(), super_call: super_call, body: walker.walk_expr(&c.body)?, }) }).transpose()?; let decls = d.decls.iter().map(|d| { // TODO Technically ClassConstDecl and ClassVarDecl contain // expressions. We should walk those too. let new_decl = match &d.value { decl::ClassInnerDeclF::ClassSignalDecl(_) => d.value.clone(), decl::ClassInnerDeclF::ClassConstDecl(_) => d.value.clone(), decl::ClassInnerDeclF::ClassVarDecl(_) => d.value.clone(), decl::ClassInnerDeclF::ClassFnDecl(inner) => { decl::ClassInnerDeclF::ClassFnDecl(decl::ClassFnDecl { is_static: inner.is_static, is_nullargs: inner.is_nullargs, name: inner.name.clone(), args: inner.args.clone(), body: walker.walk_expr(&inner.body)?, }) } }; Ok(decl::ClassInnerDecl { value: new_decl, pos: d.pos }) }).collect::, _>>()?; DeclF::ClassDecl(decl::ClassDecl { visibility: d.visibility, name: d.name.clone(), extends: d.extends.clone(), main_class: d.main_class, constructor: constructor, decls: decls, }) } }; Ok(Decl::new(new_decl, decl.pos)) } pub fn walk_exprs_in_toplevel<'a, E>(decl: &decl::TopLevel, mut walker: impl FnMut(&Expr) -> Result + 'a) -> Result { Ok(decl::TopLevel { imports: decl.imports.clone(), decls: decl.decls.iter().map(|d| walk_exprs_in_decl(d, |x| walker(x))).collect::, _>>()?, minimalist_flag: decl.minimalist_flag, }) } pub fn walk_exprs_in_toplevel_ok<'a>(decl: &decl::TopLevel, mut walker: impl FnMut(&Expr) -> Expr + 'a) -> decl::TopLevel { let result = walk_exprs_in_toplevel(decl, move |x| Ok(walker(x))); extract_err(result) } // TODO Test me :) ================================================ FILE: src/optimize/ir/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod expr_walker; use crate::ir::decl::TopLevel; use crate::compile::error::GDError; pub trait FileOptimization { fn run_on_file(&self, file: &mut TopLevel) -> Result<(), GDError>; } ================================================ FILE: src/optimize/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod gdscript; pub mod ir; ================================================ FILE: src/parser.lalrpop ================================================ // -*- Rust -*- // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use std::str::FromStr; use crate::sxp::ast::AST; use crate::sxp::string; use crate::sxp::syntax; use crate::pipeline::source::SourceOffset; grammar; Int: i32 = => i32::from_str(s).unwrap(); Float: f32 = => f32::from_str(s).unwrap(); String: String = => { let t = &s[1..s.bytes().count()-1]; string::parse_escapes(t).unwrap() }; Symbol: String = => String::from(s); NodePath: String = { => { String::from(&s[1..]) }, => { let t = &s[2..s.bytes().count()-1]; string::parse_escapes(t).unwrap() }, }; match { r"\s*" => {}, r";[^\n\r]*[\n\r]?" => {}, // Line comments (starting with semicolon) r"#\|([^|]|\|[^#])*\|#" => {}, // Block comments (nesting is not currently allowed) "#t", "#f", r"[+-]?[0-9]+" => INT, r"[+-]?[0-9]+((\.[0-9]+)([eE][+-]?[0-9]+)?|(\.[0-9]+)?([eE][+-]?[0-9]+))" => FLOAT, r#""([^\\"]|\\.)*""# => STRING, r"\$[A-Za-z0-9_~+=\-\\!$%^&*<>/?]+" => PATH, r#"\$"([^\\"]|\\.)*""# => QUOTED_PATH, "(", ")", ".", "[", "]", "'", "#'", "`", ",", ":", "V{", "{", "}", ",.", "@", } else { // A symbol consists of the following, in order: // // 1. A starting character // // 2. An initial segment of zero or more subsequent characters. // // 3. A sequence of zero or more qualifiers, where a qualifier is a // dot followed by one or more subsequent characters. // // The starting character can be an ASCII letter, underscore, tilde, // plus, equal sign, minus, backslash, forward slash, exclamation // mark, percent, caret, ampersand, star, less than, greater than, // question mark, or any Unicode character in the categories L, Mn, // Nl, No, S, Pc, Pd, Po. // // The subsequent characters can be any starting character, an ASCII // number, or any Unicode character in the category N. r"[A-Za-z_~+=\-\\!%^&*<>/?[\p{L}\p{Mn}\p{No}\p{Nl}\p{S}\p{Pc}\p{Pd}\p{Po}--\p{Ascii}]][A-Za-z0-9_~+=\-\\!$%^&*<>/?[\p{L}\p{Mn}\p{N}\p{S}\p{Pc}\p{Pd}\p{Po}--\p{Ascii}]]*(\.[A-Za-z0-9_~+=\-\\!$%^&*<>/?[\p{L}\p{Mn}\p{N}\p{S}\p{Pc}\p{Pd}\p{Po}--\p{Ascii}]]+)*" => SYMBOL, } pub AST: AST = { => ast, "'" => syntax::quote(body, SourceOffset(pos)), "#'" => syntax::function(body, SourceOffset(pos)), "`" => syntax::quasiquote(body, SourceOffset(pos)), "," => syntax::unquote(body, SourceOffset(pos)), ",." => syntax::unquote_spliced(body, SourceOffset(pos)), } pub SomeAST: AST = { => AST::cons(one, AST::nil(SourceOffset(pos)), SourceOffset(pos)), => AST::cons(first, rest, SourceOffset(pos)), } // This is here to fix an ambiguity with the grammar. Specifically, I // want ' a : b to parse as (quote (access-slot a b)), not // (access-slot (quote a) b). There is never a use case where the // latter would be helpful, as (quote a) has no GDScript-only slots on // it. The same applies to function and quasiquote. unquote is a bit // more nebulous, as there are use cases for either possible parse of // ,a:b, but I'm treating it the same as the other three for now. pub LiteralOrNestedAST: AST = { "#t" => AST::from_value(true, SourceOffset(pos)), "#f" => AST::from_value(false, SourceOffset(pos)), => AST::from_value(i, SourceOffset(pos)), => AST::from_value(f, SourceOffset(pos)), => AST::string(s, SourceOffset(pos)), // TODO Escaping => AST::symbol(s, SourceOffset(pos)), "(" ")" => AST::nil(SourceOffset(pos)), "(" ")" => AST::cons(car, cdr, SourceOffset(pos)), "[" "]" => syntax::array(contents, SourceOffset(pos)), "{" "}" => syntax::dict(contents, SourceOffset(pos)), "V{" "}" => syntax::vector2(x, y, SourceOffset(pos)), "V{" "}" => syntax::vector3(x, y, z, SourceOffset(pos)), ":" => syntax::access_slot(body, AST::symbol(name, SourceOffset(rpos)), SourceOffset(pos)), ":" => syntax::get_node_on(body, AST::string(path, SourceOffset(pos)), SourceOffset(pos)), => syntax::get_node_on(AST::symbol("self", SourceOffset(pos)), AST::string(path, SourceOffset(pos)), SourceOffset(pos)), "@" => syntax::access_slot(AST::symbol("self", SourceOffset(pos)), AST::symbol(name, SourceOffset(rpos)), SourceOffset(pos)), } Cdr: AST = { => AST::nil(SourceOffset(pos)), "." => cdr, => { AST::cons(cadr, cddr, SourceOffset(pos)) } } ArrayContents: AST = { => AST::nil(SourceOffset(pos)), => { AST::cons(first, rest, SourceOffset(pos)) } } DictContents: AST = { => AST::nil(SourceOffset(pos)), => { AST::cons(first_key, AST::cons(first_value, rest, SourceOffset(mpos)), SourceOffset(pos)) } } ================================================ FILE: src/parser_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . // These are the tests for parser.lalrpop #[cfg(test)] mod tests { use crate::AST_PARSER; use crate::sxp::ast::{AST, ASTF}; use crate::pipeline::source::SourceOffset; fn so(x: usize) -> SourceOffset { // I'm tired of writing it >.< SourceOffset(x) } #[test] fn parser_simple() { let p = &AST_PARSER; assert_eq!(p.parse("12").unwrap(), AST::new(ASTF::int(12), so(0))); assert_eq!(p.parse("12.0").unwrap(), AST::new(ASTF::float(12.0), so(0))); assert_eq!(p.parse("abc").unwrap(), AST::symbol("abc", so(0))); assert_eq!(p.parse("abc.def").unwrap(), AST::symbol("abc.def", so(0))); assert_eq!(p.parse("\"abc\"").unwrap(), AST::string("abc", so(0))); } #[test] fn parser_simple_unicode() { let p = &AST_PARSER; assert_eq!(p.parse("αβγ").unwrap(), AST::symbol("αβγ", so(0))); // Category Ll assert_eq!(p.parse("ΓΔ.ῼῼ").unwrap(), AST::symbol("ΓΔ.ῼῼ", so(0))); // Categories Lu and Lt assert_eq!(p.parse("aʳ").unwrap(), AST::symbol("aʳ", so(0))); // Category Lm assert_eq!(p.parse("aª").unwrap(), AST::symbol("aª", so(0))); // Category Lo assert_eq!(p.parse("a͚").unwrap(), AST::symbol("a͚", so(0))); // Category Mn assert_eq!(p.parse("Ⅸ³").unwrap(), AST::symbol("Ⅸ³", so(0))); // Categories Nl and No assert_eq!(p.parse("£3÷£2").unwrap(), AST::symbol("£3÷£2", so(0))); // Categories Sm and Sc assert_eq!(p.parse("©©©˄").unwrap(), AST::symbol("©©©˄", so(0))); // Categories Sk and So assert_eq!(p.parse("⁀־־٭").unwrap(), AST::symbol("⁀־־٭", so(0))); // Categories Pc, Pd, and Po assert_eq!(p.parse("value༣༣༣").unwrap(), AST::symbol("value༣༣༣", so(0))); // Category Nd (not allowed at beginning) } #[test] fn parser_string() { let p = &AST_PARSER; assert_eq!(p.parse("\"abcdef\"").unwrap(), AST::string("abcdef", so(0))); assert_eq!(p.parse(r#""abc\"def""#).unwrap(), AST::string("abc\"def", so(0))); assert_eq!(p.parse(r#""abc\\def\\""#).unwrap(), AST::string("abc\\def\\", so(0))); } #[test] fn parser_list() { let p = &AST_PARSER; assert_eq!(p.parse("()").unwrap(), AST::nil(so(0))); assert_eq!(p.parse("(1)").unwrap(), AST::cons(AST::int(1, so(1)), AST::nil(so(2)), so(0))); assert_eq!(p.parse("(1 . 2)").unwrap(), AST::cons(AST::int(1, so(1)), AST::int(2, so(5)), so(0))); assert_eq!(p.parse("(1 2)").unwrap(), AST::cons(AST::int(1, so(1)), AST::cons(AST::int(2, so(3)), AST::nil(so(4)), so(3)), so(0))); } #[test] fn parser_quoting() { let p = &AST_PARSER; assert_eq!(p.parse("'a").unwrap(), AST::list(vec!(AST::symbol("quote", so(0)), AST::symbol("a", so(1))), so(0))); assert_eq!(p.parse("`a").unwrap(), AST::list(vec!(AST::symbol("quasiquote", so(0)), AST::symbol("a", so(1))), so(0))); assert_eq!(p.parse(",a").unwrap(), AST::list(vec!(AST::symbol("unquote", so(0)), AST::symbol("a", so(1))), so(0))); } #[test] fn parser_colon() { let p = &AST_PARSER; assert_eq!(p.parse("a:b").unwrap(), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::symbol("a", so(0)), AST::symbol("b", so(2))), so(0))); assert_eq!(p.parse("(1 . 2):b").unwrap(), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::cons(AST::int(1, so(1)), AST::int(2, so(5)), so(0)), AST::symbol("b", so(8))), so(0))); assert_eq!(p.parse("'a:b").unwrap(), AST::list(vec!(AST::symbol("quote", so(0)), AST::list(vec!(AST::symbol("access-slot", so(1)), AST::symbol("a", so(1)), AST::symbol("b", so(3))), so(1))), so(0))); assert_eq!(p.parse("a:b:c").unwrap(), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::symbol("a", so(0)), AST::symbol("b", so(2))), so(0)), AST::symbol("c", so(4))), so(0))); } #[test] fn parser_at_self() { let p = &AST_PARSER; assert_eq!(p.parse("@b").unwrap(), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::symbol("self", so(0)), AST::symbol("b", so(1))), so(0))); assert_eq!(p.parse("@b:c").unwrap(), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::list(vec!(AST::symbol("access-slot", so(0)), AST::symbol("self", so(0)), AST::symbol("b", so(1))), so(0)), AST::symbol("c", so(3))), so(0))); } #[test] fn parser_comments() { let p = &AST_PARSER; assert_eq!(p.parse("\"abcdef\" ;; test comment").unwrap(), AST::string("abcdef", so(0))); assert_eq!(p.parse("\"abc ;; def\"").unwrap(), AST::string("abc ;; def", so(0))); // Note: Not a comment assert_eq!(p.parse("(a ;; b \n\n\n c)").unwrap(), AST::cons(AST::symbol("a", so(1)), AST::cons(AST::symbol("c", so(12)), AST::nil(so(13)), so(12)), so(0))); assert_eq!(p.parse("\"abcdef\" #| test comment |#").unwrap(), AST::string("abcdef", so(0))); assert_eq!(p.parse("\"abc #| |# def\"").unwrap(), AST::string("abc #| |# def", so(0))); // Note: Not a comment assert_eq!(p.parse("(a #| b |# c)").unwrap(), AST::cons(AST::symbol("a", so(1)), AST::cons(AST::symbol("c", so(11)), AST::nil(so(12)), so(11)), so(0))); } #[test] fn parser_array() { let p = &AST_PARSER; assert_eq!(p.parse("[]").unwrap(), AST::list(vec!(AST::symbol("array", so(0))), so(1))); assert_eq!(p.parse("[1]").unwrap(), AST::list(vec!(AST::symbol("array", so(0)), AST::int(1, so(1))), so(2))); assert_eq!(p.parse("[1 2]").unwrap(), AST::list(vec!(AST::symbol("array", so(0)), AST::int(1, so(1)), AST::int(2, so(3))), so(4))); } #[test] fn parser_dict() { let p = &AST_PARSER; assert_eq!(p.parse("{}").unwrap(), AST::list(vec!(AST::symbol("dict", so(0))), so(1))); assert_eq!(p.parse("{1 2}").unwrap(), AST::list(vec!(AST::symbol("dict", so(0)), AST::int(1, so(1)), AST::int(2, so(3))), so(4))); assert_eq!(p.parse("{1 2 3 4}").unwrap(), AST::list(vec!(AST::symbol("dict", so(0)), AST::int(1, so(1)), AST::int(2, so(3)), AST::int(3, so(5)), AST::int(4, so(7))), so(8))); } #[test] fn parser_failures() { let p = &AST_PARSER; assert!(p.parse("(").is_err()); assert!(p.parse("\"foo\\\"").is_err()); assert!(p.parse(")").is_err()); assert!(p.parse("(()").is_err()); assert!(p.parse("1.").is_err()); assert!(p.parse("()(").is_err()); assert!(p.parse("(1 . )").is_err()); assert!(p.parse("a:").is_err()); assert!(p.parse(":b").is_err()); assert!(p.parse("a:(1 . 2)").is_err()); assert!(p.parse("abc.").is_err()); assert!(p.parse(".def").is_err()); assert!(p.parse("[a").is_err()); assert!(p.parse("{a").is_err()); assert!(p.parse("{a}").is_err()); // Not an even number of entries assert!(p.parse("༣value").is_err()); // Category Nd (not allowed at beginning) assert!(p.parse("valueऻ").is_err()); // Category Mc assert!(p.parse("value⃝value").is_err()); // Category Me assert!(p.parse("value❰").is_err()); // Category Ps assert!(p.parse("value❱").is_err()); // Category Pe assert!(p.parse("value«").is_err()); // Category Pi assert!(p.parse("value»").is_err()); // Category Pf } } ================================================ FILE: src/pipeline/can_load.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`CanLoad`] trait. use crate::runner::path::RPathBuf; use super::Pipeline; /// Trait for things that have a reasonable implementation of "load /// the current file" as an expression. The typical example is /// [`Pipeline`], and this trait is mainly provided as a way for the /// type checker to say "I only need the load expression" as opposed /// to requiring the whole `Pipeline` in general. /// /// That is, there are many functions which need to take a `Pipeline` /// but only do so in order to get the current file name. Those /// functions can opt to take `impl CanLoad` instead, to demonstrate /// to the viewer and the type checker that they only need a limited /// part of the `Pipeline` functionality. pub trait CanLoad { /// Gets the current filename, or `None` if there is no current /// file. fn current_filename_option(&self) -> Option; /// As /// [`current_filename_option`](CanLoad::current_filename_option), /// but panics in case of `None`. Generally, when we call the /// functions on this trait, we fully expect our pipeline (or /// equivalent object) to be in a fully-initialized state. If this /// precondition is violated, we should fail fast. /// /// # Panics /// /// Panics if `current_filename_option` returns `None`. fn current_filename(&self) -> RPathBuf { self.current_filename_option().expect("Could not identify current filename") } } impl CanLoad for Pipeline { fn current_filename_option(&self) -> Option { let mut filename = self.currently_loading_file()?.to_owned(); filename.path_mut().set_extension("gd"); Some(filename) } } ================================================ FILE: src/pipeline/config.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Project-specific configuration data. use crate::runner::version::VersionInfo; use std::path::PathBuf; /// This structure describes any project-specific settings and /// information that is necessary throughout the project compilation /// process. #[derive(Debug, Clone)] pub struct ProjectConfig { /// The path to the root directory of the Godot project. If we're /// compiling a full project, this directory should contain a /// `project.godot`. If we're compiling an individual file, this /// directory should contain that file. pub root_directory: PathBuf, /// Whether or not optimizations are turned on. Generally, this /// setting defaults to `true`, except for integration testing, /// where all optimizations are turned off. pub optimizations: bool, /// The Godot version being used to compile this project. pub godot_version: VersionInfo, } ================================================ FILE: src/pipeline/error.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::compile::error::GDError; use crate::pipeline::source::{SourceOffset, Sourced}; use crate::sxp::dotted::TryFromDottedExprError; use crate::ir::loops::error::LoopPrimitiveError; use crate::ir::arglist::error::ArgListParseError; use crate::ir::modifier::{ParseError as ModifierParseError}; use crate::ir::decl::DuplicateMainClassError; use crate::ir::scope::error::ScopeError; use crate::ir::depends::DependencyError; use lalrpop_util::ParseError; use std::io; use std::fmt; use std::error::Error; #[derive(Debug)] pub enum PError { ParseError(ParseError), IOError(IOError), GDError(GDError), } /// An [`io::Error`] which has a [`SourceOffset`] attached to it. #[derive(Debug)] pub struct IOError { pub error: io::Error, pub pos: SourceOffset, } /// [`PError`] is [`Sourced`] in a somewhat trivial way. It's not a /// recursive data type, so it doesn't "contain" a separate value in /// the same sense as other implementors like [`GDError`] (which /// contains [`GDErrorF`](crate::compile::error::GDErrorF)). But a /// `PError` always has its `SourceOffset` nonetheless. impl Sourced for PError { type Item = PError; fn get_source(&self) -> SourceOffset { match self { PError::ParseError(err) => get_source_from_parse_error(err), PError::IOError(err) => err.pos, PError::GDError(err) => err.get_source(), } } fn get_value(&self) -> &PError { self } } impl Error for PError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { PError::ParseError(err) => Some(err), PError::IOError(err) => Some(&err.error), PError::GDError(err) => Some(err), } } } fn get_source_from_parse_error(err: &ParseError) -> SourceOffset { match err { ParseError::InvalidToken { location } => *location, ParseError::UnrecognizedEOF { location, .. } => *location, ParseError::UnrecognizedToken { token: (location, _, _), .. } => *location, ParseError::ExtraToken { token: (location, _, _), .. } => *location, ParseError::User { .. } => SourceOffset::default(), // TODO There's no error location information for this one. Can we get the user error type E to contain that information somehow? } } impl fmt::Display for PError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PError::ParseError(err) => write!(f, "{}", err), PError::IOError(err) => write!(f, "{}", err), PError::GDError(err) => write!(f, "{}", err), } } } impl fmt::Display for IOError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.error) } } impl IOError { pub fn new(error: io::Error, pos: SourceOffset) -> IOError { IOError { error, pos } } } impl From for io::Error { fn from(err: IOError) -> io::Error { err.error } } fn _impl_partial_eq_warning(err: PError) { // If this function produces exhaustive match warnings, then // PartialEq is missing a case. match err { PError::ParseError(_) => (), PError::IOError(_) => (), PError::GDError(_) => (), } } impl PartialEq for PError { fn eq(&self, other: &PError) -> bool { match (self, other) { (PError::ParseError(a), PError::ParseError(b)) => a == b, (PError::IOError(_), PError::IOError(_)) => true, // Best we can do (PError::GDError(a), PError::GDError(b)) => a == b, (_, _) => false, } } } impl Eq for PError {} impl From> for PError where SourceOffset : From { fn from(e: ParseError) -> PError { PError::ParseError(e.map_location(SourceOffset::from).map_token(|x| x.to_string()).map_error(|x| x.to_string())) } } impl From for PError { fn from(e: IOError) -> PError { PError::IOError(e) } } impl From for PError { fn from(e: GDError) -> PError { PError::GDError(e) } } impl From for PError { fn from(e: TryFromDottedExprError) -> PError { PError::from(GDError::from(e)) } } impl From for PError { fn from(e: ArgListParseError) -> PError { PError::from(GDError::from(e)) } } impl From for PError { fn from(e: ModifierParseError) -> PError { PError::from(GDError::from(e)) } } impl From for PError { fn from(e: DuplicateMainClassError) -> PError { PError::from(GDError::from(e)) } } impl From> for PError where GDError: From> { fn from(e: ScopeError) -> PError { PError::from(GDError::from(e)) } } impl From for PError { fn from(e: LoopPrimitiveError) -> PError { PError::from(GDError::from(e)) } } impl From for PError { fn from(e: DependencyError) -> PError { PError::from(GDError::from(e)) } } ================================================ FILE: src/pipeline/loader.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . #![deprecated] use super::TranslationUnit; use std::path::Path; pub struct NullFileLoader; pub struct NullFileLoaderError; pub trait FileLoader { type Error; fn load_file<'a, 'b, P>(&'a mut self, input_path: &'b P) -> Result<&'a TranslationUnit, Self::Error> where P : AsRef + ?Sized; fn get_file<'a, 'b, P>(&'a self, input_path: &'b P) -> Option<&'a TranslationUnit> where P :AsRef + ?Sized; } // Minimal implementation; always results in an error. impl FileLoader for NullFileLoader { type Error = NullFileLoaderError; fn load_file<'a, 'b, P>(&'a mut self, _input_path: &'b P) -> Result<&'a TranslationUnit, Self::Error> where P : AsRef + ?Sized { Err(NullFileLoaderError) } fn get_file<'a, 'b, P>(&'a self, _input_path: &'b P) -> Option<&'a TranslationUnit> where P :AsRef + ?Sized { None } } ================================================ FILE: src/pipeline/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod error; pub mod translation_unit; pub mod stdlib_unit; pub mod config; pub mod loader; pub mod resolver; pub mod can_load; pub mod source; use translation_unit::TranslationUnit; use config::ProjectConfig; use error::{PError, IOError}; use source::SourceOffset; use resolver::{NameResolver, DefaultNameResolver}; use crate::SOME_AST_PARSER; use crate::ir; use crate::ir::main_function::DisallowMainFunctionHandler; use crate::compile::Compiler; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::body::builder::CodeBuilder; use crate::compile::body::class_scope::OutsideOfClass; use crate::compile::symbol_table::SymbolTable; use crate::compile::preload_resolver::{DefaultPreloadResolver, LookupPreloadResolver}; use crate::compile::error::{GDError, GDErrorF}; use crate::gdscript::library; use crate::gdscript::library::gdnative::NativeClasses; use crate::gdscript::class_extends::ClassExtends; use crate::util; use crate::util::lazy::Lazy; use crate::runner::macro_server::named_file_server::NamedFileServer; use crate::runner::path::{RPathBuf, PathSrc}; use crate::optimize::gdscript::run_standard_passes; use tempfile::Builder; use std::io::Write; use std::path::{Path, PathBuf}; use std::collections::HashMap; use std::mem; pub struct Pipeline { config: ProjectConfig, resolver: Box, known_files: HashMap, known_files_paths: HashMap, server: NamedFileServer, native_classes: Lazy NativeClasses>, current_file_path: Option, } /// The state of a file known to the [`Pipeline`]. enum KnownFile { /// A partially loaded file. An attempt to access a file in this /// state results in a [`GDError::CyclicImport`] error. PartiallyLoaded, /// A fully loaded file, accessible to other files for import. FullyLoaded(Box), } impl Pipeline { pub fn with_resolver(config: ProjectConfig, resolver: Box) -> Pipeline { Pipeline { config: config, resolver: resolver, known_files: HashMap::new(), known_files_paths: HashMap::new(), server: NamedFileServer::new(), native_classes: Lazy::new(Pipeline::get_native_classes_impl), current_file_path: None, } } pub fn new(config: ProjectConfig) -> Pipeline { Pipeline::with_resolver(config, Box::new(DefaultNameResolver)) } fn get_native_classes_impl() -> NativeClasses { let result = NativeClasses::get_api_from_godot(); result.expect("Could not read GDNative API from Godot binary") } pub fn compile_code

+ ?Sized>(&mut self, filename: &P, input: &str) -> Result { let file_path = filename.as_ref().strip_prefix(&self.config.root_directory).expect("Non-local file load detected").to_owned(); let mut old_file_path = Some(RPathBuf::new(PathSrc::Res, file_path).expect("Non-local file load detected")); mem::swap(&mut old_file_path, &mut self.current_file_path); let ast = SOME_AST_PARSER.parse(input)?; let (ir, macros) = ir::compile_and_check(self, &ast, &DisallowMainFunctionHandler)?; let mut compiler = Compiler::new(FreshNameGenerator::new(ast.all_symbols()), Box::new(DefaultPreloadResolver), ir.minimalist_flag); let mut table = SymbolTable::new(); library::bind_builtins(&mut table, ir.minimalist_flag); let mut builder = CodeBuilder::new(ClassExtends::SimpleIdentifier("Node".to_owned())); compiler.frame(self, &mut builder, &mut table, &mut OutsideOfClass).compile_toplevel(&ir)?; let mut result = builder.build(); if self.config.optimizations { run_standard_passes(&mut result)?; } mem::swap(&mut old_file_path, &mut self.current_file_path); let exports = ir::export::get_export_list(&ir.decls); Ok(TranslationUnit::new(filename.as_ref().to_owned(), table, ir, result, exports, macros)) } fn load_file_unconditionally<'a, 'b, P>(&'a mut self, input_path: &'b P) -> Result<&'a TranslationUnit, PError> where P : AsRef + ?Sized { let input_path = input_path.as_ref(); let output_path = input_to_output_filename(input_path); let mut input_file = self.resolver.resolve_input_path(input_path).map_err(|err| IOError::new(err, SourceOffset(0)))?; let mut output_file = self.resolver.resolve_output_path(&output_path).map_err(|err| IOError::new(err, SourceOffset(0)))?; self.known_files.insert(input_path.to_owned(), KnownFile::PartiallyLoaded); let contents = util::read_to_end(&mut input_file).map_err(|err| IOError::new(err, SourceOffset(0)))?; let unit = self.compile_code(&input_path, &contents)?; write!(output_file, "{}", unit.gdscript.to_gd()).map_err(|err| IOError::new(err, SourceOffset(0)))?; let file_path = input_path.strip_prefix(&self.config.root_directory).expect("Non-local file load detected").to_owned(); let mut old_file_path = Some(RPathBuf::new(PathSrc::Res, file_path).expect("Non-local file load detected")); mem::swap(&mut old_file_path, &mut self.current_file_path); // Also output to a temporary file let mut tmpfile = Builder::new() .prefix("__gdlisp_file") .suffix(".gd") .rand_bytes(5) .tempfile() .map_err(|err| IOError::new(err, SourceOffset(0)))?; let mut input_path_store_name = input_path.strip_prefix(&self.config.root_directory).expect("Non-local file load detected").to_owned(); input_path_store_name.set_extension("gd"); self.known_files_paths.insert(input_path_store_name, tmpfile.path().to_owned()); let ast = SOME_AST_PARSER.parse(&contents)?; let resolver = self.make_preload_resolver(); let mut compiler = Compiler::new(FreshNameGenerator::new(ast.all_symbols()), Box::new(resolver), false); let mut table = SymbolTable::new(); library::bind_builtins(&mut table, unit.ir.minimalist_flag); let mut builder = CodeBuilder::new(ClassExtends::SimpleIdentifier("Node".to_owned())); compiler.frame(self, &mut builder, &mut table, &mut OutsideOfClass).compile_toplevel(&unit.ir)?; let mut tmpresult = builder.build(); if self.config.optimizations { run_standard_passes(&mut tmpresult)?; } if !unit.ir.minimalist_flag { write!(tmpfile, "{}", tmpresult.to_gd()).map_err(|err| IOError::new(err, SourceOffset(0)))?; tmpfile.flush().map_err(|err| IOError::new(err, SourceOffset(0)))?; self.server.stand_up_file(tmpfile).map_err(|err| IOError::new(err, SourceOffset(0)))?; } mem::swap(&mut old_file_path, &mut self.current_file_path); self.known_files.insert(input_path.to_owned(), KnownFile::FullyLoaded(Box::new(unit))); Ok(self.get_loaded_file(input_path, SourceOffset(0)).expect("Path not present in load_file")) } pub fn get_loaded_file<'a, 'b, P>(&'a self, input_path: &'b P, pos: SourceOffset) -> Result<&'a TranslationUnit, PError> where P : AsRef + ?Sized { match self.known_files.get(input_path.as_ref()) { None => Err(PError::from(GDError::new(GDErrorF::NoSuchFile(input_path.as_ref().to_string_lossy().into_owned()), pos))), Some(KnownFile::PartiallyLoaded) => Err(PError::from(GDError::new(GDErrorF::CyclicImport(input_path.as_ref().to_string_lossy().into_owned()), pos))), Some(KnownFile::FullyLoaded(unit)) => Ok(unit), } } fn to_absolute_path

(&self, input_path: &P) -> PathBuf where P : AsRef + ?Sized { let input_path = input_path.as_ref(); if input_path.is_absolute() { input_path.to_owned() } else { self.config.root_directory.join(input_path) } } pub fn load_file<'a, 'b, P>(&'a mut self, input_path: &'b P, pos: SourceOffset) -> Result<&'a TranslationUnit, PError> where P : AsRef + ?Sized { let input_path = self.to_absolute_path(input_path); // if-let here causes Rust to complain due to lifetime rules, so // we use contains_key instead. if self.known_files.contains_key(&input_path) { self.get_loaded_file(&input_path, pos) } else { self.load_file_unconditionally(&input_path) } } // TODO This is redundant with get_loaded_file and is only here for // legacy reasons. pub fn get_file<'a, 'b, P>(&'a self, input_path: &'b P, pos: SourceOffset) -> Result<&'a TranslationUnit, PError> where P :AsRef + ?Sized { self.get_loaded_file(input_path, pos) } pub fn get_server(&self) -> &NamedFileServer { &self.server } pub fn get_server_mut(&mut self) -> &mut NamedFileServer { &mut self.server } pub fn make_preload_resolver(&self) -> LookupPreloadResolver { LookupPreloadResolver(self.known_files_paths.clone()) } pub fn currently_loading_file(&self) -> Option<&RPathBuf> { self.current_file_path.as_ref() } // This is done automatically if you use any of the built-in compile // functions above. You can do it yourself manually with this // function for testing purposes. pub fn set_currently_loading_file(&mut self, path: RPathBuf) { self.current_file_path = Some(path); } pub fn config(&self) -> &ProjectConfig { &self.config } pub fn get_native_classes(&mut self) -> &NativeClasses { self.native_classes.force_mut() } } pub fn input_to_output_filename

+ ?Sized>(input: &P) -> PathBuf { let mut output_path = input.as_ref().to_owned(); let renamed = output_path.set_extension("gd"); assert!(renamed); // Make sure the path was actually to a file output_path } ================================================ FILE: src/pipeline/resolver.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the [`NameResolver`] trait and a suitable [default //! implementation](DefaultNameResolver). use std::path::Path; use std::io::{self, Read, BufReader, Write, BufWriter}; use std::fs::File; /// A [`NameResolver`] which simply opens the file it is given, for /// reading or writing as requested. /// /// `DefaultNameResolver` should be good for 99% of actual use cases. /// The primary use case for having a non-standard name resolver is /// during testing, when we want to mock a virtual filesystem rather /// than importing from the real one where feasible. #[derive(Clone, Debug)] pub struct DefaultNameResolver; /// A [`NameResolver`] which always fails. The resolver methods will /// unconditionally panic if called on this object. /// /// This is mainly provided for testing purposes, to assert that a /// given test case should *not* invoke the name resolver. #[derive(Clone, Debug)] pub struct PanickingNameResolver; /// A `NameResolver` specifies to a [`Pipeline`](super::Pipeline) how /// it should read and write data before and after the compilation /// process. /// /// A `Pipeline` is always given an input pathname of a GDLisp source /// file to load. From that pathname, a predetermined process /// translates the name into the output pathname for GDScript code. /// Then the pipeline's `NameResolver` is queried, to actually open /// those files. /// /// For most practical use cases, [`DefaultNameResolver`] is the /// appropriate name resolver. `DefaultNameResolver` simply opens the /// files for input and output respectively, without performing any /// additional transformations. This is provided as a trait to allow /// dependency injection, primarily for testing purposes. A testing /// framework can replace the name resolver in order to load files /// from a virtual testing environment, rather than from the actual /// hard drive. pub trait NameResolver { /// Open the file with the given filename for reading. An /// [`io::Error`] should be signaled if the file does not exist. fn resolve_input_path(&self, filename: &Path) -> io::Result>; /// Open the file with the given filename for writing, clearing the /// file if it already exists. fn resolve_output_path(&self, filename: &Path) -> io::Result>; } impl NameResolver for DefaultNameResolver { fn resolve_input_path(&self, filename: &Path) -> io::Result> { let input_file = BufReader::new(File::open(filename)?); Ok(Box::new(input_file)) } fn resolve_output_path(&self, filename: &Path) -> io::Result> { let output_file = BufWriter::new(File::create(filename)?); Ok(Box::new(output_file)) } } impl NameResolver for PanickingNameResolver { fn resolve_input_path(&self, _filename: &Path) -> io::Result> { panic!("PanickingNameResolver.resolve_input_path") } fn resolve_output_path(&self, _filename: &Path) -> io::Result> { panic!("PanickingNameResolver.resolve_output_path") } } ================================================ FILE: src/pipeline/source.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Wrapper struct and helpers for keeping track of positions in the //! source code. //! //! All source positions always refer to the original GDLisp source //! file, never to the position in an intermediate representation. use crate::util; use std::fmt; use std::io::{self, BufReader}; use std::path::Path; use std::fs::File; /// A `SourceOffset` is really just a [`usize`] representing a /// 0-indexed byte offset into the original file. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[repr(transparent)] pub struct SourceOffset(pub usize); /// A `SourcePos` indicates a line and column number in a file, where /// all offsets are indicated in *characters*. Source position lines /// and columns are always 1-indexed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SourcePos { pub line: usize, pub column: usize, } /// A `SourcedValue` is a view of a [`Sourced`] instance, with its /// [`SourceOffset`] converted to a [`SourcePos`] by looking at the /// original material from which the offset was constructed. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcedValue<'a, T: Sourced> { source: &'a T, error_pos: SourcePos, } /// Trait for types which contain [source offset](SourceOffset) /// information. /// /// Several data structures in GDLisp take the following form. There /// is one type `FooF` which represents some abstract syntax tree. /// `FooF` values contain `Foo` values, while `Foo` is a struct that /// is roughly isomorphic to `(FooF, SourceOffset)`. For any such /// `Foo` type, this trait applies and provides the mechanism to get /// the underlying data structure and the source offset. pub trait Sourced { /// The type of value underlying this data structure. In the `Foo` /// example in the trait description, this would be `FooF`. type Item; /// Gets the [`SourceOffset`] for the position `self` starts at. fn get_source(&self) -> SourceOffset; /// Gets the value underlying `self`. fn get_value(&self) -> &Self::Item; } impl<'a, T: Sourced> SourcedValue<'a, T> { /// Constructs a `SourcedValue` view of `value`, assuming /// `value.get_source()` is a reference to a position in the string /// `source_code`. pub fn new(value: &'a T, source_code: &str) -> SourcedValue<'a, T> { let error_offset = value.get_source(); let error_pos = SourcePos::from_offset(error_offset, source_code); SourcedValue { source: value, error_pos } } /// Constructs a `SourcedValue` view of `value`, assuming /// `value.get_source()` is a reference to a position in the file /// indicated. Returns an IO error in case of error reading the /// file. pub fn from_file

(value: &'a T, source_file: &P) -> io::Result> where P: AsRef + ?Sized { let mut input_file = BufReader::new(File::open(source_file)?); let file_contents = util::read_to_end(&mut input_file)?; Ok(SourcedValue::new(value, &file_contents)) } pub fn get_source_pos(&self) -> SourcePos { self.error_pos } } impl<'a, T: Sourced> Sourced for SourcedValue<'a, T> { type Item = T::Item; fn get_source(&self) -> SourceOffset { self.source.get_source() } fn get_value(&self) -> &Self::Item { self.source.get_value() } } /// Returns the position of all of the newlines in the source text. /// For the purposes of this function, CARRIAGE RETURN (U+001D) and /// NEWLINE (U+001A) are considered to be newlines. Additionally, a /// CRLF (U+001D U+001A) sequence is considered to be only a single /// newline. pub fn find_newlines(source: &str) -> Vec { let mut result = Vec::new(); let mut prev = None; for (idx, ch) in source.bytes().enumerate() { if ch == 13 { result.push(SourceOffset(idx)); } else if ch == 10 { // An LF after a CR doesn't count if prev != Some(13) { result.push(SourceOffset(idx)); } } prev = Some(ch) } result } impl SourcePos { /// Constructs a SourcePos representing the given line and column /// number. pub fn new(line: usize, column: usize) -> SourcePos { SourcePos { line, column } } /// Converts `offset` into a [`SourcePos`] representing a position /// in the given string of text `source`. pub fn from_offset(offset: SourceOffset, source: &str) -> SourcePos { let lines = { let mut lines = find_newlines(source); lines.push(SourceOffset(usize::MAX)); // Implicitly assume there's a final newline at the end of time. lines }; let mut line_number = 1; let mut column_number = 1; for (idx, ch) in source.char_indices() { if idx >= offset.0 { break } if lines[line_number - 1].0 <= idx { line_number += 1; column_number = 1; } else if ch != '\n' { // Windows compatibility (don't advance on CRLF) column_number += 1; } } SourcePos::new(line_number, column_number) } } impl From for SourceOffset { fn from(x: usize) -> SourceOffset { SourceOffset(x) } } impl From for usize { fn from(x: SourceOffset) -> usize { x.0 } } impl fmt::Display for SourceOffset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl fmt::Display for SourcePos { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "line {} column {}", self.line, self.column) } } impl<'a, T: Sourced + fmt::Display> fmt::Display for SourcedValue<'a, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "On {}: {}", self.error_pos, self.source) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_newlines() { let s1 = "abcdef"; assert_eq!(find_newlines(s1), vec!()); let s2 = "abcdef\nghi"; assert_eq!(find_newlines(s2), vec!(SourceOffset(6))); let s3 = "abcdef\nghi\njkl"; assert_eq!(find_newlines(s3), vec!(SourceOffset(6), SourceOffset(10))); let s4 = "abcdef\nghi\rjkl"; assert_eq!(find_newlines(s4), vec!(SourceOffset(6), SourceOffset(10))); let s5 = "abcdef\nghi\r\njkl"; assert_eq!(find_newlines(s5), vec!(SourceOffset(6), SourceOffset(10))); let s6 = "abcdef\n\nghi\r\njkl"; assert_eq!(find_newlines(s6), vec!(SourceOffset(6), SourceOffset(7), SourceOffset(11))); let s7 = "abcdef\n\nghi\r\n\rjkl"; assert_eq!(find_newlines(s7), vec!(SourceOffset(6), SourceOffset(7), SourceOffset(11), SourceOffset(13))); let s8 = "abcdef\n\nghi\r\n\r\njkl"; assert_eq!(find_newlines(s8), vec!(SourceOffset(6), SourceOffset(7), SourceOffset(11), SourceOffset(13))); } #[test] fn test_offset() { assert_eq!(SourcePos::from_offset(SourceOffset(0), "abc"), SourcePos::new(1, 1)); assert_eq!(SourcePos::from_offset(SourceOffset(1), "abc"), SourcePos::new(1, 2)); assert_eq!(SourcePos::from_offset(SourceOffset(2), "abc"), SourcePos::new(1, 3)); assert_eq!(SourcePos::from_offset(SourceOffset(3), "abc"), SourcePos::new(1, 4)); assert_eq!(SourcePos::from_offset(SourceOffset(0), "abc\ndef"), SourcePos::new(1, 1)); assert_eq!(SourcePos::from_offset(SourceOffset(1), "abc\ndef"), SourcePos::new(1, 2)); assert_eq!(SourcePos::from_offset(SourceOffset(2), "abc\ndef"), SourcePos::new(1, 3)); assert_eq!(SourcePos::from_offset(SourceOffset(3), "abc\ndef"), SourcePos::new(1, 4)); assert_eq!(SourcePos::from_offset(SourceOffset(4), "abc\ndef"), SourcePos::new(2, 1)); assert_eq!(SourcePos::from_offset(SourceOffset(5), "abc\ndef"), SourcePos::new(2, 2)); assert_eq!(SourcePos::from_offset(SourceOffset(6), "abc\ndef"), SourcePos::new(2, 3)); assert_eq!(SourcePos::from_offset(SourceOffset(7), "abc\ndef"), SourcePos::new(2, 4)); assert_eq!(SourcePos::from_offset(SourceOffset(0), "abc\r\ndef"), SourcePos::new(1, 1)); assert_eq!(SourcePos::from_offset(SourceOffset(1), "abc\r\ndef"), SourcePos::new(1, 2)); assert_eq!(SourcePos::from_offset(SourceOffset(2), "abc\r\ndef"), SourcePos::new(1, 3)); assert_eq!(SourcePos::from_offset(SourceOffset(3), "abc\r\ndef"), SourcePos::new(1, 4)); assert_eq!(SourcePos::from_offset(SourceOffset(4), "abc\r\ndef"), SourcePos::new(2, 1)); assert_eq!(SourcePos::from_offset(SourceOffset(5), "abc\r\ndef"), SourcePos::new(2, 1)); assert_eq!(SourcePos::from_offset(SourceOffset(6), "abc\r\ndef"), SourcePos::new(2, 2)); assert_eq!(SourcePos::from_offset(SourceOffset(7), "abc\r\ndef"), SourcePos::new(2, 3)); assert_eq!(SourcePos::from_offset(SourceOffset(8), "abc\r\ndef"), SourcePos::new(2, 4)); } } ================================================ FILE: src/pipeline/stdlib_unit.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`StdlibUnit`] structure. use crate::compile::symbol_table::SymbolTable; use crate::ir::identifier::Id; use crate::ir::macros::MacroData; use super::translation_unit::TranslationUnit; use serde::{Serialize, Deserialize}; use std::collections::HashMap; /// A stripped-down version of [`TranslationUnit`] containing the /// parts of the unit that are necessary to understand the standard /// library. #[derive(Serialize, Deserialize)] pub struct StdlibUnit { /// See [`TranslationUnit::table`]. pub table: SymbolTable, /// See [`TranslationUnit::exports`]. pub exports: Vec, /// See [`TranslationUnit::macros`]. pub macros: HashMap, } impl From for StdlibUnit { fn from(unit: TranslationUnit) -> StdlibUnit { let TranslationUnit { table, exports, macros, .. } = unit; StdlibUnit { table, exports, macros } } } ================================================ FILE: src/pipeline/translation_unit.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`TranslationUnit`] structure. use crate::compile::symbol_table::SymbolTable; use crate::ir::decl::TopLevel; use crate::ir::identifier::Id; use crate::ir::macros::MacroData; use crate::gdscript::decl::TopLevelClass; use std::path::PathBuf; use std::collections::HashMap; /// A translation unit is the result of compiling a single file of /// GDLisp code. As we compile a Godot project, the /// [`Pipeline`](super::Pipeline) keeps an index of loaded files in /// the form of translation units. If the same file is required /// multiple times, the latter loads can simply query the cached file. /// As such, this structure necessarily keeps all of the information /// needed to reconstruct the entire relevant compilation process for /// import purposes. #[derive(Debug)] pub struct TranslationUnit { /// The path to the file this translation unit references. pub filename: PathBuf, /// The compiler symbol table for the top-level of this translation /// unit. This includes all top-level declarations which are /// available at runtime. /// /// Note that, at present, this symbol table includes the names of /// macros defined in the file. This may change in the future, if /// macros become unavailable at runtime. pub table: SymbolTable, /// The intermediate representation used during compilation of the /// translation unit. pub ir: TopLevel, /// The compiled GDScript code, the final result of compilation. pub gdscript: TopLevelClass, /// A vector of exported identifiers. This includes any names which /// should be available from imports and explicitly *excludes* those /// names marked private or those generated for local use, such as /// lambda closure classes. pub exports: Vec, /// A map of all of the macros defined in the translation unit. /// /// This map should exclude macros which were imported into the /// unit's scope but were not defined there, so /// [`MacroData::imported`] should be false for every value in this /// map. pub macros: HashMap, } impl TranslationUnit { /// Convenience function for constructing translation units. pub fn new(filename: PathBuf, table: SymbolTable, ir: TopLevel, gdscript: TopLevelClass, exports: Vec, macros: HashMap) -> TranslationUnit { TranslationUnit { filename, table, ir, gdscript, exports, macros } } /// Clone the [`TranslationUnit`]. /// /// `TranslationUnit` does not directly implement [`Clone`], and for /// good reason. The identifiers in a given translation unit (for /// example, those in [`TranslationUnit::macros`]) apply to a /// particular [`Pipeline`](super::Pipeline). Associating a /// translation unit with a different pipeline can result in /// unintended consequences. Thus, we provide this explicit /// mechanism for cloning a translation unit. This method should /// only be called if you *definitely* know what you're doing. /// /// In particular, `clone_detached` is safe on translation units /// which do not define any macros, or which define only macros /// using reserved identifiers. That means that `clone_detached` is /// safe to use on the translation unit describing the GDLisp /// standard library. pub fn clone_detached(&self) -> TranslationUnit { TranslationUnit { filename: self.filename.clone(), table: self.table.clone(), ir: self.ir.clone(), gdscript: self.gdscript.clone(), exports: self.exports.clone(), macros: self.macros.clone(), } } } impl From for TopLevelClass { fn from(trans: TranslationUnit) -> TopLevelClass { trans.gdscript } } ================================================ FILE: src/repl.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use crate::pipeline::Pipeline; use crate::pipeline::source::SourceOffset; use crate::pipeline::error::{PError, IOError}; use crate::pipeline::config::ProjectConfig; use crate::runner::path::RPathBuf; use crate::sxp::ast::AST; use crate::ir; use crate::ir::incremental::IncCompiler; use crate::ir::macros::MacroData; use crate::ir::identifier::{Namespace, Id}; use crate::ir::main_function::StaticMainFunctionHandler; use crate::compile::Compiler; use crate::compile::body::builder::CodeBuilder; use crate::compile::body::class_scope::OutsideOfClass; use crate::compile::symbol_table::SymbolTable; use crate::compile::symbol_table::local_var::VarName; use crate::compile::symbol_table::function_call::FnSpecs; use crate::compile::symbol_table::call_magic::CallMagic; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::preload_resolver::DefaultPreloadResolver; use crate::compile::error::{GDError, GDErrorF}; use crate::gdscript::library; use crate::gdscript::class_extends::ClassExtends; use crate::gdscript::metadata::REPL_FUNCTION_NAME; use crate::optimize::gdscript::run_standard_passes; use crate::SOME_AST_PARSER; use tempfile::Builder; use std::convert::TryFrom; use std::io::Write; use std::path::Path; use std::collections::HashMap; /// A `Repl` instance maintains the state of a read-eval-print loop. /// Specifically, it maintains a running Godot instance /// (lazily-initialized, as always), as well as a table of all known /// symbols from *previous* REPL commands. /// /// Commands are sent to a `Repl` via [`Repl::run_code`] or /// [`Repl::parse_and_run_code`]. pub struct Repl { pipeline: Pipeline, full_symbol_table: SymbolTable, full_macro_table: HashMap, /// Indicates whether the REPL has had its REPL-specific /// initialization code run yet. initialized: bool, } impl Repl { const REPL_FILENAME: &'static str = if cfg!(windows) { "C:/tmp/REPL.lisp" } else { "/tmp/REPL.lisp" }; pub fn new(config: ProjectConfig) -> Repl { Repl::with_pipeline(Pipeline::new(config)) } pub fn with_pipeline(pipeline: Pipeline) -> Repl { Repl { pipeline, full_symbol_table: SymbolTable::new(), full_macro_table: HashMap::new(), initialized: false, } } fn initialize(&mut self) { // Do this first, or else the parse_and_run_code implementation // below will end us up in an infinite loop. self.initialized = true; // Re-route print to printerr, since we suppress stdout output. // Fix for https://github.com/Mercerenies/gdlisp/issues/119. self.parse_and_run_code("(defn print (&rest args) (apply #'printerr args))") .expect("Internal error in Repl::initialize"); } /// Normally, the macro server and all of the subsystems are lazy /// and only load when requested. However, this creates the awkward /// effect where the REPL loads fast but then the first command (and /// only the first command) takes a long time to run. This function /// can be called to force the subsystems to load immediately. pub fn force_load(&mut self) { self.parse_and_run_code("()").expect("Internal error in force_load"); } /// Runs the code given by the AST as GDLisp source. The result is /// returned. In case of success, the result shall be the string /// representation of the value produced by the expression. On /// failure, the result shall be the error message. /// /// The top-level AST shall be a list, or a GDLisp error will be /// returned. The list can consist of a mix of expressions and /// declarations freely. The declarations will be put into scope /// first (in the order they are declared), and then (and *only* /// then) the expressions will be evaluated in order. If there are /// no expressions and the declarations are successfully compiled, /// then a string representation of the nil list will be returned as /// a default value. /// /// `run_code` exhibits a strong exception guarantee. If this /// function returns an error, then the [`Repl`] object it was /// called on has not been modified. On the other hand, if this /// function returns an `Ok` value, then the `Repl` object's /// internal symbol table will have any new declarations from the /// given code added to it. /// /// For a version that parses the code and *then* runs it, see /// [`parse_and_run_code`](Repl::parse_and_run_code). pub fn run_code(&mut self, code: &AST) -> Result { if !self.initialized { self.initialize(); } self.pipeline.set_currently_loading_file(RPathBuf::try_from(String::from(Repl::REPL_FILENAME)).unwrap()); let mut compiler = Compiler::new(FreshNameGenerator::new(code.all_symbols()), Box::new(DefaultPreloadResolver), false); let mut icompiler = IncCompiler::with_ambient_symbols(code.all_symbols(), self.full_symbol_table.clone()); // TODO Don't like this clone here, symbol tables are big and cloning is expensive. icompiler.bind_macros_from(self.full_macro_table.iter().map(|(id, data)| (id.clone(), data.clone()))); let (ir, macros) = icompiler.compile_toplevel(&mut self.pipeline, code, &Repl::main_function_handler())?; ir::check_ir(&ir)?; if ir.minimalist_flag { return Err(PError::from(GDError::new(GDErrorF::MinimalistAtRepl, SourceOffset(0)))); } let mut table = SymbolTable::new(); library::bind_builtins(&mut table, false); self.bind_existing_names(&mut table); let mut builder = CodeBuilder::new(ClassExtends::SimpleIdentifier("Node".to_owned())); compiler.frame(&mut self.pipeline, &mut builder, &mut table, &mut OutsideOfClass).compile_toplevel(&ir)?; let mut result = builder.build(); if self.pipeline.config().optimizations { run_standard_passes(&mut result)?; } let mut tmpfile = Builder::new() .prefix("__gdlisp_replfile") .suffix(".gd") .rand_bytes(5) .tempfile() .map_err(|err| IOError::new(err, SourceOffset(0)))?; let tmpfile_name = tmpfile.path().to_owned(); write!(tmpfile, "{}", result.to_gd()).map_err(|err| IOError::new(err, SourceOffset(0)))?; tmpfile.flush().map_err(|err| IOError::new(err, SourceOffset(0)))?; let server = self.pipeline.get_server_mut(); let macro_id = server.stand_up_macro(String::from(REPL_FUNCTION_NAME), tmpfile) .map_err(|err| IOError::new(err, SourceOffset(0)))?; let final_result = server.run_server_file_str(macro_id, vec!(), FnSpecs::EMPTY, vec!(), SourceOffset(0))?; // If everything happened correctly, then save the symbols we got // from this line. self.remember_symbols(&tmpfile_name, &mut table, &ir::export::get_export_list(&ir.decls)); self.full_macro_table.extend(macros.into_iter()); Ok(final_result) } /// Parse the code as one or more GDLisp S-expressions and then run /// the result in the REPL. /// /// See [`Repl::run_code`] for more details on the effects running /// code has on the [`Repl`] object itself. pub fn parse_and_run_code(&mut self, code: &str) -> Result { let ast = SOME_AST_PARSER.parse(code)?; self.run_code(&ast) } /// Returns true if the child process backing the REPL is still /// running. /// /// Note that there is a frame of lag between a Godot command that /// terminates the REPL and the actual termination, so if a call to /// [`Repl::run_code`] or [`Repl::parse_and_run_code`] terminates /// the REPL, then it will take approximately 16 milliseconds before /// `Repl::is_running` begins to return false. pub fn is_running(&mut self) -> bool { self.pipeline.get_server_mut().is_process_healthy() } fn remember_symbols(&mut self, filename: &Path, table: &mut SymbolTable, exports: &[Id]) { let direct_load_import = VarName::DirectLoad(filename.to_string_lossy().into_owned()); for name in exports { match name.namespace { Namespace::Value => { let mut var = table.get_var(&name.name).unwrap().to_owned(); var.name = var.name.into_imported_var(direct_load_import.clone()); self.full_symbol_table.set_var(name.name.to_owned(), var); } Namespace::Function => { let mut func = table.get_fn(&name.name).unwrap().0.to_owned(); func.object = func.object.into_imported_var(direct_load_import.clone()); self.full_symbol_table.set_fn(name.name.to_owned(), func, CallMagic::DefaultCall); } } } } fn bind_existing_names(&self, table: &mut SymbolTable) { table.assign_from(&self.full_symbol_table); } fn main_function_handler() -> StaticMainFunctionHandler { StaticMainFunctionHandler::new(String::from(REPL_FUNCTION_NAME)) } } #[cfg(test)] mod tests { use super::*; use crate::runner::version::VersionInfo; use std::path::PathBuf; use std::thread::sleep; use std::time::Duration; fn dummy_config() -> ProjectConfig { ProjectConfig { root_directory: PathBuf::from("."), optimizations: false, godot_version: VersionInfo::default(), } } #[test] fn basic_repl_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(+ 1 1)"), Ok(String::from("2"))); } #[test] fn quoted_repl_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("'(1 2 . (3 . 4))"), Ok(String::from("(1 2 3 . 4)"))); } #[test] fn array_repl_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("[1 2 3 4]"), Ok(String::from("[1 2 3 4]"))); } #[test] fn dict_repl_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("{1 2 3 4}"), Ok(String::from("{1 2 3 4}"))); } #[test] fn decl_then_expr_in_one_repl_command_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(defn foo (x) (+ x 1)) (foo 100)"), Ok(String::from("101"))); } #[test] fn force_load_in_repl_test() { let mut repl = Repl::new(dummy_config()); // This returns nothing, so there's nothing to assert. But we do // expect this function to run without panicking. repl.force_load(); } #[test] fn decl_then_expr_in_two_repl_commands_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(defn foo (x) (+ x 1))"), Ok(String::from("()"))); assert_eq!(repl.parse_and_run_code("(foo 100)"), Ok(String::from("101"))); assert!(repl.is_running()); } #[test] fn failed_decl_then_expr_repl_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(defn foo (x) (+ x 1)) nonexistent-variable"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("nonexistent-variable")), SourceOffset(23))))); assert_eq!(repl.parse_and_run_code("(foo 100)"), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("foo")), SourceOffset(0))))); } #[test] fn failed_repl_continues_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(defn foo (x) (+ x 1))"), Ok(String::from("()"))); assert_eq!(repl.parse_and_run_code("nonexistent-variable"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("nonexistent-variable")), SourceOffset(0))))); assert_eq!(repl.parse_and_run_code("(foo 100)"), Ok(String::from("101"))); } #[test] fn minimalist_at_repl_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(sys/nostdlib)"), Err(PError::from(GDError::new(GDErrorF::MinimalistAtRepl, SourceOffset(0))))); } #[test] #[ignore = "Race condition (see issue #127)"] fn terminate_repl_via_tree_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("((GDLisp:get-tree):quit)"), Ok(String::from("()"))); sleep(Duration::from_millis(200)); assert!(!repl.is_running()); } #[test] #[ignore = "Race condition (see issue #127)"] fn terminate_repl_via_quit_test() { let mut repl = Repl::new(dummy_config()); assert_eq!(repl.parse_and_run_code("(quit)"), Ok(String::from("()"))); sleep(Duration::from_millis(200)); assert!(!repl.is_running()); } } ================================================ FILE: src/runner/godot.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides a DSL for executing arbitrary Godot subprocesses. use std::process::{Command, Stdio, Output, Child, ExitStatus}; use std::path::Path; use std::ffi::OsStr; use std::io; /// `GodotCommand` is a wrapper around [`Command`] specifically /// designed for running Godot processes. This structure implements /// much of the same interface as `Command`, as well as adding some /// Godot-specific functions. /// /// `GodotCommand` instances are created either using /// [`raw`](GodotCommand::raw) or [`base`](GodotCommand::base). #[derive(Debug)] pub struct GodotCommand { command: Command, } impl GodotCommand { /// A raw [`GodotCommand`]. This is the simplest way to create a /// Godot command. It provides *no* command line arguments or /// special stream handling, leaving everything at the [`Command`] /// defaults. /// /// Equivalent to `Command::new("godot")`, except wrapped as a /// `GodotCommand`. /// /// For most GDLisp use cases, [`base`](GodotCommand::base) should /// be preferred, as it implements common sense defaults on top of /// `raw`. pub fn raw() -> Self { GodotCommand { command: Command::new("godot"), } } /// Constructs a [`GodotCommand`] with sensible defaults. This /// includes passing the `--no-window` argument to Godot, as well as /// setting stderr to [`Stdio::inherit`] and stdout to /// [`Stdio::piped`]. /// /// For a version of [`GodotCommand`] with no defaults set, see /// [`raw`](GodotCommand::raw). pub fn base() -> Self { let mut instance = Self::raw(); instance .no_window() .stderr(Stdio::inherit()) .stdout(Stdio::piped()); instance } /// Adds an argument to pass to Godot. /// /// See [`Command::arg`]. pub fn arg(&mut self, arg_text: impl AsRef) -> &mut Self { self.command.arg(arg_text); self } /// Adds the `--no-window` option to the command line arguments. /// /// This is already included if `self` was created with /// [`GodotCommand::base`], so it's only necessary for commands /// created with [`GodotCommand::raw`]. pub fn no_window(&mut self) -> &mut Self { self.arg("--no-window") } /// Adds a command line argument indicating to the Godot process /// that the script at the given file path should be run. pub fn script_file(&mut self, filename: &Path) -> &mut Self { self.arg("-s").arg(filename) } /// Adds a command line argument indicating to the Godot process /// that the Godot project in the given directory should be run. /// There must be a `project.godot` file in the directory. /// /// Note that the path argument to `project_dir` should be a /// *directory*, not the `project.godot` file itself. pub fn project_dir(&mut self, directory_name: &Path) -> &mut Self { self.arg("--path").arg(directory_name) } /// Adds the `--quit` option to the command line arguments, /// indicating that the process should quit after one iteration. pub fn quit_after_one(&mut self) -> &mut Self { self.arg("--quit") } /// Adds the `--quiet` option to the command line arguments. pub fn quiet(&mut self) -> &mut Self { self.arg("--quiet") } /// Adds a single environment variable that will be visible to the /// Godot process. /// /// See [`Command::env`]. pub fn env(&mut self, key: K, val: V) -> &mut Self where K: AsRef, V: AsRef { self.command.env(key, val); self } /// Adds environment variables that will be visible to the Godot /// process. /// /// See [`Command::envs`]. pub fn envs(&mut self, vars: I) -> &mut Self where I: IntoIterator, K: AsRef, V: AsRef { self.command.envs(vars); self } /// Sets a handler for the standard error stream. /// /// See [`Command::stderr`]. pub fn stderr(&mut self, cfg: impl Into) -> &mut Self { self.command.stderr(cfg); self } /// Sets a handler for the standard output stream. /// /// See [`Command::stdout`]. pub fn stdout(&mut self, cfg: impl Into) -> &mut Self { self.command.stdout(cfg); self } /// Sets a handler for the standard input stream. /// /// See [`Command::stdin`]. pub fn stdin(&mut self, cfg: impl Into) -> &mut Self { self.command.stdin(cfg); self } /// Executes the command, waits on it to terminate, and then /// collects all of its output. /// /// See [`Command::output`]. pub fn output(&mut self) -> io::Result { self.command.output() } /// Executes the command, waits on it to terminate, and then /// returns its exit status. /// /// See [`Command::status`]. pub fn status(&mut self) -> io::Result { self.command.status() } /// Executes the command, returning a [`Child`] instance /// immediately. /// /// See [`Command::spawn`]. pub fn spawn(&mut self) -> io::Result { self.command.spawn() } } impl From for Command { fn from(godot_command: GodotCommand) -> Command { godot_command.command } } ================================================ FILE: src/runner/into_gd_file.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! This module defines [`IntoGDFile`], a trait for objects which can //! be written to disk as GDScript source files. //! //! The primary implementor is [`TopLevelClass`], which represents //! GDScript source code as a Rust-side AST. use crate::gdscript::decl::TopLevelClass; use std::io::{self, Write}; use std::borrow::Borrow; /// A trait for objects which can be reasonably written to a file as /// GDScript source code. pub trait IntoGDFile { /// Writes the contents of `self` to `file` as GDScript source code. fn write_to_gd(&self, file: &mut impl Write) -> io::Result<()>; } /// A [`TopLevelClass`] can be written to a file using /// [`to_gd`](TopLevelClass::to_gd()). impl IntoGDFile for TopLevelClass { fn write_to_gd(&self, file: &mut impl Write) -> io::Result<()> { write!(file, "{}", self.to_gd()) } } /// A string of GDScript source code can be written to a file /// verbatim. impl + ?Sized> IntoGDFile for T { fn write_to_gd(&self, file: &mut impl Write) -> io::Result<()> { write!(file, "{}", self.borrow()) } } ================================================ FILE: src/runner/macro_server/command.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! The types representing commands which can be sent to a //! [`MacroServer`](super::MacroServer). //! //! [`ServerCommand`] represents the various commands which can be //! sent to the macro server. Commands are eventually encoded as JSON //! and passed onto the server. Eventually, the server will receive a //! [`ServerResponse`](super::response::ServerResponse) back as reply. //! //! Note that, while it is *highly* unlikely that this will ever come //! into play, the total length of a command, including the JSON //! formatting and command name, cannot exceed 2^32 characters. Thus, //! any string arguments to `ServerCommand` should not approach that //! length. use json::JsonValue; /// A server command. #[derive(Clone, Debug, Eq, PartialEq)] pub enum ServerCommand { Quit, Ping, Eval(String), Exec(String), Load(String), } impl ServerCommand { /// The name of the command, as a string. pub fn name(&self) -> &'static str { match self { ServerCommand::Quit => "quit", ServerCommand::Ping => "ping", ServerCommand::Eval(_) => "eval", ServerCommand::Exec(_) => "exec", ServerCommand::Load(_) => "load", } } /// The arguments to the command, as a sequence of strings. pub fn arguments(&self) -> Vec<&str> { match self { ServerCommand::Quit => vec!(), ServerCommand::Ping => vec!(), ServerCommand::Eval(s) => vec!(s), ServerCommand::Exec(s) => vec!(s), ServerCommand::Load(s) => vec!(s), } } /// Convert the command to JSON. /// /// Commands are sent as JSON objects which have the following keys. /// /// * command (required) - The name of the command. /// /// * args (required) - An array of values, usually strings. Its /// interpretation depends on the command being run. pub fn to_json(&self) -> JsonValue { let command = String::from(self.name()); let args = self.arguments().into_iter().map(JsonValue::from).collect(); json::object!{ "command" => command, "args" => JsonValue::Array(args), } } } ================================================ FILE: src/runner/macro_server/lazy.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides [`LazyServer`] for lazily constructing //! [`MacroServer`](super::MacroServer) instances. use super::MacroServer; use std::io; use std::process::ExitStatus; /// A macro server that may or may not have been initialized yet. /// /// A `LazyServer` is similar to a /// [`Lazy`](https://doc.rust-lang.org/std/lazy/struct.Lazy.html), /// in that it doesn't actually construct a macro server until the /// value is actually forced. This provides the key benefit that the /// server will not be forced as soon as the compiler starts up, /// instead waiting until it's actually required for macro resolution, /// macro definition, or for storage of a completed file. /// /// Note that, unlike `Lazy`, this type does *not* provide /// [`Deref`](std::ops::Deref). Macro servers must be explicitly /// queried with [`LazyServer::get_mut`]. This is because the macro server /// construction is a nontrivial operation, so we require the user to /// clearly assert that they understand the cost. pub struct LazyServer(Option); // **Note:** [`Default`] doesn't make as much sense for this type, as // it's meant to behave like a server, not a simple datatype. The fact // that it *can* be default-allocated is an implementation detail. #[allow(clippy::new_without_default)] impl LazyServer { /// Construct a new, empty `LazyServer`. No additional processes /// will be spawned. pub fn new() -> LazyServer { LazyServer(None) } /// Query whether or not the server has been started. pub fn is_running(&self) -> bool { self.0.is_some() } /// Get the macro server, starting a new server process if one has /// not been started yet. /// /// If the server has already been started /// ([`is_running`](LazyServer::is_running) is true), then this /// method is guaranteed to succeed. If the server has not been /// started yet, then IO errors while spawning the process will be /// propagated to the return value of this method. If an error /// occurs, the `LazyServer` is guaranteed to be left in an /// uninitialized state, and future calls to `get_mut` will attempt /// to spawn the process once again. pub fn get_mut(&mut self) -> io::Result<&mut MacroServer> { if self.0.is_none() { self.0 = Some(MacroServer::new()?); } Ok(self.0.as_mut().unwrap()) } /// Returns the macro server, but only if it has already been /// started. If the macro server has not yet been started, then this /// will return `None`. /// /// For a function that lazily initializes the server if it hasn't /// been initialized, see [`LazyServer::get_mut`]. pub fn get_mut_if_initialized(&mut self) -> Option<&mut MacroServer> { self.0.as_mut() } /// Consume the `LazyServer` and shut down the server, if it's /// running. This is equivalent to simply dropping the `LazyServer` /// instance but allows custom error handling to be implemented. /// /// If the lazy server is uninitialized, then `Ok(None)` will always /// be returned. Otherwise, the server will be shut down via /// [`MacroServer::shutdown`] and any errors will be propagated out. /// Assuming there are no IO errors during shutdown, the result will /// be `Ok(Some(exit_status))`, where `exit_status` is the exit /// status of the macro server. pub fn shutdown(self) -> io::Result> { match self.0 { None => Ok(None), Some(x) => x.shutdown().map(Some), } } } ================================================ FILE: src/runner/macro_server/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Functionality for constructing and interacting with a Godot server //! process. //! //! This module provides [`MacroServer`], for spawning Godot server //! processes. A `MacroServer` can be sent [`ServerCommand`] commands //! and will receive responses in the form of [`ServerResponse`]. //! //! In addition to the primitive interface, this module also provides //! submodules for richer server interaction. [`lazy`] provides a //! means of lazily constructing a macro server, delaying construction //! of the child process until absolutely necessary. //! [`named_file_server`] provides a high-level API for providing //! macros and other resources to a server and for calling macros on a //! server. pub mod command; pub mod response; pub mod lazy; pub mod named_file_server; use super::godot::GodotCommand; use crate::gdscript::library; use command::ServerCommand; use response::ServerResponse; use std::io::{self, Write, Read, ErrorKind}; use std::process::{Child, Stdio, ExitStatus}; use std::net::{TcpListener, TcpStream}; use std::convert::{TryFrom, TryInto}; use std::mem::ManuallyDrop; use std::sync::{Mutex, MutexGuard}; use std::env::current_exe; /// The TCP port used for communicating with the macro server. pub const DEFAULT_PORT_NUMBER: u16 = 61992; lazy_static! { static ref MACRO_SERVER_LOCK: Mutex<()> = Mutex::new(()); } /// A `MacroServer` instance manages a child Godot process and a TCP /// connection to that process. Using this instance, callers can send /// commands and receive responses via /// [`issue_command`](MacroServer::issue_command). pub struct MacroServer { tcp_server: TcpStream, godot_server: Child, } impl MacroServer { /// Construct a new `MacroServer`. This function immediately spawns /// a child process. For a lazily-constructed server that only /// spawns once necessary, consider using [`lazy::LazyServer`] /// instead. pub fn new() -> io::Result { Self::new_on_ports(DEFAULT_PORT_NUMBER, u16::MAX) } pub fn new_on_ports(min_port: u16, max_port: u16) -> io::Result { // This mutex protects our access to GDLisp.gd. Especially when // testing (but also just in general), multiple threads can easily // try to run the fs::copy command below at the same time, // resulting in an inconsistent state of the GDLisp.gd file. With // this mutex, the following steps are atomic: // // 1. Initialize stdlib (if uninitialized) // 2. Copy the GDLisp.gd file into the subdirectory // 3. Load Godot and allow it to read GDLisp.gd // // No other thread can start copying GDLisp.gd until our current // thread allows its subprocess to parse it fully. let _lock_guard = MacroServer::lock_macro_server_init()?; library::ensure_stdlib_loaded(); let (tcp_listener, port) = MacroServer::try_to_bind_port(min_port, max_port)?; let gd_server = run_godot_child_process(port)?; let (tcp_server, _) = tcp_listener.accept()?; Ok(MacroServer { tcp_server: tcp_server, godot_server: gd_server, }) } fn lock_macro_server_init() -> io::Result> { MACRO_SERVER_LOCK.lock().map_err(|_| { io::Error::new(io::ErrorKind::Other, "MACRO_SERVER_LOCK was poisoned") }) } fn try_to_bind_port(start: u16, end: u16) -> io::Result<(TcpListener, u16)> { for port in start..=end { match TcpListener::bind(("127.0.0.1", port)) { Ok(listener) => { return Ok((listener, port)); } Err(err) if err.kind() == ErrorKind::AddrInUse => { // Continue. } Err(err) => { // Unhandled error return Err(err); } } } Err(io::Error::new(ErrorKind::AddrInUse, "Could not find TCP port to bind")) } fn send_string(&mut self, string: &str) -> io::Result<()> { let mut buf = Vec::new(); let len: u32 = string.len().try_into().expect("String too long to send to Godot TCP server"); buf.extend(len.to_be_bytes()); buf.extend(string.bytes()); self.tcp_server.write_all(&buf)?; Ok(()) } fn receive_string(&mut self) -> io::Result { let mut len_buf = [0; 4]; self.tcp_server.read_exact(&mut len_buf)?; let len: usize = u32::from_be_bytes(len_buf).try_into().expect("String too long to receive from Godot TCP server"); let mut buf = vec![0; len]; self.tcp_server.read_exact(&mut buf)?; String::from_utf8(buf).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Error in UTF8 conversion")) } /// Issues the given command to the macro server, waits on a /// response, and returns a [`ServerResponse`] indicating success or /// failure. pub fn issue_command(&mut self, command: &ServerCommand) -> io::Result { let json = command.to_json(); self.send_string(&json.to_string())?; let result = self.receive_string()?; let result = json::parse(&result).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))?; ServerResponse::try_from(result).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string())) } // Unsafe to use the server afterward, so we only expose a public // version that takes ownership. fn _shutdown(&mut self) -> io::Result { self.issue_command(&ServerCommand::Quit)?; self.godot_server.wait() } /// Shuts down the server process. This is equivalent to simply /// dropping `self` except that this method allows the caller to /// handle any error conditions that arise from shutting down the /// server. pub fn shutdown(self) -> io::Result { let mut server = ManuallyDrop::new(self); server._shutdown() } /// Returns true if the Godot subprocess is running and is healthy. /// If the process has terminated for any reason or the GDLisp /// process cannot communicate with it, then this function returns false. pub fn is_process_healthy(&mut self) -> bool { match self.godot_server.try_wait() { Ok(Some(_)) => { // Process has terminated. false } Ok(None) => { // Process has not terminated. true } Err(_) => { // Error occurred trying to get process status; assume // unhealthy. false } } } } /// Dropping a `MacroServer` kills the child process, suppressing any /// errors that result from doing so. impl Drop for MacroServer { fn drop(&mut self) { let _r = self._shutdown(); // Ignore io::Result (we're in Drop so we can't handle it) } } fn run_godot_child_process(port: u16) -> io::Result { let exe_path = current_exe()?; let exe_dir = exe_path.parent().ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not locate executable path"))?; GodotCommand::base() .project_dir(&exe_dir.join("MacroServer")) .env("GDLISP_PORT_NUMBER", port.to_string()) .stdout(Stdio::null()) .spawn() } #[cfg(test)] mod tests { use super::*; use crate::sxp::reify::Reify; use crate::AST_PARSER; use json::object; use std::convert::TryFrom; use std::thread::sleep; use std::time::Duration; fn issue_command_and_unwrap(server: &mut MacroServer, value: &ServerCommand) -> String { let result = server.issue_command(value).unwrap(); String::try_from(result).unwrap() } fn roundtrip_value(server: &mut MacroServer, value: &str) { let ast = AST_PARSER.parse(value).unwrap(); let result = issue_command_and_unwrap(server, &ServerCommand::Eval(ast.reify().to_gd())); assert_eq!(value, &result); } #[test] fn spawn_server_simple_test() { MacroServer::new().unwrap().shutdown().unwrap(); } #[test] fn spawn_server_ping_pong_test() { let mut server = MacroServer::new().unwrap(); let response = issue_command_and_unwrap(&mut server, &ServerCommand::Ping); assert_eq!(response, "pong"); server.shutdown().unwrap(); } #[test] fn spawn_server_eval_test() { let mut server = MacroServer::new().unwrap(); let response = issue_command_and_unwrap(&mut server, &ServerCommand::Eval(String::from("1 + 1"))); assert_eq!(response, "2"); } #[test] fn spawn_server_roundtrip_test_1() { let mut server = MacroServer::new().unwrap(); roundtrip_value(&mut server, "(1)"); roundtrip_value(&mut server, "(1 2 . 3)"); roundtrip_value(&mut server, "(1 . 2)"); roundtrip_value(&mut server, "(array #t #f abc def)"); roundtrip_value(&mut server, "(array 10 20 (30 40) \"ABC\")"); roundtrip_value(&mut server, "(array 10 20 (30 40 . 50) \"ABC\")"); roundtrip_value(&mut server, "(array 10 20 (30 40 50 (60 70)) \"ABC\")"); } #[test] fn spawn_server_roundtrip_test_2() { let mut server = MacroServer::new().unwrap(); roundtrip_value(&mut server, "\"ABC\""); roundtrip_value(&mut server, "\"αβγ ⊕\""); roundtrip_value(&mut server, "\"😁😁😁😁😁😁\""); roundtrip_value(&mut server, r#""abc\"def""#); roundtrip_value(&mut server, r#""''\"'\"'""#); roundtrip_value(&mut server, r#""\\""#); roundtrip_value(&mut server, r#""\"\\\"""#); roundtrip_value(&mut server, r#""\\\\\\""#); } #[test] fn spawn_server_roundtrip_test_3() { let mut server = MacroServer::new().unwrap(); roundtrip_value(&mut server, r#""\n\t\t\n""#); roundtrip_value(&mut server, r#""\r\r\r\r""#); roundtrip_value(&mut server, r#""abc \a def""#); roundtrip_value(&mut server, r#""\t\v\f\f""#); roundtrip_value(&mut server, r#""xxx\b\b\b""#); roundtrip_value(&mut server, r#""\v""#); roundtrip_value(&mut server, r#""\\v""#); roundtrip_value(&mut server, r#""\\\v""#); roundtrip_value(&mut server, r#""\\\\v""#); } #[test] fn spawn_server_load_test() { let mut server = MacroServer::new().unwrap(); let load_response = issue_command_and_unwrap(&mut server, &ServerCommand::Load(String::from("res://TestLoadedFile.gd"))); assert_eq!(load_response, "0"); let eval_response = issue_command_and_unwrap(&mut server, &ServerCommand::Eval(String::from(r#"MAIN.loaded_files[0].example()"#))); assert_eq!(eval_response, "\"Test succeeded\""); server.shutdown().unwrap(); } #[test] fn spawn_server_exec_test() { let mut server = MacroServer::new().unwrap(); let command = ServerCommand::Exec(String::from(" var tmp_var = 1 + 1\n return tmp_var")); let response = issue_command_and_unwrap(&mut server, &command); assert_eq!(response, "2"); } #[test] fn spawn_server_bad_json_test() { let mut server = MacroServer::new().unwrap(); let command = "[{INVALID_JSON"; server.send_string(command).unwrap(); let response = server.receive_string().unwrap(); let response = json::parse(&response).unwrap(); let expected_response = object!{ "error_code" => 30, // ERR_INVALID_DATA "error_string" => "Invalid JSON Expected key", "response_string" => "", }; assert_eq!(response, expected_response); } #[test] fn healthy_server_test() { let mut server = MacroServer::new().unwrap(); assert!(server.is_process_healthy()); } #[test] #[ignore = "Race condition (see issue #127)"] fn unhealthy_server_test() { let mut server = MacroServer::new().unwrap(); let command = ServerCommand::Eval(String::from("GDLisp.get_tree().quit()")); issue_command_and_unwrap(&mut server, &command); // The quit command will terminate the Godot subprocess within // 1/60th of a second (Godot will terminate at the end of the // current frame, and the default framerate is 60 frames per // second). So we'll wait 300 milliseconds to make sure it has // time to terminate. sleep(Duration::from_millis(300)); assert!(!server.is_process_healthy()); } } ================================================ FILE: src/runner/macro_server/named_file_server.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! A server which manages macro resolution and can run Godot code. //! //! A [`NamedFileServer`] is a more powerful form of //! [`MacroServer`](super::MacroServer). Whereas the latter manages //! the primitive operations of sending commands to and from a Godot //! process, a `NamedFileServer` manages the higher-level operations //! of declaring and calling macros. use crate::sxp::ast::AST; use crate::runner::macro_server::lazy::LazyServer; use crate::compile::names; use crate::compile::names::fresh::FreshNameGenerator; use crate::compile::symbol_table::local_var::VarName; use crate::compile::symbol_table::function_call::{FnCall, FnScope, FnSpecs, FnName}; use crate::compile::symbol_table::call_magic::compile_default_call; use crate::compile::stmt_wrapper::{self, StmtWrapper}; use crate::gdscript::expr::{Expr as GDExpr}; use crate::gdscript::stmt::Stmt; use crate::gdscript::library; use crate::pipeline::error::{PError, IOError}; use crate::pipeline::source::SourceOffset; use crate::AST_PARSER; use super::command::ServerCommand; use super::response; use tempfile::NamedTempFile; use serde::{Serialize, Deserialize}; use std::collections::HashMap; use std::path::Path; use std::io; use std::convert::TryFrom; /// A `NamedFileServer` maintains a [`LazyServer`], as well as a /// registry of the macros which have been uploaded to the server. /// /// Macros are normally uploaded to the file server via /// [`stand_up_macro`](NamedFileServer::stand_up_macro), which also /// adds the macro to the server's registry for later access. /// Alternatively, files which do *not* contain macros can be uploaded /// with [`stand_up_file`](NamedFileServer::stand_up_file). The latter /// method loads a file onto the macro server without adding it to the /// registry. Hence, the file is available for other macros to use, /// provided they know its name, but it is unavailable for direct /// calling, since it is not a macro. Finally, /// [`add_reserved_macro`](NamedFileServer::add_reserved_macro) is /// used to add data to the registry without standing up any files. /// This is used to add standard library macros (which are side-loaded /// using another mechanism before `NamedFileServer` is even /// constructed) to the registry. /// /// Macros are indexed by [`MacroID`], a wrapper struct around `u32`. pub struct NamedFileServer { server: LazyServer, macro_files: HashMap, next_id: MacroID, next_reserved_id: MacroID, } /// A simple identifier type which is used as the key type in /// [`NamedFileServer`]. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] #[repr(transparent)] pub struct MacroID(u32); #[derive(Debug)] struct MacroCall { index: u32, // Index in the lookup table on the GDScript side. #[allow(dead_code)] original_name: String, // Probably not needed, but we have it so we may as well keep track of it. name: String, #[allow(dead_code)] file: Option, // Macros can optionally retain a file resource, which will be deleted when the macro is discarded from scope. } const RESERVED_MACRO_INDEX: u32 = u32::MAX; // TODO Make this return GDError like it probably should. fn response_to_string(response: response::ServerResponse) -> io::Result { String::try_from(response).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } #[allow(clippy::new_without_default)] impl NamedFileServer { /// Constructs a new `NamedFileServer`. The server does not /// initially spawn off a Godot process and will only do so once /// required to (similar to [`LazyServer`]). pub fn new() -> NamedFileServer { NamedFileServer { server: LazyServer::new(), macro_files: HashMap::new(), next_id: MacroID::smallest_unreserved(), next_reserved_id: MacroID::smallest_stdlib(), } } /// Adds a reserved standard library macro to this file server's /// known macros list. /// /// A reserved macro is a special kind of macro that does not have /// its own file. Instead, a reserved macro is a macro that is /// side-loaded onto the macro server via some other means (usually, /// by being present in the standard library file `GDLisp.lisp`, /// which gets preloaded into the macro server at process start /// time). This function does *not* instruct the Godot process to /// load any new files or to do anything at all. Instead, this /// function simply makes the `NamedFileServer` struct aware that /// there is a macro with the given name in the standard library /// file. It is the caller's responsibility to ensure that the /// information is actually correct. pub fn add_reserved_macro(&mut self, name: String) -> MacroID { let id = self.next_reserved_id; self.next_reserved_id = self.next_reserved_id.next(); self.macro_files.insert(id, MacroCall { index: RESERVED_MACRO_INDEX, original_name: name.clone(), name, file: None, }); id } fn load_file_on_server(&mut self, path: &Path) -> io::Result { let server = self.server.get_mut()?; let cmd = ServerCommand::Load((*path.to_string_lossy()).to_owned()); let result: String = response_to_string(server.issue_command(&cmd)?)?; result.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } /// Stand up a file on the `NamedFileServer`. /// /// This should be used instead of /// [`stand_up_macro`](NamedFileServer::stand_up_macro) in cases /// where the resulting file will never be called directly. /// Specifically, this should be used to supply the macro server /// with resources that are not directly macros but which may be /// needed indirectly by other macros in the future. The file will /// simply be loaded into the server using [`ServerCommand::Load`] /// blindly, and it is the caller's responsibility to keep track of /// the filename and provide some way to access the loaded file. pub fn stand_up_file(&mut self, file: NamedTempFile) -> io::Result<()> { self.load_file_on_server(file.path())?; Ok(()) } /// Stand up a file as a macro file on the `NamedFileServer`. /// /// This is the more powerful form of `stand_up_file`. In addition /// to physically loading the file via [`ServerCommand::Load`], this /// method also stores the macro, with the given name and argument /// list, in the `NamedFileServer`'s macro registry, which can later /// be accessed via /// [`run_server_file`](NamedFileServer::run_server_file). /// /// This method returns a [`MacroID`] which can be used later to /// call the macro. pub fn stand_up_macro(&mut self, name: String, file: NamedTempFile) -> io::Result { let idx = self.load_file_on_server(file.path())?; let gdname = names::lisp_to_gd(&name); let call = MacroCall { index: idx, original_name: name, name: gdname, file: Some(file) }; let id = self.next_id; self.next_id = self.next_id.next(); self.macro_files.insert(id, call); Ok(id) } fn get_file(&self, id: MacroID) -> Option<&MacroCall> { self.macro_files.get(&id) } /// Run a macro with the given arguments. `id` should be a /// [`MacroID`] returned from a prior call to /// [`stand_up_macro`](NamedFileServer::stand_up_macro) or similar, /// and `args` should be a list of Godot expressions to provide as /// the arguments. In case of Godot errors, including but not /// limited to parsing errors, IO communication errors, or semantic /// errors in macro evaluation, an `Err` value is returned. /// /// `prelude` is a vector of statements which will be run before the /// actual macro call and can be used to set up the environment or /// provide local variables to the macro arguments. /// /// # Panics /// /// This method will panic if given an invalid `id` value. The `id` /// must be the macro ID from a prior invocation of `stand_up_macro` /// or [`add_reserved_macro`](NamedFileServer::add_reserved_macro) /// on the same `NamedFileServer` instance. pub fn run_server_file(&mut self, id: MacroID, prelude: Vec, specs: FnSpecs, args: Vec, pos: SourceOffset) -> Result { let result = self.run_server_file_str(id, prelude, specs, args, pos)?; let parsed = AST_PARSER.parse(&result)?; //println!("{}", parsed); Ok(parsed) } pub fn run_server_file_str(&mut self, id: MacroID, prelude: Vec, specs: FnSpecs, args: Vec, pos: SourceOffset) -> Result { let call = self.get_file(id).expect("Invalid MacroID in run_server_file"); let call_object = if id.is_reserved() { let gdlisp_root = library::gdlisp_root_var_name(); FnName::OnLocalVar(Box::new(gdlisp_root)) } else { // A non-reserved macro should never end up at this position. // (This would get caught by the below try_from, but we can // supply a better error message in this specific case) if call.index == RESERVED_MACRO_INDEX { panic!("Non-reserved macro at reserved index {}", call.index); } let index_error_message = format!("Macro reference indices exceeded i32 range, got {}", call.index); let call_index = i32::try_from(call.index).expect(&index_error_message); let target = VarName::SubscriptedConstant( Box::new(VarName::ImportedConstant( Box::new(VarName::FileConstant(String::from("MAIN"))), String::from("loaded_files"), )), call_index, ); FnName::OnLocalVar(Box::new(target)) }; let call = FnCall { scope: FnScope::Global, object: call_object, function: call.name.to_owned(), specs: specs, is_macro: true, }; self.do_macro_call(prelude, call, args, pos) } fn do_macro_call(&mut self, prelude: Vec, call: FnCall, args: Vec, pos: SourceOffset) -> Result { let server = self.server.get_mut().map_err(|err| IOError::new(err, pos))?; let expr = compile_default_call(call, args, pos)?; let mut stmts = prelude; stmts.push(stmt_wrapper::Return.wrap_expr(expr)); let mut exec_str = String::new(); Stmt::write_gd_stmts(stmts.iter(), &mut exec_str, 4).expect("Could not write to string in do_macro_call"); let result = server.issue_command(&ServerCommand::Exec(exec_str)).map_err(|err| IOError::new(err, pos))?; let result = response_to_string(result).map_err(|err| IOError::new(err, pos))?; Ok(result) } /// Issues a command to the server setting its global name generator /// to `gen`. pub fn set_global_name_generator(&mut self, gen: &FreshNameGenerator) -> io::Result<()> { let server = self.server.get_mut()?; let json = gen.to_json(); let exec_str = format!(" GDLisp.__gdlisp_Global_name_generator = GDLisp.sys_DIV_FreshNameGenerator.from_json({})", json); let cmd = ServerCommand::Exec(exec_str); let _result = response_to_string(server.issue_command(&cmd)?)?; Ok(()) } /// Issues a command to the server setting the global name generator /// to a newly-constructed name generator. pub fn reset_global_name_generator(&mut self) -> io::Result<()> { let server = self.server.get_mut()?; let exec_str = String::from(r#" GDLisp.__gdlisp_Global_name_generator = GDLisp.sys_DIV_FreshNameGenerator.new([], "")"#); let cmd = ServerCommand::Exec(exec_str); let _result = response_to_string(server.issue_command(&cmd)?)?; Ok(()) } /// Returns true if the Godot subprocess is running and is healthy. /// If the child process has started, then this delegates to /// [`MacroServer::is_process_healthy`](super::MacroServer::is_process_healthy). /// If not, then this returns false, as the process is not running. pub fn is_process_healthy(&mut self) -> bool { match self.server.get_mut_if_initialized() { None => false, Some(server) => server.is_process_healthy(), } } } impl MacroID { // Current partitioning scheme: // // 0-63: Unused (future use) // 64-511: Built-in (stdlib) macros // 512-1023: Unused (future use) // 1024-max: User-defined macros const RESERVED: u32 = 1024; fn smallest_stdlib() -> MacroID { MacroID(64) } fn smallest_unreserved() -> MacroID { MacroID(MacroID::RESERVED) } fn is_reserved(self) -> bool { self.0 < MacroID::RESERVED } fn next(self) -> MacroID { MacroID(self.0 + 1) } } ================================================ FILE: src/runner/macro_server/response.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Types for representing the success or failure of a //! [`ServerCommand`](super::command::ServerCommand). use json::JsonValue; use std::convert::TryFrom; use std::fmt; use std::error::Error; /// A response received from the server as the result of a command. /// /// `ServerResponse` objects are typically constructed using /// [`TryFrom::`] on the actual JSON data received from the /// server. #[derive(Clone, Debug, Eq, PartialEq)] pub enum ServerResponse { Failure(Failure), Success(Success), } /// A negative response, indicating that something went wrong during /// command evaluation. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Failure { /// A [Godot error /// code](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-constant-ok) /// indicating what went wrong. pub error_code: u32, /// A user-friendly string detailing the error. pub error_string: String, } /// A positive response, indicating successful completion of the /// command. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Success { /// A string detailing the response. The exact format and use of /// this string is entirely dependent on the command issued. pub response_string: String, } /// An error during parsing of a [`JsonValue`] as a /// [`ServerResponse`]. #[derive(Clone, Debug)] pub enum ParseError { MalformedResponse(String), } fn fail(json: &JsonValue) -> ParseError { ParseError::MalformedResponse(json.to_string()) } impl TryFrom for Success { type Error = Failure; fn try_from(resp: ServerResponse) -> Result { match resp { ServerResponse::Failure(err) => Err(err), ServerResponse::Success(val) => Ok(val), } } } impl TryFrom for String { type Error = Failure; fn try_from(resp: ServerResponse) -> Result { let Success { response_string } = Success::try_from(resp)?; Ok(response_string) } } /// Responses are received as JSON objects which have the following keys. /// /// * error_code (required) - 0 if successful, or a Godot error code /// if failed. /// /// * error_string (required) - A string of text, possibly empty, /// specifying more details about the error. /// /// response_string (required) - A string of text specifying the /// response. This string must be empty if an error occurred. impl TryFrom for ServerResponse { type Error = ParseError; fn try_from(json: JsonValue) -> Result { let obj = match &json { JsonValue::Object(obj) => obj, _ => return Err(fail(&json)), }; let error_code = obj.get("error_code") .and_then(|err| err.as_u32()) .ok_or_else(|| fail(&json))?; if error_code == 0 { // Success case let response_string = obj.get("response_string").and_then(|r| r.as_str()).ok_or_else(|| fail(&json))?; let response_string = response_string.to_owned(); Ok(ServerResponse::Success(Success { response_string })) } else { // Failure case let error_string = obj.get("error_string").and_then(|r| r.as_str()).ok_or_else(|| fail(&json))?; let error_string = error_string.to_owned(); Ok(ServerResponse::Failure(Failure { error_code, error_string })) } } } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ParseError::MalformedResponse(s) => { writeln!(f, "{}", s) } } } } impl fmt::Display for Failure { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{} {}", self.error_code, self.error_string) } } impl Error for Failure {} ================================================ FILE: src/runner/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Mechanisms for running Godot code in child processes. //! //! The functions defined in this module are general-purpose functions //! for running arbitrary Godot code in child processes. The submodule //! [`macro_server`] provides the more specific use-case of starting //! up a Godot process as a server and communicating with it via TCP. pub mod godot; pub mod into_gd_file; pub mod macro_server; pub mod named_file; pub mod path; pub mod version; use godot::GodotCommand; use tempfile::{Builder, NamedTempFile}; use std::io::{self, Seek, SeekFrom}; use std::fmt; use std::error::Error; /// An error in the exit status of the process spawned by /// `dump_json_api`. #[derive(Debug, Clone)] pub struct JsonApiError; impl fmt::Display for JsonApiError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "error reading JSON API from Godot binary") } } impl Error for JsonApiError {} /// Runs a Godot process to dump the JSON API for GDNative to a /// (newly-created) temporary file. This method returns the temporary /// file object. The corresponding file will be deleted when the /// returned value is dropped. pub fn dump_json_api() -> io::Result { let mut file = make_tmp_file()?; let status = GodotCommand::base() .quiet() .arg("--gdnative-generate-json-api") .arg(file.path()) .status()?; if !status.success() { return Err(io::Error::new(io::ErrorKind::Other, JsonApiError)); } // Not sure if this is necessary, but better safe than sorry. Ensure // that the seek position of the opened file handle is at 0. file.seek(SeekFrom::Start(0))?; Ok(file) } fn make_tmp_file() -> io::Result { Builder::new() .prefix("__gdlisp_test") .suffix(".gd") .rand_bytes(5) .tempfile() } #[cfg(test)] mod tests { use super::*; use crate::runner::into_gd_file::IntoGDFile; use std::io::Write; fn run_with_temporary(data: &str) -> io::Result { let mut tmp = make_tmp_file()?; data.write_to_gd(&mut tmp)?; tmp.flush()?; let out = GodotCommand::base() .script_file(tmp.path()) .quit_after_one() .output()?; let text = String::from_utf8_lossy(&out.stdout); Ok(text.into()) } #[test] fn run_minimal_test() { run_with_temporary(r#" extends SceneTree func _init(): pass "#).unwrap(); } #[test] fn run_printing_test() { let out = run_with_temporary(r#" extends SceneTree func _init(): print("999_SAMPLE_OUTPUT_STRING_999") "#).unwrap(); assert!(out.contains("999_SAMPLE_OUTPUT_STRING_999")); } #[test] fn dump_json_api_test() { // Checks that dump_json_api completes successfully and produces // output. It does *not* check the contents of the output. That is // tested over in `crate::gdscript::library::gdnative`. let mut tempfile = dump_json_api().unwrap(); let file_len = tempfile.seek(SeekFrom::End(0)).unwrap(); assert!(file_len > 0); } } ================================================ FILE: src/runner/named_file.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the [`NamedFile`] trait and some trivial implementations. use tempfile::NamedTempFile; use std::path::{Path, PathBuf}; use std::io; use std::fs::File; use std::borrow::ToOwned; /// Instances of `NamedFile` are file-like objects which have a path. /// Any object which has a reasonable notion of "file path" can /// implement this trait. pub trait NamedFile : io::Write { /// Returns the path of the file represented by this object. fn path(&self) -> &Path; } /// A `SimpleNamedFile` is a [`NamedFile`] which simply contains a /// path and the file with no additional behavior. This struct is most /// useful in situations where [`NamedTempFile`]-like semantics are /// desired, but we wish to persist the relevant file. pub struct SimpleNamedFile(PathBuf, File); impl SimpleNamedFile { /// Construct a `SimpleNamedFile`. This will attempt to open the /// file for writing, and in case of failure will return the /// appropriate IO error. pub fn create

>(path: P) -> io::Result { let buf = path.as_ref().to_owned(); File::create(path).map(|f| SimpleNamedFile(buf, f)) } } impl io::Write for SimpleNamedFile { fn write(&mut self, buf: &[u8]) -> io::Result { self.1.write(buf) } fn flush(&mut self) -> io::Result<()> { self.1.flush() } } impl NamedFile for SimpleNamedFile { fn path(&self) -> &Path { self.0.as_ref() } } impl NamedFile for NamedTempFile { fn path(&self) -> &Path { NamedTempFile::path(self) } } ================================================ FILE: src/runner/path.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Structure for representing a Godot resource path. //! //! Godot resource paths are represented using a special syntax. There //! are three types of paths that can be represented in this way. //! //! 1. **Absolute paths** have no prefix and are written as-is. These //! paths start from the file-system root and refer to a specific //! location. //! //! 2. **Resource paths** begin with `res://` and represent a path //! relative to the project directory. //! //! 3. **User paths** begin with `user://` and represent a path //! relative to some user-local storage directory, usually used for //! save files or the like. //! //! This module defines [`RPathBuf`], a [`PathBuf`]-like structure //! which represents the above file path formats. // An RPathBuf consists of a PathBuf together with a specifier use std::path::{PathBuf, Path, Components}; use std::convert::TryFrom; use std::str::FromStr; use std::fmt; use std::ffi::OsStr; /// An `RPathBuf` represents a file path using Godot's format. /// /// See the [module-level documentation](crate::runner::path) for more /// details. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RPathBuf { source: PathSrc, path: PathBuf, } /// An [`RPathBuf`] can refer to an absolute path, a resource path, or /// a user path. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum PathSrc { /// A user path, relative to some user-local storage directory. User, /// A resource path, relative to the project direction. Res, /// An absolute path, from the file system root directory. Absolute, } /// Errors which can occur when constructing an [`RPathBuf`] object. #[derive(Clone, Copy, Debug)] pub enum TryFromRPathBufError { /// A relative path was expected (based on the [`PathSrc`]) but an /// absolute one was provided. ExpectedRelative, /// An absolute path was expected (based on the [`PathSrc`]) but a /// relative one was provided. ExpectedAbsolute, } impl RPathBuf { /// Construct an `RPathBuf` from a source and a file path. /// /// Note that `path` should be a relative path if and only if /// [`source.should_be_relative_path()`](PathSrc::should_be_relative_path) /// is true. If this condition is violated, then an error will be /// returned from this function. pub fn new(source: PathSrc, path: PathBuf) -> Result { if source.should_be_relative_path() == path.is_relative() { Ok(RPathBuf { source, path }) } else if source.should_be_relative_path() { Err(TryFromRPathBufError::ExpectedRelative) } else { Err(TryFromRPathBufError::ExpectedAbsolute) } } /// Gets the path's source type. pub fn source(&self) -> PathSrc { self.source } /// Gets the file path. pub fn path(&self) -> &Path { &self.path } /// Gets the file path, mutably. /// /// Callers of this function are expected to maintain the /// constructor's precondition, namely that the path is relative if /// and only if [`PathSrc::should_be_relative_path`] is true. pub fn path_mut(&mut self) -> &mut PathBuf { &mut self.path } /// Consumes `self` and produce its file path as a [`PathBuf`]. pub fn into_path(self) -> PathBuf { self.path } /// Returns the path's components. Equivalent to /// `self.path().components()`. pub fn components(&self) -> Components<'_> { self.path().components() } /// Returns the path's extension. Equivalent to /// `self.path().extension()`. pub fn extension(&self) -> Option<&OsStr> { self.path().extension() } /// Returns the path's components, like [`RPathBuf::components`]. /// However, if the path is an absolute path, then the root `/` will /// be stripped from the components. pub fn components_no_root(&self) -> Components<'_> { let mut comp = self.components(); if self.path().has_root() { let _ignore_root = comp.next(); } comp } /// Return the path as a string. /// /// Forward slashes will always be used as the path delimiter, even /// on Windows. This is consistent with Godot's behavior. pub fn path_to_string

+ ?Sized>(path: &P) -> String { path.as_ref().to_string_lossy().replace('\\', "/") } } /// Displays the path as a string, using Godot's `user://` and /// `res://` syntax as appropriate. Absolute paths will be shown /// verbatim with no prefix. impl fmt::Display for RPathBuf { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let path = RPathBuf::path_to_string(self.path()); write!(f, "{}{}", self.source().prefix(), path) } } impl PathSrc { /// Returns whether a path with this source type should be a /// relative path. /// /// All source types except [`PathSrc::Absolute`] should refer to /// relative paths. pub fn should_be_relative_path(self) -> bool { self != PathSrc::Absolute } /// Returns the Godot-style prefix (`user://` or `res://`) for the /// path type. /// /// In the case of [`PathSrc::Absolute`], an empty string is /// returned, as no prefix is necessary in this case. pub fn prefix(self) -> &'static str { match self { PathSrc::User => "user://", PathSrc::Res => "res://", PathSrc::Absolute => "", } } } /// Converts a Godot-style import string (potentially beginning with /// `res://` or `user://`) into the appropriate type of [`RPathBuf`] /// object. impl TryFrom for RPathBuf { type Error = TryFromRPathBufError; fn try_from(path: String) -> Result { if let Some(s) = path.strip_prefix("res://") { RPathBuf::new(PathSrc::Res, PathBuf::from_str(s).unwrap()) // Infallible } else if let Some(s) = path.strip_prefix("user://") { RPathBuf::new(PathSrc::User, PathBuf::from_str(s).unwrap()) // Infallible } else { // Assume it's an absolute path RPathBuf::new(PathSrc::Absolute, PathBuf::from(path)) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_rpathbuf_new_relative() { assert!(RPathBuf::new(PathSrc::Res, PathBuf::from_str("a/b").unwrap()).is_ok()); assert!(RPathBuf::new(PathSrc::User, PathBuf::from_str("a/b").unwrap()).is_ok()); assert!(RPathBuf::new(PathSrc::Absolute, PathBuf::from_str("a/b").unwrap()).is_err()); } #[test] #[cfg(target_family = "windows")] fn test_rpathbuf_new_absolute_windows() { assert!(RPathBuf::new(PathSrc::Absolute, PathBuf::from_str("C:/a/b").unwrap()).is_ok()); assert!(RPathBuf::new(PathSrc::Res, PathBuf::from_str("C:/a/b").unwrap()).is_err()); assert!(RPathBuf::new(PathSrc::User, PathBuf::from_str("C:/a/b").unwrap()).is_err()); } #[test] #[cfg(target_family = "unix")] fn test_rpathbuf_new_absolute_unix() { assert!(RPathBuf::new(PathSrc::Absolute, PathBuf::from_str("/a/b").unwrap()).is_ok()); assert!(RPathBuf::new(PathSrc::Res, PathBuf::from_str("/a/b").unwrap()).is_err()); assert!(RPathBuf::new(PathSrc::User, PathBuf::from_str("/a/b").unwrap()).is_err()); } #[test] fn test_rpathbuf_parse() { let res = RPathBuf::try_from(String::from("res://foo/bar")); assert!(res.is_ok()); let res = res.unwrap(); assert_eq!(res.source(), PathSrc::Res); assert_eq!(res.path(), PathBuf::from_str("foo/bar").unwrap()); let user = RPathBuf::try_from(String::from("user://foo/bar")); assert!(user.is_ok()); let user = user.unwrap(); assert_eq!(user.source(), PathSrc::User); assert_eq!(user.path(), PathBuf::from_str("foo/bar").unwrap()); } #[test] #[cfg(target_family = "windows")] fn test_rpath_parse_windows() { let abs = RPathBuf::try_from(String::from("C:/foo/bar")); assert!(abs.is_ok()); let abs = abs.unwrap(); assert_eq!(abs.source(), PathSrc::Absolute); assert_eq!(abs.path(), PathBuf::from_str("C:/foo/bar").unwrap()); } #[test] #[cfg(target_family = "unix")] fn test_rpath_parse_unix() { let abs = RPathBuf::try_from(String::from("/foo/bar")); assert!(abs.is_ok()); let abs = abs.unwrap(); assert_eq!(abs.source(), PathSrc::Absolute); assert_eq!(abs.path(), PathBuf::from_str("/foo/bar").unwrap()); } #[test] fn test_rpathbuf_display() { assert_eq!(RPathBuf::try_from(String::from("res://foo/bar")).unwrap().to_string(), "res://foo/bar"); assert_eq!(RPathBuf::try_from(String::from("user://foo/bar")).unwrap().to_string(), "user://foo/bar"); } #[test] #[cfg(target_family = "windows")] fn test_rpathbuf_display_windows() { assert_eq!(RPathBuf::try_from(String::from("C:/foo/bar")).unwrap().to_string(), "C:/foo/bar"); } #[test] #[cfg(target_family = "unix")] fn test_rpathbuf_display_unix() { assert_eq!(RPathBuf::try_from(String::from("/foo/bar")).unwrap().to_string(), "/foo/bar"); } } ================================================ FILE: src/runner/version.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helper to get the Godot version. use std::process::{Command, Stdio}; use std::io; use std::collections::VecDeque; use std::str::FromStr; /// A version for a piece of software, in Semantic Versioning. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Version { pub major: i32, pub minor: i32, pub patch: i32, } /// Version information, together with modifiers applied after the /// base Semantic Versioning information. #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct VersionInfo { pub version: Version, pub modifiers: Vec, } impl Version { /// Constructs a version object representing a version of the /// software. pub fn new(major: i32, minor: i32, patch: i32) -> Self { Version { major, minor, patch } } /// Converts the version number into a single integer value which /// represents the version information. This is *not* a one-to-one /// mapping. It is intended to be a human-readable number /// representing the same version, which preserves the [`Version`] /// order for reasonable values. pub fn into_i32(self) -> i32 { self.major * 1000000 + self.minor * 10000 + self.patch * 100 } } impl VersionInfo { /// An empty version info object, corresponding to the version /// string `"0.0.0"`. pub fn new() -> Self { VersionInfo::default() } /// Given a Godot version string, parse it as a [`VersionInfo`] /// object. This method will not fail. Any missing fields will be /// initialized to an appropriate default (zero in the case of /// version numbers). pub fn parse(version_string: &str) -> VersionInfo { if version_string.is_empty() { // Corner case, since .split() will give us a single empty // string (rather than no values) in the case of an empty string // as input. return VersionInfo::default(); } let mut atoms: VecDeque<&str> = version_string.split('.').collect(); // Try to identify major, minor, patch version let major = pop_version_number(&mut atoms).unwrap_or(0); let minor = pop_version_number(&mut atoms).unwrap_or(0); let patch = pop_version_number(&mut atoms).unwrap_or(0); let version = Version { major, minor, patch }; VersionInfo { version, modifiers: atoms.into_iter().map(str::to_owned).collect() } } } /// Get the version of Godot that is present on the system path. /// /// The format of the resulting string is dependent on Godot and /// should generally be used as an informative message to the user, /// not compared to implement a feature check. /// /// In the case of IO error (including the situation where no /// executable named `godot` is on the system path), returns an /// appropriate error. pub fn get_godot_version_as_string() -> io::Result { let output = Command::new("godot") .arg("--version") .stderr(Stdio::null()) .stdout(Stdio::piped()) .output()?; let mut s = String::from_utf8_lossy(&output.stdout).into_owned(); s.pop(); // Remove newline from end Ok(s) } /// Get the version of Godot that is present on the system path, /// parsing it as a version string. pub fn get_godot_version() -> io::Result { let version_string = get_godot_version_as_string()?; Ok(VersionInfo::parse(&version_string)) } fn pop_version_number(atoms: &mut VecDeque<&str>) -> Option { // Only pop the first element if it can actually be parsed. let v = atoms.front().and_then(|atom| i32::from_str(atom).ok())?; atoms.pop_front(); Some(v) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_basic_version_string() { assert_eq!( VersionInfo::parse("1.2.3"), VersionInfo { version: Version::new(1, 2, 3), modifiers: vec!(), }, ); assert_eq!( VersionInfo::parse("1.2"), VersionInfo { version: Version::new(1, 2, 0), modifiers: vec!(), }, ); assert_eq!( VersionInfo::parse("1"), VersionInfo { version: Version::new(1, 0, 0), modifiers: vec!(), }, ); assert_eq!( VersionInfo::parse(""), VersionInfo { version: Version::new(0, 0, 0), modifiers: vec!(), }, ); } #[test] fn test_parse_nonsense_version_string() { // Parsing version strings does not fail. It may just produce // minimal information. assert_eq!( VersionInfo::parse("e"), VersionInfo { version: Version::new(0, 0, 0), modifiers: vec!(String::from("e")), }, ); } #[test] fn test_parse_full_version_string() { assert_eq!( VersionInfo::parse("10.9.0.a.b.foo"), VersionInfo { version: Version::new(10, 9, 0), modifiers: vec!(String::from("a"), String::from("b"), String::from("foo")), }, ); assert_eq!( VersionInfo::parse("10.9.1.a.b.foo"), VersionInfo { version: Version::new(10, 9, 1), modifiers: vec!(String::from("a"), String::from("b"), String::from("foo")), }, ); } #[test] fn test_parse_partial_version_string() { assert_eq!( VersionInfo::parse("10.9.a.b.foo"), VersionInfo { version: Version::new(10, 9, 0), modifiers: vec!(String::from("a"), String::from("b"), String::from("foo")), }, ); assert_eq!( VersionInfo::parse("10.a.b.foo"), VersionInfo { version: Version::new(10, 0, 0), modifiers: vec!(String::from("a"), String::from("b"), String::from("foo")), }, ); } } ================================================ FILE: src/sxp/ast.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the basic [`AST`] type. use crate::pipeline::source::{SourceOffset, Sourced}; use crate::util::extract_err; use crate::util::recursive::Recursive; use super::literal::Literal; use std::fmt; use std::convert::Infallible; use std::cmp::max; /// The basic type used for representing Lisp S-expressions. #[derive(PartialEq, Eq, Hash, Clone, Debug)] pub enum ASTF { /// An atomic AST that does not contain any further AST values /// inside of it. Atom(Literal), /// A pair of values. The first value is referred to as the car and /// the second as the cdr. All *proper* Lisp lists are made up of /// cons cells and [`Literal::Nil`]. Displays as `(car . cdr)`. Cons(Box, Box), } /// An `AST` is an [`ASTF`] together with information about the offset /// in the source code of the S-expression. #[derive(PartialEq, Eq, Hash, Clone, Debug)] pub struct AST { pub value: ASTF, pub pos: SourceOffset, } fn fmt_list(a: &AST, b: &AST, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &b.value { ASTF::Atom(Literal::Nil) => // End of list; just print the known value write!(f, "{}", a), ASTF::Cons(b1, c1) => { // Another cons cell in cdr; continue printing list write!(f, "{} ", a)?; fmt_list(b1, c1, f) }, _ => // Dotted list; print with dot write!(f, "{} . {}", a, b) } } impl ASTF { /// The literal nil AST value. Up to equality, there is only one /// such value. /// /// Note that, in some Lisps, the nil value is considered to be a /// symbol. In GDLisp, this is *not* the case: the nil value is a /// special value distinct from all symbols. pub const NIL: ASTF = ASTF::Atom(Literal::Nil); /// An [`ASTF::Cons`] cell. This is more convenient than calling the /// constructor directly, as you needn't explicitly box the values. pub fn cons(car: AST, cdr: AST) -> ASTF { ASTF::Cons(Box::new(car), Box::new(cdr)) } /// A [`Literal::String`]. Clones the string argument into a new /// [`ASTF`] value. pub fn string(s: S) -> ASTF where String : From { ASTF::Atom(Literal::String(String::from(s))) } /// A [`Literal::Symbol`]. Clones the string argument into a new /// [`ASTF`] value. pub fn symbol(s: S) -> ASTF where String : From { ASTF::Atom(Literal::Symbol(String::from(s))) } /// A literal integer, as an [`ASTF`]. pub fn int(value: i32) -> ASTF { ASTF::Atom(Literal::Int(value)) } /// A literal float, as an [`ASTF`]. pub fn float(value: f32) -> ASTF { ASTF::Atom(Literal::Float(value.into())) } } impl AST { /// A new `AST` with the given value and position. Equivalent to /// `AST { value, pos }`. pub fn new(value: ASTF, pos: SourceOffset) -> AST { AST { value, pos } } /// A [`Literal::Nil`] wrapped in `AST` with the given source /// offset. Equivalent to `AST::new(ASTF::Nil, pos)`. pub fn nil(pos: SourceOffset) -> AST { AST::new(ASTF::Atom(Literal::Nil), pos) } /// A [`Literal::Symbol`] with the given value. pub fn symbol(name: S, pos: SourceOffset) -> AST where String : From { AST::new(ASTF::symbol(name), pos) } /// A [`Literal::String`] with the given value. pub fn string(name: S, pos: SourceOffset) -> AST where String : From { AST::new(ASTF::string(name), pos) } /// A [`Literal::Int`] with the given value. pub fn int(value: i32, pos: SourceOffset) -> AST { AST::new(ASTF::int(value), pos) } /// A [`Literal::Float`] with the given value. pub fn float(value: f32, pos: SourceOffset) -> AST { AST::new(ASTF::float(value), pos) } /// An [`ASTF::Cons`] with the given value. pub fn cons(car: AST, cdr: AST, pos: SourceOffset) -> AST { AST::new(ASTF::cons(car, cdr), pos) } fn _recurse<'a, 'b, F1, F2, E>(&'a self, func: &mut F1, default: &mut F2) -> Result<(), E> where F1 : FnMut(&'b AST) -> Result<(), E>, F2 : FnMut() -> Result<(), E>, 'a : 'b { match &self.value { ASTF::Cons(car, cdr) => { func(car)?; func(cdr)?; } ASTF::Atom(_) => { default()?; } } Ok(()) } fn _recurse_mut<'a, 'b, F1, F2, E>(&'a mut self, func: &mut F1, default: &mut F2) -> Result<(), E> where F1 : FnMut(&'b mut AST) -> Result<(), E>, F2 : FnMut() -> Result<(), E>, 'a : 'b { match &mut self.value { ASTF::Cons(car, cdr) => { func(&mut *car)?; func(&mut *cdr)?; } ASTF::Atom(_) => { default()?; } } Ok(()) } fn _walk_preorder<'a, 'b, F, E>(&'a self, func: &mut F) -> Result<(), E> where F: FnMut(&'b AST) -> Result<(), E>, 'a: 'b { func(self)?; self._recurse(&mut |x| x._walk_preorder(func), &mut || Ok(())) } fn _walk_preorder_mut<'a, F, E>(&'a mut self, func: &mut F) -> Result<(), E> where F: for<'b> FnMut(&'b mut AST) -> Result<(), E> { func(self)?; self._recurse_mut(&mut |x| x._walk_preorder_mut(func), &mut || Ok(())) } fn _walk_postorder<'a, 'b, F, E>(&'a self, func: &mut F) -> Result<(), E> where F: FnMut(&'b AST) -> Result<(), E>, 'a: 'b { self._recurse(&mut |x| x._walk_postorder(func), &mut || Ok(()))?; func(self) } fn _walk_postorder_mut<'a, F, E>(&'a mut self, func: &mut F) -> Result<(), E> where F: for<'b> FnMut(&'b mut AST) -> Result<(), E> { self._recurse_mut(&mut |x| x._walk_postorder_mut(func), &mut || Ok(()))?; func(self) } /// Walk the `AST`, calling a function on the node itself and every /// child recursively. That includes both elements of an /// [`ASTF::Cons`] recursively. The function will be called on the /// current node *before* recursing on its children. /// /// Any error that occurs during walking will be propagated to the /// caller. pub fn walk_preorder<'a, 'b, F, E>(&'a self, mut func: F) -> Result<(), E> where F: FnMut(&'b AST) -> Result<(), E>, 'a: 'b { self._walk_preorder(&mut func) } /// As [`AST::walk_preorder`], but with a mutable `self`. pub fn walk_preorder_mut<'a, F, E>(&'a mut self, mut func: F) -> Result<(), E> where F: for<'b> FnMut(&'b mut AST) -> Result<(), E> { self._walk_preorder_mut(&mut func) } /// Walk the `AST`, calling a function on the node itself and every /// child recursively. That includes both elements of an /// [`ASTF::Cons`] recursively. The function will be called on the /// current node only *after* recursing on its children. /// /// Any error that occurs during walking will be propagated to the /// caller. pub fn walk_postorder<'a, 'b, F, E>(&'a self, mut func: F) -> Result<(), E> where F: FnMut(&'b AST) -> Result<(), E>, 'a: 'b { self._walk_postorder(&mut func) } /// As [`AST::walk_postorder`], but with a mutable `self`. pub fn walk_postorder_mut<'a, F, E>(&'a mut self, mut func: F) -> Result<(), E> where F: for<'b> FnMut(&'b mut AST) -> Result<(), E> { self._walk_postorder_mut(&mut func) } /// Walk the `AST`, transforming all of the [`SourceOffset`] tags /// using the given function. The walk is performed using /// [`AST::walk_preorder_mut`]. pub fn each_source_mut(&mut self, mut func: F) where F: FnMut(SourceOffset) -> SourceOffset { let result = self.walk_preorder_mut(|ast| { ast.pos = func(ast.pos); Ok(()) }); extract_err(result) } /// As [`AST::each_source_mut`], but returns a new object. pub fn each_source(&self, func: F) -> AST where F: FnMut(SourceOffset) -> SourceOffset { let mut ast = self.clone(); ast.each_source_mut(func); ast } /// Walk the `AST`, producing a list of all symbols that appear (as /// [`Literal::Symbol`]) anywhere in the tree. The symbols will /// appear in the resulting list in the order they appear in the /// `AST`, and any duplicates will be represented multiple times, /// once for each appearance. pub fn all_symbols<'a>(&'a self) -> Vec<&'a str> { let mut result: Vec<&'a str> = Vec::new(); let err = self.walk_preorder::<_, Infallible>(|x| { if let ASTF::Atom(Literal::Symbol(x)) = &x.value { result.push(x); } Ok(()) }); extract_err(err); result } /// In Lisp, we generally think of a *dotted list* as a sequence of /// zero or more cons cells, where the cdr of each cell is the next /// cons cell, eventually terminated by some non-cons value. For /// instance, `(1 . (2 . (3 . 4)))` would be a dotted list where the /// values in the "list" portion are `1`, `2`, and `3`, and the /// terminator is `4`. /// /// We call a dotted list which terminates in `()` (i.e. /// [`Literal::Nil`]) a *proper list*. Some sources explicitly /// define a dotted list to *not* be a proper list, but this /// documentation does not make that distinction. /// /// This function constructs an [`ASTF`] value from a sequence of /// values `vec` and a terminator `terminal`. For each value in the /// sequence, a [`ASTF::Cons`] cell will be constructed, and the /// final cdr will be `terminal`. /// /// For the inverse operation of converted an [`ASTF`] *back* into a /// sequence and terminator, see [`super::dotted::DottedExpr`]. pub fn dotted_list(vec: Vec, terminal: AST) -> AST { vec.into_iter().rev().fold(terminal, AST::dotted_list_fold) // NOTE: Arguments reversed } fn dotted_list_fold(cdr: AST, car: AST) -> AST { // NOTE: Arguments reversed from the usual order let pos = car.pos; AST::new(ASTF::cons(car, cdr), pos) } /// A dotted list terminated by nil at the given source position. pub fn list(vec: Vec, nil_pos: SourceOffset) -> AST { AST::dotted_list(vec, AST::nil(nil_pos)) } /// Uses a [`From`] instance of [`ASTF`] to construct an `AST`. pub fn from_value(value: T, pos: SourceOffset) -> AST where ASTF : From { AST::new(ASTF::from(value), pos) } /// If `self` stores a [`Literal::Symbol`], then a reference to the /// inside of that symbol is returned. Otherwise, `None` is /// returned. pub fn as_symbol_ref(&self) -> Option<&str> { if let ASTF::Atom(Literal::Symbol(s)) = &self.value { Some(s) } else { None } } } /// Pretty-print an AST, using a format compatible with [`parser`](crate::parser). /// Cons cells whose cdr is a cons cell will be pretty-printed as list /// prefixes. impl fmt::Display for AST { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.value { ASTF::Atom(lit) => write!(f, "{}", lit), ASTF::Cons(a, b) => { write!(f, "(")?; fmt_list(a, b, f)?; write!(f, ")") } } } } impl Sourced for AST { type Item = ASTF; fn get_source(&self) -> SourceOffset { self.pos } fn get_value(&self) -> &ASTF { &self.value } } impl Recursive for AST { fn depth(&self) -> u32 { match &self.value { ASTF::Atom(_) => 1, ASTF::Cons(a, b) => 1 + max(a.depth(), b.depth()), } } } impl From<()> for ASTF { fn from(_: ()) -> ASTF { ASTF::Atom(Literal::Nil) } } impl From for ASTF { fn from(n: i32) -> ASTF { ASTF::Atom(Literal::from(n)) } } impl From for ASTF { fn from(b: bool) -> ASTF { ASTF::Atom(Literal::from(b)) } } impl From for ASTF { fn from(f: f32) -> ASTF { ASTF::Atom(Literal::from(f)) } } impl From for ASTF { fn from(s: String) -> ASTF { ASTF::Atom(Literal::String(s)) } } impl<'a> From<&'a str> for ASTF { fn from(s: &'a str) -> ASTF { ASTF::Atom(Literal::String(String::from(s))) } } #[cfg(test)] mod tests { use super::*; use std::string::ToString; // A handful of helpers for the tests that don't care about // SourceOffset and are only testing the structure. These just fill // in SourceOffset::default() wherever necessary. fn int(n: i32) -> AST { AST::new(ASTF::int(n), SourceOffset::default()) } fn nil() -> AST { AST::nil(SourceOffset::default()) } fn cons(a: AST, b: AST) -> AST { AST::new(ASTF::cons(a, b), SourceOffset::default()) } #[test] fn runtime_repr_numerical() { assert_eq!(int(150).to_string(), 150.to_string()); assert_eq!(int(-99).to_string(), (-99).to_string()); assert_eq!(AST::new(ASTF::float(0.83), SourceOffset::default()).to_string(), (0.83).to_string()); assert_eq!(AST::new(ASTF::float(-1.2), SourceOffset::default()).to_string(), (-1.2).to_string()); } #[test] fn runtime_repr_nil() { assert_eq!(AST::new(ASTF::Atom(Literal::Nil), SourceOffset::default()).to_string(), "()"); } #[test] fn runtime_repr_string() { assert_eq!(AST::string("abc", SourceOffset::default()).to_string(), r#""abc""#); assert_eq!(AST::string("abc\"d", SourceOffset::default()).to_string(), r#""abc\"d""#); assert_eq!(AST::string("\\foo\"bar\\", SourceOffset::default()).to_string(), r#""\\foo\"bar\\""#); } #[test] fn runtime_repr_symbol() { assert_eq!(AST::symbol("foo", SourceOffset::default()).to_string(), "foo"); assert_eq!(AST::symbol("bar", SourceOffset::default()).to_string(), "bar"); } #[test] fn runtime_repr_cons() { assert_eq!(cons(int(1), int(2)).to_string(), "(1 . 2)"); assert_eq!(cons(int(1), cons(int(2), int(3))).to_string(), "(1 2 . 3)"); assert_eq!(cons(int(1), cons(int(2), cons(int(3), nil()))).to_string(), "(1 2 3)"); } #[test] fn runtime_repr_list() { assert_eq!(AST::dotted_list(vec!(int(1), int(2), int(3)), nil()).to_string(), "(1 2 3)"); assert_eq!(AST::dotted_list(vec!(int(1), int(2), int(3)), int(4)).to_string(), "(1 2 3 . 4)"); } #[test] fn get_all_symbols() { assert_eq!(nil().all_symbols(), Vec::<&str>::new()); assert_eq!(int(3).all_symbols(), Vec::<&str>::new()); assert_eq!(AST::symbol("abc", SourceOffset::default()).all_symbols(), vec!("abc")); let foo = AST::symbol("foo", SourceOffset::default()); let bar = AST::symbol("bar", SourceOffset::default()); assert_eq!(cons(foo.clone(), bar.clone()).all_symbols(), vec!("foo", "bar")); assert_eq!(AST::dotted_list(vec!(foo.clone(), bar.clone()), nil()).all_symbols(), vec!("foo", "bar")); } #[test] fn each_source() { let example1 = AST::symbol("foo", SourceOffset(3)); let example2 = AST::symbol("foo", SourceOffset(13)); assert_eq!(example1.each_source(add10), example2); } fn add10(x: SourceOffset) -> SourceOffset { (usize::from(x) + 10).into() } #[test] fn test_depth_atomic() { assert_eq!(int(3).depth(), 1); assert_eq!(nil().depth(), 1); assert_eq!(AST::symbol("my-symbol", SourceOffset(0)).depth(), 1); assert_eq!(AST::string("my-string", SourceOffset(0)).depth(), 1); assert_eq!(AST::float(0.0, SourceOffset(0)).depth(), 1); } #[test] fn test_depth_nested() { assert_eq!(cons(int(1), int(2)).depth(), 2); assert_eq!(cons(int(1), cons(int(2), int(3))).depth(), 3); assert_eq!(cons(cons(int(1), int(2)), cons(int(3), int(4))).depth(), 3); assert_eq!(cons(cons(int(1), int(2)), int(3)).depth(), 3); assert_eq!(cons(cons(int(1), int(2)), cons(int(3), nil())).depth(), 3); } } ================================================ FILE: src/sxp/dotted.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Parsing of [`AST`](super::ast::AST) values as dotted S-expressions. //! //! This module provides the inverse operation to //! [`AST::dotted_list`](super::ast::AST::dotted_list). That function //! takes a vector and a terminator and produces an `AST` value, while //! [`DottedExpr::new`] takes an `AST` value and interprets it as a //! vector and a terminator. use super::ast::{AST, ASTF}; use crate::pipeline::source::SourceOffset; use std::convert::TryFrom; /// A dotted list expression consists of a sequence of elements and a /// final terminator. /// /// Note that a `DottedExpr` does not *own* any data itself. Instead, /// it acts as a view on an existing [`AST`](super::ast::AST) owned by /// someone else. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct DottedExpr<'a> { /// The sequence of elements leading up to the terminator. pub elements: Vec<&'a AST>, /// The terminator element. pub terminal: &'a AST, } /// The type of errors produced by [`TryFrom::::try_from`]. #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct TryFromDottedExprError { pub pos: SourceOffset, } fn accumulate_ast<'a>(vec: &mut Vec<&'a AST>, ast: &'a AST) -> &'a AST { match &ast.value { ASTF::Cons(car, cdr) => { vec.push(car); accumulate_ast(vec, cdr) } _ => ast } } impl<'a> DottedExpr<'a> { /// Given an `AST`, construct a `DottedExpr` view representing it. /// /// A dotted list is, here, defined to be a collection of zero or /// more cons cells, linked together by their cdr, and terminated by /// an arbitrary element. Note that, under this definition, *every* /// `AST` can be meaningfully interpreted as a dotted list, which is /// why this function cannot fail. /// /// If `ast` is not a cons cell, then it is a dotted list of zero /// elements where the terminator is `ast`. If `ast` is a cons cell, /// then the first element of the dotted list is its car, and the /// rest of the dotted list, as well as its terminator, is /// calculated recursively on the cdr of `ast`. pub fn new(ast: &'a AST) -> DottedExpr<'a> { let mut elements = Vec::new(); let terminal = accumulate_ast(&mut elements, ast); DottedExpr { elements, terminal } } } /// A dotted list whose terminator is /// [`Literal::Nil`](super::literal::Literal::Nil) is called a proper /// list. It is reasonable to interpret a proper list as a simple /// vector, since the nil terminator is merely a placeholder for the /// end of the list. [`TryFrom::::try_from`] performs this /// conversion, returning [`DottedExpr::elements`] if /// [`DottedExpr::terminal`] is nil and producing an error otherwise. impl<'a> TryFrom> for Vec<&'a AST> { type Error = TryFromDottedExprError; // TODO This should really be an inherent method on DottedExpr, // rather than relying on type inference and TryFrom to figure out // what we really mean. fn try_from(expr: DottedExpr<'a>) -> Result, TryFromDottedExprError> { if expr.terminal.value == ASTF::NIL { Ok(expr.elements) } else { Err(TryFromDottedExprError { pos: expr.terminal.pos }) } } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; fn int(n: i32) -> AST { AST::new(ASTF::int(n), SourceOffset::default()) } fn nil() -> AST { AST::nil(SourceOffset::default()) } fn list(data: Vec) -> AST { AST::dotted_list(data, nil()) } #[test] fn simple_dot() { let ast = int(33); let dot = DottedExpr::new(&ast); assert!(dot.elements.is_empty()); assert_eq!(*dot.terminal, ast); assert!(Vec::try_from(dot).is_err()); } #[test] fn proper_list() { let vec = vec!(int(1), int(2), int(3)); let ast = list(vec.clone()); let dot = DottedExpr::new(&ast); assert_eq!(dot.elements, vec.iter().collect::>()); assert_eq!(*dot.terminal, nil()); let vec1 = Vec::try_from(dot); assert_eq!(vec1, Ok(vec.iter().collect())); } #[test] fn improper_list() { let vec = vec!(int(1), int(2), int(3)); let end = int(4); let ast = AST::dotted_list(vec.clone(), end.clone()); let dot = DottedExpr::new(&ast); assert_eq!(dot.elements, vec.iter().collect::>()); assert_eq!(*dot.terminal, end); assert!(Vec::try_from(dot).is_err()); } } ================================================ FILE: src/sxp/literal.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Literal values at the abstract syntax tree level. use super::string::insert_escapes; use ordered_float::OrderedFloat; use std::fmt; /// A GDLisp literal AST. A literal is an AST that is not recursive, /// i.e. does not contain any more ASTs inside of it. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Literal { /// A nil value, or `()`. This is comparable to `null` in some /// languages but also functions as the empty list. Nil, /// A literal 32-bit integer value. Int(i32), /// A literal floating-point value. For AST purposes, we do not use /// standard IEEE comparison semantics and instead use /// [`OrderedFloat`], whose ordering and equality relations satisfy /// convenient abstract mathematical properties. Float(OrderedFloat), /// A literal string. String(String), /// A literal symbol. Symbol(String), /// A literal Boolean value. Bool(bool), } /// Pretty-print the atomic AST value, using a format compatible with /// [`parser`](crate::parser). impl fmt::Display for Literal { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Literal::Nil => write!(f, "()"), Literal::Int(n) => write!(f, "{}", n), Literal::Bool(true) => write!(f, "#t"), Literal::Bool(false) => write!(f, "#f"), Literal::Float(x) => write!(f, "{}", x), Literal::String(s) => write!(f, "\"{}\"", insert_escapes(s)), Literal::Symbol(s) => write!(f, "{}", s), } } } impl From for Literal { fn from(value: i32) -> Literal { Literal::Int(value) } } impl From for Literal { fn from(value: f32) -> Literal { Literal::Float(value.into()) } } impl From> for Literal { fn from(value: OrderedFloat) -> Literal { Literal::Float(value) } } impl From for Literal { fn from(value: String) -> Literal { Literal::String(value) } } impl<'a> From<&'a str> for Literal { fn from(value: &'a str) -> Literal { Literal::String(String::from(value)) } } impl From for Literal { fn from(value: bool) -> Literal { Literal::Bool(value) } } ================================================ FILE: src/sxp/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Data-type and associated functionality for dealing with Lisp //! S-expressions. pub mod ast; pub mod dotted; pub mod literal; pub mod reify; pub mod string; pub mod syntax; ================================================ FILE: src/sxp/reify/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`Reify`] trait. pub mod pretty; use crate::gdscript::expr::Expr; use crate::compile::names::fresh::FreshNameGenerator; use super::ast::AST; /// This trait describes any type for which there is a reasonable way /// to convert a `&self` into [`crate::gdscript::expr::Expr`]. Note /// that this is subtly different from [`Into`], which requires /// ownership of `self` to do the conversion. The resulting `Expr` /// should not share any data with `&self`. pub trait Reify { /// Reify the value into a GDScript expression would should evaluate /// to something comparable to `self`. fn reify(&self) -> Expr; } impl Reify for AST { fn reify(&self) -> Expr { let mut gen = FreshNameGenerator::new(vec!()); let (stmts, result) = pretty::reify_pretty_expr(self, u32::MAX, &mut gen); assert!(stmts.is_empty()); // At u32::MAX, we should never have need to generate any helper statements. result } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; use crate::sxp::ast::ASTF; fn int(n: i32) -> AST { AST::new(ASTF::int(n), SourceOffset::default()) } fn nil() -> AST { AST::nil(SourceOffset::default()) } #[allow(dead_code)] fn list(data: Vec) -> AST { AST::dotted_list(data, nil()) } fn cons(a: AST, b: AST) -> AST { AST::new(ASTF::cons(a, b), SourceOffset::default()) } #[test] fn reify_test() { assert_eq!(nil().reify().to_gd(), "null"); assert_eq!(cons(int(1), int(2)).reify().to_gd(), "GDLisp.cons(1, 2)"); assert_eq!(AST::new(ASTF::from(false), SourceOffset::default()).reify().to_gd(), "false"); assert_eq!(AST::new(ASTF::from(true), SourceOffset::default()).reify().to_gd(), "true"); assert_eq!(AST::symbol("foo", SourceOffset::default()).reify().to_gd(), "GDLisp.intern(\"foo\")"); } } ================================================ FILE: src/sxp/reify/pretty.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides pretty-printing facilities for //! [`AST`](crate::sxp::ast::AST) nodes. use crate::gdscript::expr::Expr; use crate::gdscript::stmt::Stmt; use crate::gdscript::library; use crate::compile::names::generator::NameGenerator; use crate::compile::stmt_wrapper::StmtWrapper; use crate::pipeline::source::{Sourced, SourceOffset}; use crate::sxp::ast::{AST, ASTF}; use crate::sxp::literal::Literal; /// Reifies the expression, as though through /// [`Reify::reify`](super::Reify::reify). However, whereas /// `Reify::reify` will generate a single expression, this function /// will split the reification into several lines, using local /// variables to break the code up as needed. /// /// `value` is the value to be reified. `max_depth` is the maximum /// depth of a single expression. After recursing deeper than this, a /// local variable will be constructed in GDScript to store the /// intermediate. A `max_depth` of zero will create a local variable /// at every recursion step. `gen` is used to generate the names for /// the reification helper variables. /// /// This function returns a vector of intermediate variable /// declaration statements, as well as the final expression. The /// expression should be used in GDScript only after all of the /// statements have been run. If you know how the expression will be /// used, then [`reify_pretty`] may be more convenient. pub fn reify_pretty_expr(value: &AST, max_depth: u32, gen: &mut impl NameGenerator) -> (Vec, Expr) { let mut printer = PrettyPrinter::new(gen, max_depth); let expr = printer.reify_pretty_rec(value, 0); (printer.build(), expr) } /// As [`reify_pretty_expr`], but the final expression is wrapped /// using the statement wrapper `wrapper` and appended to the /// statements vector. pub fn reify_pretty(value: &AST, max_depth: u32, gen: &mut impl NameGenerator, wrapper: &W) -> Vec where W : StmtWrapper + ?Sized { let (mut stmts, expr) = reify_pretty_expr(value, max_depth, gen); stmts.push(wrapper.wrap_expr(expr)); stmts } struct PrettyPrinter<'a, G: NameGenerator> { gen: &'a mut G, builder: Vec, max_depth: u32, } impl<'a, G: NameGenerator> PrettyPrinter<'a, G> { fn new(gen: &'a mut G, max_depth: u32) -> Self { PrettyPrinter { gen, max_depth, builder: Vec::new() } } fn reify_pretty_rec(&mut self, value: &AST, depth: u32) -> Expr { if depth > self.max_depth { // Make a local variable and store the result in it. let inner = self.reify_pretty_rec(value, 0); let name = self.gen.generate_with("_reify"); self.builder.push(Stmt::var_decl(name.clone(), inner, value.get_source())); Expr::var(&name, value.get_source()) } else { match &value.value { ASTF::Atom(lit) => { reify_literal(lit, value.pos) } ASTF::Cons(a, b) => { let a = self.reify_pretty_rec(a, depth + 1); let b = self.reify_pretty_rec(b, depth + 1); Expr::call(Some(library::gdlisp_root(value.pos)), "cons", vec!(a, b), value.pos) } } } } fn build(self) -> Vec { self.builder } } fn reify_literal(value: &Literal, pos: SourceOffset) -> Expr { match value { Literal::Nil => { Expr::null(pos) } Literal::Int(n) => { Expr::from_value(*n, pos) } Literal::Bool(b) => { Expr::from_value(*b, pos) } Literal::Float(f) => { Expr::from_value(*f, pos) } Literal::String(s) => { Expr::from_value(s.to_owned(), pos) } Literal::Symbol(s) => { let s = Expr::from_value(s.to_owned(), pos); Expr::call(Some(library::gdlisp_root(pos)), "intern", vec!(s), pos) } } } #[cfg(test)] mod tests { use super::*; use crate::pipeline::source::SourceOffset; use crate::compile::names::fresh::FreshNameGenerator; fn cons(a: AST, b: AST) -> AST { AST::new(ASTF::cons(a, b), SourceOffset::default()) } fn int(n: i32) -> AST { AST::new(ASTF::int(n), SourceOffset::default()) } #[test] fn reify_pretty_test_1() { let (stmts, expr) = reify_pretty_expr(&cons(int(1), int(2)), u32::MAX, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert!(stmts.is_empty()); assert_eq!(expr.to_gd(), "GDLisp.cons(1, 2)"); } #[test] fn reify_pretty_test_2() { let (stmts, expr) = reify_pretty_expr(&cons(cons(cons(cons(int(1), int(2)), int(3)), int(4)), int(5)), u32::MAX, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert!(stmts.is_empty()); assert_eq!(expr.to_gd(), "GDLisp.cons(GDLisp.cons(GDLisp.cons(GDLisp.cons(1, 2), 3), 4), 5)"); } #[test] fn reify_pretty_test_3() { let ast = cons(cons(cons(cons(int(1), int(2)), int(3)), int(4)), cons(int(5), int(6))); let (stmts, expr) = reify_pretty_expr(&ast, u32::MAX, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert!(stmts.is_empty()); assert_eq!(expr.to_gd(), "GDLisp.cons(GDLisp.cons(GDLisp.cons(GDLisp.cons(1, 2), 3), 4), GDLisp.cons(5, 6))"); } #[test] fn reify_pretty_test_4() { let ast = cons(cons(cons(cons(int(1), int(2)), int(3)), int(4)), cons(int(5), int(6))); let (stmts, expr) = reify_pretty_expr(&ast, 3, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert_eq!(stmts, vec!("var _reify_0 = 1\n", "var _reify_1 = 2\n")); assert_eq!(expr.to_gd(), "GDLisp.cons(GDLisp.cons(GDLisp.cons(GDLisp.cons(_reify_0, _reify_1), 3), 4), GDLisp.cons(5, 6))"); } #[test] fn reify_pretty_test_5() { let ast = cons(cons(cons(cons(int(1), int(2)), int(3)), int(4)), cons(int(5), int(6))); let (stmts, expr) = reify_pretty_expr(&ast, 2, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert_eq!(stmts, vec!("var _reify_0 = GDLisp.cons(1, 2)\n", "var _reify_1 = 3\n")); assert_eq!(expr.to_gd(), "GDLisp.cons(GDLisp.cons(GDLisp.cons(_reify_0, _reify_1), 4), GDLisp.cons(5, 6))"); } #[test] fn reify_pretty_test_6() { let ast = cons(cons(cons(cons(int(1), int(2)), int(3)), int(4)), cons(int(5), int(6))); let (stmts, expr) = reify_pretty_expr(&ast, 1, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert_eq!(stmts, vec!( "var _reify_0 = 1\n", "var _reify_1 = 2\n", "var _reify_2 = GDLisp.cons(GDLisp.cons(_reify_0, _reify_1), 3)\n", "var _reify_3 = 4\n", "var _reify_4 = 5\n", "var _reify_5 = 6\n", )); assert_eq!(expr.to_gd(), "GDLisp.cons(GDLisp.cons(_reify_2, _reify_3), GDLisp.cons(_reify_4, _reify_5))"); } #[test] fn reify_pretty_test_7() { let ast = cons(cons(cons(cons(int(1), int(2)), int(3)), int(4)), cons(int(5), int(6))); let (stmts, expr) = reify_pretty_expr(&ast, 0, &mut FreshNameGenerator::new(vec!())); let stmts: Vec<_> = stmts.into_iter().map(|s| s.to_gd(0)).collect(); assert_eq!(stmts, vec!( "var _reify_0 = 1\n", "var _reify_1 = 2\n", "var _reify_2 = GDLisp.cons(_reify_0, _reify_1)\n", "var _reify_3 = 3\n", "var _reify_4 = GDLisp.cons(_reify_2, _reify_3)\n", "var _reify_5 = 4\n", "var _reify_6 = GDLisp.cons(_reify_4, _reify_5)\n", "var _reify_7 = 5\n", "var _reify_8 = 6\n", "var _reify_9 = GDLisp.cons(_reify_7, _reify_8)\n", )); assert_eq!(expr.to_gd(), "GDLisp.cons(_reify_6, _reify_9)"); } } ================================================ FILE: src/sxp/string.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Helper functions for working with strings within //! [`AST`](super::ast::AST) values. use phf::{phf_map}; use std::collections::HashMap; use std::fmt::Write; // Note: \u{...} is a special case and is not listed here. const ESCAPE_SEQUENCES: phf::Map = phf_map! { 'n' => '\n', 't' => '\t', 'r' => '\r', 'a' => '\x07', 'b' => '\x08', 'f' => '\x0C', 'v' => '\x0B', '"' => '"', '\'' => '\'', '\\' => '\\', }; /// The type of errors produced by [`parse_escapes`]. #[derive(Debug)] pub enum Error { /// An escape sequence `\` was not terminated. UnfinishedEscape, /// An escape sequence `\` was not recognized. InvalidEscape(char), /// A Unicode escape sequence `\u` was invalid. InvalidUnicodeEscape, } /// Given a string, parse escape sequences beginning with `\` and /// produce a string containing the result. /// /// Note that our `\u{...}` syntax is distinct from (but compatible /// with) GDScript's. GDLisp supports two different Unicode escapes. /// The first, identical to GDScript, consists of `\u` followed by /// exactly four hexadecimal digits. This is capable of supporting any /// character in the basic multilingual plane. The second consists of /// `\u` followed by one or more hexadecimal digits contained in /// mandatory curly braces. This is capable of representing *any* /// Unicode character. /// /// # Examples /// /// ``` /// # use gdlisp::sxp::string::parse_escapes; /// assert_eq!(parse_escapes(r#"foobar"#).unwrap(), "foobar"); /// assert_eq!(parse_escapes(r#"foo\"\"bar\\"#).unwrap(), "foo\"\"bar\\"); /// assert_eq!(parse_escapes(r#"\u03B1\u03b1\u{3b1}\u{3B1}\u{000000003b1}"#).unwrap(), "ααααα"); /// ``` pub fn parse_escapes(input: &str) -> Result { let length = input.chars().count(); let mut result = String::with_capacity(length); let mut iter = input.chars(); while let Some(ch) = iter.next() { if ch == '\\' { match iter.next() { Some('u') => { parse_unicode_sequence(&mut iter, &mut result)?; } Some(esc) => { if let Some(value_char) = ESCAPE_SEQUENCES.get(&esc) { result.push(*value_char); } else { return Err(Error::InvalidEscape(esc)); } } None => { return Err(Error::UnfinishedEscape); } } } else { result.push(ch); } } Ok(result) } /// Parses a Unicode escape sequence. It is assumed that this function /// will be called immediately *after* parsing `\u` in the iterator. /// This function can parse either four hexadecimal digits or a /// brace-enclosed collection of at least one hexadecimal character. /// /// In either case, the hexadecimal digits are case insensitive. fn parse_unicode_sequence(iter: &mut impl Iterator, dest: &mut impl Write) -> Result<(), Error> { let digits = match iter.next() { Some(first) if is_hex_digit(first) => { // A sequence of exactly four hexadecimal digits. let mut digits: String = String::with_capacity(4); digits.push(first); for _ in 0..3 { let next_char = iter.next().ok_or(Error::InvalidUnicodeEscape)?; if !is_hex_digit(next_char) { return Err(Error::InvalidUnicodeEscape); } digits.push(next_char); } digits } Some('{') => { let mut digits: String = String::with_capacity(8); loop { let next_char = iter.next().ok_or(Error::InvalidUnicodeEscape)?; if next_char == '}' { break; } else if !is_hex_digit(next_char) { return Err(Error::InvalidUnicodeEscape); } digits.push(next_char); } if digits.is_empty() { return Err(Error::InvalidUnicodeEscape); } digits } _ => { return Err(Error::InvalidUnicodeEscape); } }; let codepoint = u32::from_str_radix(&digits, 16).expect("Internal error in parse_unicode_sequence"); let ch = char::from_u32(codepoint).ok_or(Error::InvalidUnicodeEscape)?; write!(dest, "{}", ch).expect("Internal error in parse_unicode_sequence"); Ok(()) } fn is_hex_digit(digit: char) -> bool { ('0'..='9').contains(&digit) || ('a'..='f').contains(&digit) || ('A'..='F').contains(&digit) } /// Given a string, insert Godot-style escape sequences to make the /// string valid as a GDScript string literal. This involves escaping /// any control characters whose scalar values are less than 32, as /// well as quotes and backslashes. Characters outside of the standard /// ASCII range may or may not be escaped. /// /// ``` /// # use gdlisp::sxp::string::insert_escapes; /// assert_eq!(insert_escapes(r#"foobar"#), r#"foobar"#); /// assert_eq!(insert_escapes(r#"a"b'c"#), r#"a\"b\'c"#); /// assert_eq!(insert_escapes(r#"A\B"#), r#"A\\B"#); /// assert_eq!(insert_escapes("\n"), "\\n"); /// assert_eq!(insert_escapes("α"), "α"); /// assert_eq!(insert_escapes("😀"), "😀"); /// assert_eq!(insert_escapes("\x06"), r#"\u0006"#); /// ``` pub fn insert_escapes(input: &str) -> String { let mut result = String::with_capacity(input.len() + 16); // Just a bit of extra space for escape sequences. let reverse_map = reverse_escape_map(); for ch in input.chars() { if should_escape(ch) { // Escape it match reverse_map.get(&ch) { None => { // Fall back to \uXXXX write!(result, "\\u{:04X}", ch as u32).expect("Failed to write to local string"); } Some(esc) => { // Built-in escape sequence result.push_str(esc); } } } else { // Leave it result.push(ch); } } result } fn should_escape(ch: char) -> bool { ((ch as u32) < 32) || (ch == '"') || (ch == '\'') || (ch == '\\') } fn reverse_escape_map() -> HashMap { let mut map: HashMap = HashMap::new(); for (escape_char, value_char) in &ESCAPE_SEQUENCES { map.insert(*value_char, format!("\\{}", escape_char)); } map } #[cfg(test)] mod tests { use super::*; #[test] fn successful_escaping() { assert_eq!(parse_escapes(r#"I said, \"foobar\"."#).unwrap(), "I said, \"foobar\"."); assert_eq!(parse_escapes(r#"\'\'\'"#).unwrap(), "\'\'\'"); assert_eq!(parse_escapes(r#"\\\\"#).unwrap(), "\\\\"); assert_eq!(parse_escapes(r#"\a\b\n"#).unwrap(), "\x07\x08\n"); assert_eq!(parse_escapes(r#"\f\v\r\t"#).unwrap(), "\x0C\x0B\r\t"); } #[test] fn failed_escaping() { assert!(parse_escapes(r#"\"#).is_err()); assert!(parse_escapes(r#"\I"#).is_err()); assert!(parse_escapes(r#"\u"#).is_err()); assert!(parse_escapes(r#"\u{00"#).is_err()); assert!(parse_escapes(r#"\u13zz"#).is_err()); assert!(parse_escapes(r#"\u{}"#).is_err()); assert!(parse_escapes(r#"\u{potato}"#).is_err()); } } ================================================ FILE: src/sxp/syntax.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! This module provides several helpers for syntactic forms which //! need to be special-cased in the parser. //! //! This includes forms such as `(quote x)`, which is generated by the //! `'x` syntax. use super::ast::AST; use crate::pipeline::source::SourceOffset; use crate::ir::special_form::access_slot::ACCESS_SLOT_FORM_NAME; /// Produces an arbitrary syntactic expression of the form `(a b)`, /// where `a` is a symbol and `b` is an arbitrary [`AST`]. pub fn unary(head: &str, value: AST, pos: SourceOffset) -> AST { AST::list(vec!( AST::symbol(head, pos), value ), pos) } /// Produces an arbitrary syntactic expression of the form `(a b c)`, /// where `a` is a symbol, and `b` and `c` are arbitrary [`AST`] /// values. pub fn binary(head: &str, b: AST, c: AST, pos: SourceOffset) -> AST { AST::list(vec!( AST::symbol(head, pos), b, c, ), pos) } /// Produces an arbitrary syntactic expression of the form `(a b c)`, /// where `a` is a symbol, and `b`, `c`, and `d` are arbitrary [`AST`] /// values. pub fn trinary(head: &str, b: AST, c: AST, d: AST, pos: SourceOffset) -> AST { AST::list(vec!( AST::symbol(head, pos), b, c, d, ), pos) } /// Produces the syntactic form `(quote value)`. pub fn quote(value: AST, pos: SourceOffset) -> AST { unary("quote", value, pos) } /// Produces the syntactic form `(function value)`. pub fn function(value: AST, pos: SourceOffset) -> AST { unary("function", value, pos) } /// Produces the syntactic form `(quasiquote value)`. pub fn quasiquote(value: AST, pos: SourceOffset) -> AST { unary("quasiquote", value, pos) } /// Produces the syntactic form `(unquote value)`. pub fn unquote(value: AST, pos: SourceOffset) -> AST { unary("unquote", value, pos) } /// Produces the syntactic form `(unquote-spliced value)`. pub fn unquote_spliced(value: AST, pos: SourceOffset) -> AST { unary("unquote-spliced", value, pos) } /// Produces the syntactic form `(array ...)`, given the tail of the /// list of arguments. pub fn array(tail: AST, pos: SourceOffset) -> AST { AST::cons(AST::symbol("array", pos), tail, pos) } /// Produces the syntactic form `(dict ...)`, given the tail of the /// list of arguments. pub fn dict(tail: AST, pos: SourceOffset) -> AST { AST::cons(AST::symbol("dict", pos), tail, pos) } /// Produces the syntactic form `(vector x y)` pub fn vector2(x: AST, y: AST, pos: SourceOffset) -> AST { binary("vector", x, y, pos) } /// Produces the syntactic form `(vector x y z)` pub fn vector3(x: AST, y: AST, z: AST, pos: SourceOffset) -> AST { trinary("vector", x, y, z, pos) } /// Produces the syntactic form `(access-slot x y)` pub fn access_slot(x: AST, y: AST, pos: SourceOffset) -> AST { binary(ACCESS_SLOT_FORM_NAME, x, y, pos) } /// Produces a function call which will perform `(x:get-node y)`. /// /// Written in full generality, this is the form `((access-slot x /// get-node) y)`. pub fn get_node_on(x: AST, y: AST, pos: SourceOffset) -> AST { AST::list(vec!( AST::symbol("sys/get-node", pos), x, y, ), pos) } #[cfg(test)] mod tests { use super::*; use crate::sxp::ast::AST; #[test] fn test_unary() { assert_eq!( unary("foo", AST::int(10, SourceOffset(1)), SourceOffset(3)), AST::list( vec!( AST::symbol("foo", SourceOffset(3)), AST::int(10, SourceOffset(1)), ), SourceOffset(3), ) ) } #[test] fn test_binary() { assert_eq!( binary("bar", AST::int(10, SourceOffset(1)), AST::int(20, SourceOffset(99)), SourceOffset(3)), AST::list( vec!( AST::symbol("bar", SourceOffset(3)), AST::int(10, SourceOffset(1)), AST::int(20, SourceOffset(99)), ), SourceOffset(3), ) ) } #[test] fn test_trinary() { assert_eq!( trinary("baz", AST::int(10, SourceOffset(1)), AST::int(20, SourceOffset(99)), AST::int(30, SourceOffset(999)), SourceOffset(3)), AST::list( vec!( AST::symbol("baz", SourceOffset(3)), AST::int(10, SourceOffset(1)), AST::int(20, SourceOffset(99)), AST::int(30, SourceOffset(999)), ), SourceOffset(3), ) ) } #[test] fn test_quote() { assert_eq!( quote(AST::int(9, SourceOffset(100)), SourceOffset(3)), AST::list( vec!( AST::symbol("quote", SourceOffset(3)), AST::int(9, SourceOffset(100)), ), SourceOffset(3), ) ) } #[test] fn test_function() { assert_eq!( function(AST::int(9, SourceOffset(100)), SourceOffset(3)), AST::list( vec!( AST::symbol("function", SourceOffset(3)), AST::int(9, SourceOffset(100)), ), SourceOffset(3), ) ) } #[test] fn test_quasiquote() { assert_eq!( quasiquote(AST::int(9, SourceOffset(100)), SourceOffset(3)), AST::list( vec!( AST::symbol("quasiquote", SourceOffset(3)), AST::int(9, SourceOffset(100)), ), SourceOffset(3), ) ) } #[test] fn test_unquote() { assert_eq!( unquote(AST::int(9, SourceOffset(100)), SourceOffset(3)), AST::list( vec!( AST::symbol("unquote", SourceOffset(3)), AST::int(9, SourceOffset(100)), ), SourceOffset(3), ) ) } #[test] fn test_unquote_spliced() { assert_eq!( unquote_spliced(AST::int(9, SourceOffset(100)), SourceOffset(3)), AST::list( vec!( AST::symbol("unquote-spliced", SourceOffset(3)), AST::int(9, SourceOffset(100)), ), SourceOffset(3), ) ) } // TODO The rest of these } ================================================ FILE: src/util/debug_wrapper.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Wrapper for non-[`Debug`] types that prints a trivial `Debug` //! representation. use serde::{Serialize, Deserialize}; use std::fmt::{self, Debug}; /// A `DebugWrapper` is a very thin wrapper around a `T`. /// `DebugWrapper` is mostly uninteresting, except that it /// unconditionally implements [`Debug`] for all types. The debug /// representation for every instance of `DebugWrapper` is always an /// ellipsis `"..."`. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] #[repr(transparent)] pub struct DebugWrapper(pub T); impl Debug for DebugWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "...") } } ================================================ FILE: src/util/group_by.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`group_by`] function and associated iterator. use std::iter::Peekable; /// A grouping iterator. This iterator is constructed via [`group_by`] /// or [`group_with`] and produces vectors of elements in the original /// (input) iterator, grouped together by the predicate. See /// [`group_by`] for details. pub struct GroupBy { iter: Peekable, function: F, } impl Iterator for GroupBy where I: Iterator, F: FnMut(&::Item, &::Item) -> bool { type Item = Vec<::Item>; fn next(&mut self) -> Option { let mut result: Vec<::Item> = Vec::new(); let mut head = match self.iter.next() { None => { // Iterator is already exhausted, so there's no further groups. return None; } Some(x) => { x } }; // Loop until either we've exhausted the iterator or we've hit an // element that doesn't match. loop { match self.iter.peek() { None => { // At end of iterator, so the group is complete. break; } Some(next) => { if (self.function)(&head, next) { // next is part of the current group, so continue. result.push(head); head = self.iter.next().expect("Inconsistent behavior in Peekable"); } else { // next is not part of the current group, so break. break; } } } } // head is the final element of the current group. result.push(head); Some(result) } } /// Groups elements of the iterator based on the predicate function. /// The resulting output iterator will produce vectors of items from /// the original iterator, where the subvectors are the longest /// subsequences for which the predicate returns true on consecutive /// elements. /// /// See also [`group_with`]. pub fn group_by(iter: I, function: F) -> GroupBy where I: Iterator, F: FnMut(&::Item, &::Item) -> bool { GroupBy { iter: iter.peekable(), function } } /// Groups elements of the iterator based on the grouping function, /// similar to [`group_by`]. The grouping function takes one argument /// and returns a value that can be compared for equality. The /// resulting iterator is grouped by consecutive terms which, under /// the supplied unary function, produce values which are considered /// equal. pub fn group_with(iter: I, mut function: F) -> GroupBy::Item, &::Item) -> bool> where I: Iterator, F: FnMut(&::Item) -> G, G: Eq { group_by(iter, move |a, b| { function(a) == function(b) }) } #[cfg(test)] mod tests { use super::*; #[test] fn group_by_test_1() { let vec = vec!(0, 0, 1, 1, 2, 3, 2, 2); let grouped: Vec<_> = group_by(vec.into_iter(), |a, b| a == b).collect(); assert_eq!(grouped, vec!(vec!(0, 0), vec!(1, 1), vec!(2), vec!(3), vec!(2, 2))); } #[test] fn group_by_test_2() { let vec = vec!(0, 0, 1, 1, 2, 3, 2, 2, 1); let grouped: Vec<_> = group_by(vec.into_iter(), |a, b| a == b).collect(); assert_eq!(grouped, vec!(vec!(0, 0), vec!(1, 1), vec!(2), vec!(3), vec!(2, 2), vec!(1))); } #[test] fn group_by_test_3() { let vec: Vec = vec!(); let grouped: Vec<_> = group_by(vec.into_iter(), |a, b| a == b).collect(); assert_eq!(grouped, Vec::>::new()); } #[test] fn group_with_test() { let vec: Vec = vec!(0, 0, 1, 1, -1, 2, -3); let grouped: Vec<_> = group_with(vec.into_iter(), |a| a.abs()).collect(); assert_eq!(grouped, vec!(vec!(0, 0), vec!(1, 1, -1), vec!(2), vec!(-3))); } } ================================================ FILE: src/util/lattice.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Defines the [`Lattice`] trait, for mathematical //! [lattices](https://en.wikipedia.org/wiki/Lattice_(order)). /// A [lattice](https://en.wikipedia.org/wiki/Lattice_(order)) is a /// collection of values together with a join and meet operation. /// These operations should be associative and commutative, and they /// should satisfy the following rules, often called the *absorption /// laws*. /// /// ```text /// a.join(a.meet(b)) == a /// a.meet(a.join(b)) == a /// ``` /// /// In the future, we may define relationships between this trait, /// [`PartialOrd`], and [`Ord`], since in order theory every total /// order is a lattice and every lattice is a partial order. These /// relations, as of now, are not established in Rust, and this trait /// is unrelated to the other two. In the spirit of providing an /// intuitive API, types which implement either `PartialOrd` or `Ord` /// *and* `Lattice` should make the instances compatible. pub trait Lattice { /// The lattice-theoretic join (or least upper bound) of the two /// values. fn join(self, other: Self) -> Self; /// The lattice-theoretic meet (or greatest lower bound) of the two /// values. fn meet(self, other: Self) -> Self; } #[allow(clippy::unused_unit)] impl Lattice for () { fn join(self, _other: ()) -> () { () } fn meet(self, _other: ()) -> () { () } } /// An implementation of the binary product lattice. impl Lattice for (A, B) { fn join(self, other: (A, B)) -> (A, B) { (self.0.join(other.0), self.1.join(other.1)) } fn meet(self, other: (A, B)) -> (A, B) { (self.0.meet(other.0), self.1.meet(other.1)) } } ================================================ FILE: src/util/lazy.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`Lazy`] type for lazy initialization of data. /// A lazy data structure, containing a `T` which may or may not be /// initialized yet. #[derive(Clone, Debug, Copy, Eq, PartialEq, Ord, PartialOrd)] pub struct Lazy { value: Option, init: Option, } impl T> Lazy { // Implementation detail: A Lazy shall always have exactly one // of `value` or `init` defined. The other shall be empty. An // uninitialized lazy value has only `init` and an initialized one // has only `value`. If both or neither is defined at once, the // object is in an illegal state. pub fn new(init: F) -> Lazy { Lazy { value: None, init: Some(init) } } pub fn force_mut(&mut self) -> &mut T { if self.value.is_none() { let init = self.init.take().expect("Internal error in gdlisp::util::lazy"); self.value = Some(init()); } self.value.as_mut().expect("Internal error in gdlisp::util::lazy") } } ================================================ FILE: src/util/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Various utility functions that don't have a better home. pub mod debug_wrapper; pub mod group_by; pub mod lattice; pub mod lazy; pub mod one; pub mod prefix_matcher; pub mod recursive; use std::collections::HashMap; use std::hash::Hash; use std::io::{self, Read}; use std::convert::Infallible; /// `fold1` acts like [`Iterator::fold`] but without a "starting" /// value. /// /// If `iter` is nonempty, then the first element is used as the /// starting value, and iteration begins formally at the second. If /// `iter` is empty, then [`None`] is returned. pub fn fold1 I::Item>(iter: I, mut f: F) -> Option { iter.fold(None, |x, y| match x { None => Some(y), Some(x) => Some(f(x, y)), }) } /// The type of iterator returned by [`each_pair`]. pub struct PairIter(Option, I); /// Return an iterator over each pair of adjacent elements in `iter`. /// /// # Examples /// /// ``` /// # use gdlisp::util::each_pair; /// let vec1: Vec = vec!(); /// assert_eq!(each_pair(vec1.into_iter()).collect::>(), vec!()); /// /// let vec2: Vec = vec!(10); /// assert_eq!(each_pair(vec2.into_iter()).collect::>(), vec!()); /// /// let vec3: Vec = vec!(10, 20); /// assert_eq!(each_pair(vec3.into_iter()).collect::>(), vec!((10, 20))); /// /// let vec4: Vec = vec!(10, 20, 30); /// assert_eq!(each_pair(vec4.into_iter()).collect::>(), vec!((10, 20), (20, 30))); /// ``` pub fn each_pair(iter: I) -> PairIter where I : Iterator { PairIter(None, iter) } impl Iterator for PairIter where I : Iterator, T : Clone { type Item = (T, T); fn next(&mut self) -> Option<(T, T)> { let fst = self.0.clone().or_else(|| self.1.next()); let snd = self.1.next(); self.0 = snd.clone(); fst.and_then(|fst| snd.map(|snd| (fst, snd))) } } /// The type of iterator returned by [`each_non_overlapping_pair`]. pub struct NonOverlappingPairIter(I); /// Return an iterator over each non-overlapping pair of adjacent /// elements in `iter`. If there are an odd number of elements, then /// the final element will not be included in any of the pairs. /// /// # Examples /// /// ``` /// # use gdlisp::util::each_non_overlapping_pair; /// let vec1: Vec = vec!(); /// assert_eq!(each_non_overlapping_pair(vec1.into_iter()).collect::>(), vec!()); /// /// let vec2: Vec = vec!(10); /// assert_eq!(each_non_overlapping_pair(vec2.into_iter()).collect::>(), vec!()); /// /// let vec3: Vec = vec!(10, 20); /// assert_eq!(each_non_overlapping_pair(vec3.into_iter()).collect::>(), vec!((10, 20))); /// /// let vec4: Vec = vec!(10, 20, 30); /// assert_eq!(each_non_overlapping_pair(vec4.into_iter()).collect::>(), vec!((10, 20))); /// /// let vec5: Vec = vec!(10, 20, 30, 40); /// assert_eq!(each_non_overlapping_pair(vec5.into_iter()).collect::>(), vec!((10, 20), (30, 40))); /// ``` pub fn each_non_overlapping_pair(iter: I) -> NonOverlappingPairIter { NonOverlappingPairIter(iter) } impl Iterator for NonOverlappingPairIter { type Item = (::Item, ::Item); fn next(&mut self) -> Option { let fst = self.0.next(); let snd = self.0.next(); fst.and_then(|fst| snd.map(|snd| (fst, snd))) } } /// Given two [`HashMap`] values `a` and `b`, merge `b` into `a`. /// /// For every key of `b` which is not in `a`, that key-value pair is /// added verbatim to `a`. For every key of `b` which *is* present in /// `a`, apply `merge_fn` to the two values to combine them, storing /// the result in `a`. The `a` value is passed to `merge_fn` before /// the `b` value. pub fn merge_hashmap_inplace(a: &mut HashMap, b: HashMap, mut merge_fn: impl FnMut(V, V) -> V) { for (k, v) in b { match a.remove(&k) { None => a.insert(k, v), Some(v1) => a.insert(k, merge_fn(v, v1)), }; } } /// Consume the `Read` instance, as though using [`Read::read_to_end`]. /// /// Accumulates the result into a string, returning an error of kind /// [`io::ErrorKind::InvalidData`] if the data is not valid UTF-8. pub fn read_to_end(input: &mut impl Read) -> io::Result { let mut vec = Vec::new(); input.read_to_end(&mut vec)?; String::from_utf8(vec).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } /// Convert the `Option` to a `Vec`, returning a vector of length /// either zero or one. pub fn option_to_vec(value: Option) -> Vec { match value { None => vec!(), Some(x) => vec!(x), } } /// This is equivalent to the nightly-only Rust function /// `Result::into_ok`, for safely unwrapping `Result` values which can /// provably never be an error. pub fn extract_err(value: Result) -> T { match value { Ok(value) => value, Err(contra) => match contra {}, } } /// Unzip an iterator into two collections, taking error values into /// consideration. This is the spiritual composition of /// [`Iterator::unzip`] and the /// [`FromIterator`](std::iter::FromIterator) implementation on /// [`Result`]. pub fn unzip_err(iter: I) -> Result<(FromA, FromB), E> where I : Iterator>, FromA : Default + Extend, FromB : Default + Extend { let mut from_a = FromA::default(); let mut from_b = FromB::default(); for term in iter { let (a, b) = term?; from_a.extend(one::One(a)); from_b.extend(one::One(b)); } Ok((from_a, from_b)) } /// Returns a matching element from the vector, mutably. /// /// If no matching element is found, then a new element is appended /// and returned. #[allow(clippy::redundant_closure)] // Clippy seems to have made a mistake here, suggested fix fails the borrow checker pub fn find_or_else_mut(vec: &mut Vec, default: impl FnOnce() -> T, mut pred: impl FnMut(&T) -> bool) -> &mut T { if vec.iter().any(|x| pred(x)) { vec.iter_mut().find(|x| pred(x)).expect("Internal error in find_or_else_mut") } else { vec.push(default()); vec.last_mut().expect("Internal error in find_or_else_mut") } } #[cfg(test)] mod tests { use super::*; #[derive(Eq, PartialEq, Copy, Clone, Debug)] struct FakeError; #[test] fn test_fold1() { let vec1: Vec = vec!(1, 2, 3, 4); assert_eq!(fold1(vec1.into_iter(), |x, y| x + y), Some(10)); let vec2: Vec = vec!(); assert_eq!(fold1(vec2.into_iter(), |x, y| x + y), None); let vec3: Vec = vec!(1, 2, 3, 4); assert_eq!(fold1(vec3.into_iter(), |x, y| x - y), Some(-8)); } #[test] fn test_extract_err() { assert_eq!(extract_err(Ok(1)), 1); } #[test] fn test_unzip_err_1() { let vec1: Vec> = vec!(Ok((1, 10)), Ok((2, 20)), Ok((3, 30))); assert_eq!(unzip_err(vec1.into_iter()), Ok((vec!(1, 2, 3), vec!(10, 20, 30)))); let vec2: Vec> = vec!(Ok((1, 10)), Err(FakeError), Ok((3, 30))); let unzip_result2: Result<(Vec<_>, Vec<_>), _> = unzip_err(vec2.into_iter()); assert_eq!(unzip_result2, Err(FakeError)); let vec3: Vec> = vec!(); assert_eq!(unzip_err(vec3.into_iter()), Ok((vec!(), vec!()))); } #[test] fn test_unzip_err_2() { // Make sure errors actually abort the process and the iterator is // lazy. let mut count = 0; let vec = vec!(1, 2, 3, 4, 5); let iter = vec.into_iter().map(|x| { match x { 1 => { count += 1; } 3 => { return Err(FakeError) } 5 => { count += 100; } _ => {} }; Ok((x, 0)) }); let unzip_result: Result<(Vec<_>, Vec<_>), _> = unzip_err(iter); assert_eq!(unzip_result, Err(FakeError)); // Only the count += 1 should have run, not the count += 100 assert_eq!(count, 1); } #[test] fn find_or_else_mut_1() { // If it already exists let mut v = vec!(1, 2, 3, 4); { let m = find_or_else_mut(&mut v, || panic!("default case called unexpectedly"), |x| *x == 3); assert_eq!(m, &mut 3); } assert_eq!(v, vec!(1, 2, 3, 4)); } #[test] fn find_or_else_mut_2() { // If it doesn't exist let mut v = vec!(1, 2, 3, 4); { let m = find_or_else_mut(&mut v, || 10, |x| *x == 10); assert_eq!(m, &mut 10); } assert_eq!(v, vec!(1, 2, 3, 4, 10)); } } ================================================ FILE: src/util/one.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`One`] type, a trivial collection which always //! contains one value. use std::mem::swap; /// A `One` always contains a single `T`. `One` is a collection /// type and can be iterated over. #[derive(Clone, Debug, Copy, Eq, PartialEq, Ord, PartialOrd)] pub struct One(pub T); /// The underlying iterator for [`One`]. Returned by [`One::iter`]. pub struct Iter<'a, T>(Option<&'a T>); /// The underlying mutable iterator for [`One`]. Returned by [`One::iter_mut`]. pub struct IterMut<'a, T>(Option<&'a mut T>); /// The underlying owning iterator for [`One`]. Returned by [`One::into_iter`]. #[derive(Clone)] pub struct IntoIter(Option); impl One { pub fn iter(&self) -> Iter<'_, T> { Iter(Some(&self.0)) } pub fn iter_mut(&mut self) -> IterMut<'_, T> { IterMut(Some(&mut self.0)) } } // Default implementation imposes T: Clone, which is unnecessary. impl<'a, T> Clone for Iter<'a, T> { fn clone(&self) -> Self { Iter(self.0) } } impl IntoIterator for One { type Item = T; type IntoIter = IntoIter; fn into_iter(self) -> IntoIter { IntoIter(Some(self.0)) } } impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option<&'a T> { let mut result = None; swap(&mut self.0, &mut result); result } } impl<'a, T> Iterator for IterMut<'a, T> { type Item = &'a mut T; fn next(&mut self) -> Option<&'a mut T> { let mut result = None; swap(&mut self.0, &mut result); result } } impl Iterator for IntoIter { type Item = T; fn next(&mut self) -> Option { let mut result = None; swap(&mut self.0, &mut result); result } } #[cfg(test)] mod tests { use super::*; #[test] fn one_iter() { let one = One(100); assert_eq!(one.iter().collect::>(), vec!(&100)); } #[test] fn one_iter_mut() { let mut one = One(100); for x in one.iter_mut() { *x = 200; } assert_eq!(one, One(200)); } #[test] fn one_into_iter() { let one = One(100); for x in one { assert_eq!(x, 100); } } } ================================================ FILE: src/util/prefix_matcher.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . /// Provides the [`PrefixMatcher`] type, for efficiently finding a /// matching prefix of a given string. use std::collections::HashMap; use std::iter; use std::borrow::Borrow; /// [`PrefixMatcher`] is a state machine designed for identifying one /// of a finite number of prefixes. pub struct PrefixMatcher<'a> { characters: HashMap, table: PrefixTable, terminal_states: Vec>, } // A 2D array masquerading as a vector. struct PrefixTable { width_impl: usize, table: Vec, } impl PrefixTable { fn new(width: usize) -> PrefixTable { PrefixTable { width_impl: width, table: Vec::new() } } fn width(&self) -> usize { self.width_impl } fn height(&self) -> usize { self.table.len() / self.width() } fn get(&self, y: usize, x: usize) -> Option<&T> { if y >= self.height() || x >= self.width() { None } else { Some(&self.table[y * self.width() + x]) } } fn get_mut(&mut self, y: usize, x: usize) -> Option<&mut T> { if y >= self.height() || x >= self.width() { None } else { let w = self.width(); Some(&mut self.table[y * w + x]) } } fn add_row(&mut self, row: impl Iterator) { let row: Vec<_> = row.collect(); assert_eq!(row.len(), self.width()); self.table.extend(row); } } impl PrefixTable { fn add_row_value(&mut self, value: T) { let iter = iter::repeat(value).take(self.width()); self.add_row(iter); } } impl<'a> PrefixMatcher<'a> { /// Build a prefix matcher from a collection of strings. The slice /// of strings should not contain any duplicates, but it is /// permitted to contain strings which are prefixes of each other. pub fn build(options: I) -> Self where S: Borrow + ?Sized + 'a, I: Iterator { let options: Vec<_> = options.collect(); let characters = PrefixMatcher::build_chars_map(&options); let mut table = PrefixTable::new(characters.len()); let mut terminal_states = Vec::new(); table.add_row_value(EMPTY_CELL); terminal_states.push(None); for opt in options { let mut current_state: usize = 0; for ch in opt.borrow().chars() { let column = characters[&ch]; let mut new_state = *table.get(current_state, column).unwrap(); if new_state == EMPTY_CELL { new_state = table.height(); table.add_row_value(EMPTY_CELL); terminal_states.push(None); *table.get_mut(current_state, column).unwrap() = new_state; } current_state = new_state; } terminal_states[current_state] = Some(opt.borrow()); } PrefixMatcher { characters, table, terminal_states } } fn build_chars_map(options: &[&S]) -> HashMap where S: Borrow + ?Sized + 'a { let mut next_index: usize = 0; let mut result = HashMap::new(); for string in options { for ch in (*string).borrow().chars() { result.entry(ch).or_insert_with(|| { next_index += 1; next_index - 1 }); } } result } /// Returns the longest prefix in this [`PrefixMatcher`] which is a /// prefix of the input string. If there is no match, returns /// `None`. pub fn identify_prefix(&self, string: &str) -> Option<&'a str> { let mut final_result: Option<&'a str> = None; let mut state: usize = 0; for ch in string.chars() { if let Some(new_result) = self.terminal_states[state] { final_result = Some(new_result); } match self.characters.get(&ch) { None => { // Unknown character, we're done. break; } Some(column) => { let new_state = *self.table.get(state, *column).unwrap(); if new_state == EMPTY_CELL { // Unknown prefix, we're done. break; } state = new_state; } } } if let Some(new_result) = self.terminal_states[state] { final_result = Some(new_result); } final_result } } const EMPTY_CELL: usize = usize::MAX; #[cfg(test)] mod tests { use super::*; #[test] fn test_chars_map() { let options = vec!("abc", "def", "aBC", "g"); let intended_result = HashMap::from([ ('a', 0), ('b', 1), ('c', 2), ('d', 3), ('e', 4), ('f', 5), ('B', 6), ('C', 7), ('g', 8), ]); assert_eq!(PrefixMatcher::build_chars_map(&options), intended_result); } #[test] fn test_empty_prefix_identifier() { let options: Vec<&'static str> = vec!(); let matcher = PrefixMatcher::build(options.iter()); assert_eq!(matcher.identify_prefix("abc"), None); assert_eq!(matcher.identify_prefix("def"), None); assert_eq!(matcher.identify_prefix(""), None); assert_eq!(matcher.identify_prefix("---"), None); } #[test] fn test_empty_string_prefix_identifier() { let options = vec!(""); let matcher = PrefixMatcher::build(options.iter()); assert_eq!(matcher.identify_prefix("abc"), Some("")); assert_eq!(matcher.identify_prefix("def"), Some("")); assert_eq!(matcher.identify_prefix(""), Some("")); assert_eq!(matcher.identify_prefix("---"), Some("")); } #[test] fn test_prefix_identifier_foobar() { let options = vec!("foo", "bar", "foobar"); let matcher = PrefixMatcher::build(options.iter()); assert_eq!(matcher.identify_prefix("abc"), None); assert_eq!(matcher.identify_prefix(""), None); assert_eq!(matcher.identify_prefix("foo"), Some("foo")); assert_eq!(matcher.identify_prefix("football"), Some("foo")); assert_eq!(matcher.identify_prefix("bar"), Some("bar")); assert_eq!(matcher.identify_prefix("barbecue"), Some("bar")); assert_eq!(matcher.identify_prefix("foobaz"), Some("foo")); assert_eq!(matcher.identify_prefix("foobar"), Some("foobar")); assert_eq!(matcher.identify_prefix("foobarbaz"), Some("foobar")); } #[test] fn test_prefix_identifier_foobar_with_empty() { // Empty string should act as a fallback if nothing else matches. let options = vec!("", "foo", "bar", "foobar"); let matcher = PrefixMatcher::build(options.iter()); assert_eq!(matcher.identify_prefix("abc"), Some("")); assert_eq!(matcher.identify_prefix(""), Some("")); assert_eq!(matcher.identify_prefix("foo"), Some("foo")); assert_eq!(matcher.identify_prefix("football"), Some("foo")); assert_eq!(matcher.identify_prefix("bar"), Some("bar")); assert_eq!(matcher.identify_prefix("barbecue"), Some("bar")); assert_eq!(matcher.identify_prefix("foobaz"), Some("foo")); assert_eq!(matcher.identify_prefix("foobar"), Some("foobar")); assert_eq!(matcher.identify_prefix("foobarbaz"), Some("foobar")); } } ================================================ FILE: src/util/recursive.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . //! Provides the [`Recursive`] trait. /// A `Recursive` data structure is one that (as the name implies) can /// recursively contain elements of itself. Further, implementations /// of this trait provide a mechanism for determining the maximum /// recursion depth of the trait. pub trait Recursive { /// The maximum depth of the recursive data structure. Atomic nodes, /// those which do not contain any further instances of the data /// structure, should have a depth of 1. Any node which contains /// other nodes has depth equal to the maximum of the depths of its /// subnodes plus 1. fn depth(&self) -> u32; } ================================================ FILE: tests/integration_tests.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod test; ================================================ FILE: tests/test/builtin_function_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use gdlisp::runner::version::{get_godot_version, Version}; use super::common::*; #[test] pub fn vector_test() { assert_eq!(parse_compile_and_output("(vector 9 10)"), "return Vector2(9, 10)\n"); assert_eq!(parse_compile_and_output("(vector 9 10 11)"), "return Vector3(9, 10, 11)\n"); } #[test] pub fn vector_syntax_test() { assert_eq!(parse_compile_and_output("V{9 10}"), "return Vector2(9, 10)\n"); assert_eq!(parse_compile_and_output("V{9 10 11}"), "return Vector3(9, 10, 11)\n"); } #[test] pub fn array_test() { assert_eq!(parse_compile_and_output("(array 9 10)"), "return [9, 10]\n"); assert_eq!(parse_compile_and_output("(array 9 10 11)"), "return [9, 10, 11]\n"); } #[test] pub fn array_syntax_test() { assert_eq!(parse_compile_and_output("[1 2 3 4]"), "return [1, 2, 3, 4]\n"); } #[test] pub fn dictionary_test() { assert_eq!(parse_compile_and_output("(dict 1 2 3 4)"), "return {1: 2, 3: 4}\n"); } #[test] pub fn dictionary_odd_args_test() { // Drops the last arg silently. assert_eq!(parse_compile_and_output("(dict 1 2 3)"), "return {1: 2}\n"); } #[test] pub fn dictionary_literal_test() { assert_eq!(parse_compile_and_output("{1 2 3 4}"), "return {1: 2, 3: 4}\n"); } #[test] pub fn yield_test() { assert_eq!(parse_compile_and_output("(yield)"), "return yield()\n"); assert_eq!(parse_compile_and_output("(yield 1 2)"), "return yield(1, 2)\n"); } #[test] pub fn assert_test() { assert_eq!(parse_compile_and_output("(assert #t)"), "return assert(true)\n"); assert_eq!(parse_compile_and_output("(assert #t \"a\")"), "return assert(true, \"a\")\n"); } #[test] pub fn attribute_test() { assert_eq!(parse_compile_and_output("(let ((foo 1)) foo:bar)"), "var foo = 1\nreturn foo.bar\n"); assert_eq!(parse_compile_and_output("(let ((foo 1)) foo:bar 2)"), "var foo = 1\nreturn 2\n"); } #[test] pub fn method_test() { assert_eq!(parse_compile_and_output("(let ((foo 1)) (foo:bar))"), "var foo = 1\nreturn foo.bar()\n"); assert_eq!(parse_compile_and_output("(let ((foo 1)) (foo:bar 100) 2)"), "var foo = 1\nfoo.bar(100)\nreturn 2\n"); } #[test] pub fn simple_builtin_test() { assert_eq!(parse_compile_and_output("(cons 1 2)"), "return GDLisp.cons(1, 2)\n"); assert_eq!(parse_compile_and_output("(cons 1 (cons 2 3))"), "return GDLisp.cons(1, GDLisp.cons(2, 3))\n"); assert_eq!(parse_compile_and_output("(intern 10)"), "return GDLisp.intern(10)\n"); } #[test] pub fn known_gdscript_classes_test_1() { assert_eq!(parse_compile_and_output("[Sprite Node Node2D GDScript Object]"), "return [Sprite, Node, Node2D, GDScript, GDLisp._Object]\n"); } #[test] pub fn known_gdscript_classes_test_2() { // Note: They all get checked, but all except the last is elided by the statefulness rules. assert_eq!(parse_compile_and_output("(progn Sprite Node Node2D GDScript Object)"), "return GDLisp._Object\n"); } #[test] pub fn unknown_gdscript_classes_test() { assert_eq!( parse_compile_and_output_err("(progn NotARealClass Node2D GDScript Object)"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("NotARealClass")), SourceOffset(7)))), ); } #[test] pub fn return_test() { assert_eq!(parse_compile_and_output("(progn (if 1 (return 2)) 3)"), "if 1:\n return 2\nelse:\n if true:\n pass\n else:\n pass\nreturn 3\n"); } #[test] pub fn yield_running_test() { let result = parse_and_run(r#" ((defn foo () (print 2) (yield) (print 4) 6) (print 1) (let ((x (foo))) (print 3) (let ((final (x:resume))) (print 5) (print final)))) "#); assert_eq!(result, "\n1\n2\n3\n4\n5\n6\n"); } #[test] pub fn yield_star_running_test() { let result = parse_and_run(r#" ((defn foo () (print 2) (yield) (print 4) 6) (defn bar () (yield* (foo))) (print 1) (let ((x (bar))) (print 3) (let ((final (x:resume))) (print 5) (print final)))) "#); assert_eq!(result, "\n1\n2\n3\n4\n5\n6\n"); } #[test] pub fn custom_call_magic_test() { assert_eq!(parse_compile_decl("((defn foo (x y) (sys/call-magic ADDITION) 9) (defn run () (foo 10 20)))"), r#"extends Reference static func foo(x, y): return 9 static func run(): return 10 + 20 "#); } #[test] pub fn custom_call_magic_test_failed() { assert_eq!( parse_compile_decl_err("((defn foo (x y) (sys/call-magic THIS-MAGIC-DOES-NOT-EXIST) 9))"), Err(PError::from(GDError::new(GDErrorF::NoSuchMagic(String::from("THIS-MAGIC-DOES-NOT-EXIST")), SourceOffset(2)))), ); } #[test] pub fn split_call_test_1() { assert_eq!(parse_compile_and_output("(sys/split 0:car):car:car:car"), r#"var _split = 0.car return _split.car.car.car "#); } #[test] pub fn split_call_test_2() { assert_eq!(parse_compile_and_output("(sys/split (sys/split 0:car):car):car:car"), r#"var _split = 0.car var _split_0 = _split.car return _split_0.car.car "#); } #[test] pub fn wrapped_random_functions_test() { assert_eq!(parse_compile_and_output("(randomize)"), "return randomize()\n"); assert_eq!(parse_compile_and_output("(randi)"), "return randi()\n"); assert_eq!(parse_compile_and_output("(randf)"), "return randf()\n"); assert_eq!(parse_compile_and_output("(rand-range 0 1)"), "return rand_range(0, 1)\n"); assert_eq!(parse_compile_and_output("(seed 999)"), "return seed(999)\n"); assert_eq!(parse_compile_and_output("(rand-seed 999)"), "return rand_seed(999)\n"); } #[test] pub fn wrapped_math_functions_test() { assert_eq!(parse_compile_and_output("(clamp 0 1 2)"), "return clamp(0, 1, 2)\n"); assert_eq!(parse_compile_and_output("(abs 0)"), "return abs(0)\n"); assert_eq!(parse_compile_and_output("(acos 0)"), "return acos(0)\n"); assert_eq!(parse_compile_and_output("(asin 0)"), "return asin(0)\n"); assert_eq!(parse_compile_and_output("(atan 0)"), "return atan(0)\n"); assert_eq!(parse_compile_and_output("(atan2 0 1)"), "return atan2(0, 1)\n"); assert_eq!(parse_compile_and_output("(cos 0)"), "return cos(0)\n"); assert_eq!(parse_compile_and_output("(cosh 0)"), "return cosh(0)\n"); assert_eq!(parse_compile_and_output("(sin 0)"), "return sin(0)\n"); assert_eq!(parse_compile_and_output("(sinh 0)"), "return sinh(0)\n"); assert_eq!(parse_compile_and_output("(tan 0)"), "return tan(0)\n"); assert_eq!(parse_compile_and_output("(tanh 0)"), "return tanh(0)\n"); assert_eq!(parse_compile_and_output("(ceil 0)"), "return ceil(0)\n"); assert_eq!(parse_compile_and_output("(exp 0)"), "return exp(0)\n"); assert_eq!(parse_compile_and_output("(floor 0)"), "return floor(0)\n"); assert_eq!(parse_compile_and_output("(sqrt 10)"), "return sqrt(10)\n"); assert_eq!(parse_compile_and_output("(pow 10 2)"), "return pow(10, 2)\n"); assert_eq!(parse_compile_and_output("(fmod 10 2)"), "return fmod(10, 2)\n"); assert_eq!(parse_compile_and_output("(fposmod 10 2)"), "return fposmod(10, 2)\n"); assert_eq!(parse_compile_and_output("(posmod 10 2)"), "return posmod(10, 2)\n"); assert_eq!(parse_compile_and_output("(sign 10)"), "return sign(10)\n"); assert_eq!(parse_compile_and_output("(is-nan 10)"), "return is_nan(10)\n"); assert_eq!(parse_compile_and_output("(is-inf 10)"), "return is_inf(10)\n"); assert_eq!(parse_compile_and_output("(is-equal-approx 10 10)"), "return is_equal_approx(10, 10)\n"); assert_eq!(parse_compile_and_output("(is-zero-approx 10)"), "return is_zero_approx(10)\n"); assert_eq!(parse_compile_and_output("(stepify 0.5 2)"), "return stepify(5e-1, 2)\n"); assert_eq!(parse_compile_and_output("(step-decimals 0.5)"), "return step_decimals(5e-1)\n"); assert_eq!(parse_compile_and_output("(deg2rad 60)"), "return deg2rad(60)\n"); assert_eq!(parse_compile_and_output("(rad2deg PI)"), "return rad2deg(PI)\n"); assert_eq!(parse_compile_and_output("(linear2db 0)"), "return linear2db(0)\n"); assert_eq!(parse_compile_and_output("(db2linear 0)"), "return db2linear(0)\n"); assert_eq!(parse_compile_and_output("(log 10)"), "return log(10)\n"); assert_eq!(parse_compile_and_output("(round 10)"), "return round(10)\n"); assert_eq!(parse_compile_and_output("(wrapf 10 20 15)"), "return wrapf(10, 20, 15)\n"); assert_eq!(parse_compile_and_output("(wrapi 10 20 15)"), "return wrapi(10, 20, 15)\n"); assert_eq!(parse_compile_and_output("(cartesian2polar 10 10)"), "return cartesian2polar(10, 10)\n"); assert_eq!(parse_compile_and_output("(polar2cartesian 10 PI)"), "return polar2cartesian(10, PI)\n"); assert_eq!(parse_compile_and_output("(nearest-po2 17)"), "return nearest_po2(17)\n"); } #[test] pub fn wrapped_error_functions_test() { assert_eq!(parse_compile_and_output("(push-error \"A\")"), "return push_error(\"A\")\n"); assert_eq!(parse_compile_and_output("(push-warning \"A\")"), "return push_warning(\"A\")\n"); } #[test] pub fn wrapped_load_functions_test() { assert_eq!(parse_compile_and_output("(load \"A\")"), "return load(\"A\")\n"); } #[test] pub fn failed_preload_function_test() { assert_eq!(parse_compile_and_output_err("(preload \"A\")"), Err(PError::from(GDError::new(GDErrorF::BadPreloadArgument(String::from("A")), SourceOffset(0))))); } #[test] pub fn wrapped_range_functions_test() { assert_eq!(parse_compile_and_output("(inverse-lerp 0 10 6)"), "return inverse_lerp(0, 10, 6)\n"); assert_eq!(parse_compile_and_output("(lerp 0 10 0.4)"), "return lerp(0, 10, 4e-1)\n"); assert_eq!(parse_compile_and_output("(lerp-angle 0 10 0.4)"), "return lerp_angle(0, 10, 4e-1)\n"); assert_eq!(parse_compile_and_output("(range-lerp 5 0 10 -10 -20)"), "return range_lerp(5, 0, 10, -10, -20)\n"); assert_eq!(parse_compile_and_output("(move-toward 10 5 1)"), "return move_toward(10, 5, 1)\n"); assert_eq!(parse_compile_and_output("(ease 0.7 2.0)"), "return ease(7e-1, 2e0)\n"); assert_eq!(parse_compile_and_output("(smoothstep 1.0 5.0 0.5)"), "return smoothstep(1e0, 5e0, 5e-1)\n"); } #[test] pub fn wrapped_misc_functions_test() { assert_eq!(parse_compile_and_output("(hash \"A\")"), "return hash(\"A\")\n"); assert_eq!(parse_compile_and_output("(get-stack)"), "return get_stack()\n"); assert_eq!(parse_compile_and_output("(print-stack)"), "return print_stack()\n"); assert_eq!(parse_compile_and_output("(is-instance-valid (Reference:new))"), "return is_instance_valid(Reference.new())\n"); assert_eq!(parse_compile_and_output("(parse-json nil)"), "return parse_json(GDLisp.nil)\n"); assert_eq!(parse_compile_and_output("(to-json \"{}\")"), "return to_json(\"{}\")\n"); assert_eq!(parse_compile_and_output("(validate-json \"{}\")"), "return validate_json(\"{}\")\n"); assert_eq!(parse_compile_and_output("(dict2inst {})"), "return dict2inst({})\n"); assert_eq!(parse_compile_and_output("(inst2dict (Reference:new))"), "return inst2dict(Reference.new())\n"); assert_eq!(parse_compile_and_output("(str2var \"{}\")"), "return str2var(\"{}\")\n"); assert_eq!(parse_compile_and_output("(var2str (Reference:new))"), "return var2str(Reference.new())\n"); assert_eq!(parse_compile_and_output("(weakref (Reference:new))"), "return weakref(Reference.new())\n"); assert_eq!(parse_compile_and_output("(funcref (Reference:new) \"potato\")"), "return funcref(Reference.new(), \"potato\")\n"); assert_eq!(parse_compile_and_output("(type-exists \"Reference\")"), "return type_exists(\"Reference\")\n"); assert_eq!(parse_compile_and_output("(Color8 0 0 0)"), "return Color8(0, 0, 0)\n"); assert_eq!(parse_compile_and_output("(Color8 0 0 0 255)"), "return Color8(0, 0, 0, 255)\n"); assert_eq!(parse_compile_and_output("(ColorN \"red\")"), "return ColorN(\"red\")\n"); assert_eq!(parse_compile_and_output("(ColorN \"red\" 1.0)"), "return ColorN(\"red\", 1e0)\n"); assert_eq!(parse_compile_and_output("(var2bytes 0)"), "return var2bytes(0)\n"); assert_eq!(parse_compile_and_output("(var2bytes 0 #f)"), "return var2bytes(0, false)\n"); assert_eq!(parse_compile_and_output("(bytes2var ())"), "return bytes2var(null)\n"); assert_eq!(parse_compile_and_output("(bytes2var () #f)"), "return bytes2var(null, false)\n"); } #[test] pub fn convert_function_test() { assert_eq!(parse_compile_and_output("(convert 1 Int)"), "return GDLisp._convert(1, GDLisp.Int)\n"); assert_eq!(parse_compile_and_output("(convert 1 TYPE_INT)"), "return GDLisp._convert(1, TYPE_INT)\n"); } #[test] pub fn convert_function_run_test() { assert_eq!(parse_and_run("((print (convert 1.5 Int)))"), "\n1\n"); assert_eq!(parse_and_run("((print (convert 1.5 TYPE_INT)))"), "\n1\n"); } #[test] pub fn instance_from_id_function_test() { assert_eq!(parse_compile_and_output("(instance-from-id 9)"), "return instance_from_id(9)\n"); } #[test] pub fn instance_from_id_function_run_test() { assert_eq!(parse_and_run(r#"((defclass Foo () (defvar foo "mystring")) (let* ((x (Foo:new)) (id (x:get-instance-id))) (print (instance-from-id id):foo)))"#), "\nmystring\n"); } #[test] pub fn str_test() { assert_eq!(parse_compile_and_output("(str 3)"), "return str(3)\n"); assert_eq!(parse_compile_and_output("(str 3 \"a\")"), "return str(3, \"a\")\n"); } #[test] pub fn printerr_test() { assert_eq!(parse_compile_and_output("(printerr 3 \"a\")"), "return printerr(3, \"a\")\n"); } #[test] pub fn print_test() { assert_eq!(parse_compile_and_output("(print 3 \"a\")"), "return print(3, \"a\")\n"); } #[test] pub fn printt_test() { assert_eq!(parse_compile_and_output("(printt 3 \"a\")"), "return printt(3, \"a\")\n"); } #[test] pub fn prints_test() { assert_eq!(parse_compile_and_output("(prints 3 \"a\")"), "return prints(3, \"a\")\n"); } #[test] pub fn printraw_test() { assert_eq!(parse_compile_and_output("(printraw 3 \"a\")"), "return printraw(3, \"a\")\n"); } #[test] pub fn print_debug_test() { assert_eq!(parse_compile_and_output("(print-debug 3 \"a\")"), "return print_debug(3, \"a\")\n"); } #[test] pub fn builtin_constant_on_type_test() { assert_eq!(parse_compile_and_output("(print Transform2D:IDENTITY)"), "return print(GDLisp._Transform2D.IDENTITY)\n"); } #[test] pub fn builtin_type_constructor_test() { assert_eq!(parse_compile_and_output("(Bool 0)"), "return bool(0)\n"); assert_eq!(parse_compile_and_output("(Int 99.1)"), "return int(9.91e1)\n"); assert_eq!(parse_compile_and_output("(Float \"0\")"), "return float(\"0\")\n"); assert_eq!(parse_compile_and_output("(String 10)"), "return String(10)\n"); assert_eq!(parse_compile_and_output("(Rect2 V{0 0} V{1 1})"), "return Rect2(Vector2(0, 0), Vector2(1, 1))\n"); assert_eq!(parse_compile_and_output("(Rect2 0 0 1 1)"), "return Rect2(0, 0, 1, 1)\n"); assert_eq!(parse_compile_and_output("(AABB V{0 0 0} V{1 1 1})"), "return AABB(Vector3(0, 0, 0), Vector3(1, 1, 1))\n"); assert_eq!(parse_compile_and_output("(RID (Reference:new))"), "return RID(Reference.new())\n"); assert_eq!(parse_compile_and_output("(Dictionary {})"), "return Dictionary({})\n"); assert_eq!(parse_compile_and_output("(Array [])"), "return Array([])\n"); assert_eq!(parse_compile_and_output("(PoolColorArray [])"), "return PoolColorArray([])\n"); assert_eq!(parse_compile_and_output("(PoolByteArray [])"), "return PoolByteArray([])\n"); assert_eq!(parse_compile_and_output("(PoolIntArray [])"), "return PoolIntArray([])\n"); assert_eq!(parse_compile_and_output("(PoolRealArray [])"), "return PoolRealArray([])\n"); assert_eq!(parse_compile_and_output("(PoolVector2Array [])"), "return PoolVector2Array([])\n"); assert_eq!(parse_compile_and_output("(PoolVector3Array [])"), "return PoolVector3Array([])\n"); assert_eq!(parse_compile_and_output("(Vector2 1 2)"), "return Vector2(1, 2)\n"); assert_eq!(parse_compile_and_output("(Vector3 0 1 2)"), "return Vector3(0, 1, 2)\n"); assert_eq!(parse_compile_and_output("(Transform2D ())"), "return Transform2D(null)\n"); assert_eq!(parse_compile_and_output("(Transform2D V{0 0} V{0 0} V{0 0})"), "return Transform2D(Vector2(0, 0), Vector2(0, 0), Vector2(0, 0))\n"); assert_eq!(parse_compile_and_output("(Transform2D 0 V{1 1})"), "return Transform2D(0, Vector2(1, 1))\n"); assert_eq!(parse_compile_and_output("(Plane V{0 0 0} 10)"), "return Plane(Vector3(0, 0, 0), 10)\n"); assert_eq!(parse_compile_and_output("(Plane V{0 0 0} V{0 0 1} V{1 0 0})"), "return Plane(Vector3(0, 0, 0), Vector3(0, 0, 1), Vector3(1, 0, 0))\n"); assert_eq!(parse_compile_and_output("(Plane 0 0 0 1)"), "return Plane(0, 0, 0, 1)\n"); assert_eq!(parse_compile_and_output("(Quat ())"), "return Quat(null)\n"); assert_eq!(parse_compile_and_output("(Quat V{0 0 0} 1)"), "return Quat(Vector3(0, 0, 0), 1)\n"); assert_eq!(parse_compile_and_output("(Quat 0 0 0 1)"), "return Quat(0, 0, 0, 1)\n"); assert_eq!(parse_compile_and_output("(Basis ())"), "return Basis(null)\n"); assert_eq!(parse_compile_and_output("(Basis V{0 0 0})"), "return Basis(Vector3(0, 0, 0))\n"); assert_eq!(parse_compile_and_output("(Basis V{0 0 0} 1)"), "return Basis(Vector3(0, 0, 0), 1)\n"); assert_eq!(parse_compile_and_output("(Basis V{0 0 0} V{0 0 1} V{1 0 0})"), "return Basis(Vector3(0, 0, 0), Vector3(0, 0, 1), Vector3(1, 0, 0))\n"); assert_eq!(parse_compile_and_output("(Transform ())"), "return Transform(null)\n"); assert_eq!(parse_compile_and_output("(Transform () V{0 0 0})"), "return Transform(null, Vector3(0, 0, 0))\n"); assert_eq!(parse_compile_and_output("(Transform V{0 0 0} V{0 0 1} V{1 0 0} V{2 2 2})"), "return Transform(Vector3(0, 0, 0), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(2, 2, 2))\n"); assert_eq!(parse_compile_and_output("(Color 0)"), "return Color(0)\n"); assert_eq!(parse_compile_and_output("(Color 0 0 0)"), "return Color(0, 0, 0)\n"); assert_eq!(parse_compile_and_output("(Color 0 0 0 1)"), "return Color(0, 0, 0, 1)\n"); } #[test] pub fn nodepath_constructor_test() { assert_eq!(parse_compile_and_output(r#"(let ((x "A")) (NodePath x))"#), r#"var x = "A" return GDLisp._NodePath(x) "#); assert_eq!(parse_compile_and_output(r#"(NodePath 0)"#), "return GDLisp._NodePath(0)\n"); assert_eq!(parse_compile_and_output(r#"(NodePath "a")"#), "return @\"a\"\n"); } #[test] pub fn str_running_test() { assert_eq!(parse_and_run("((print (str 1 2 #t)))"), "\n12True\n"); } #[test] pub fn str_running_test_indirect() { assert_eq!(parse_and_run("((print (funcall #'str 1 2 #t)))"), "\n12True\n"); } #[test] pub fn printerr_running_test() { let StringOutput { stdout, stderr } = parse_and_run_with_stderr("((printerr \"printerr_running_test OUTPUT\"))"); assert_eq!(stdout, "\n"); assert!(stderr.contains("printerr_running_test OUTPUT")); } #[test] pub fn printerr_running_test_indirect() { let StringOutput { stdout, stderr } = parse_and_run_with_stderr("((funcall #'printerr \"printerr_running_test_indirect OUTPUT\"))"); assert_eq!(stdout, "\n"); assert!(stderr.contains("printerr_running_test_indirect OUTPUT")); } #[test] pub fn printraw_running_test() { assert_eq!(parse_and_run("((printraw 1 2 #t))"), "\n12True"); // Note: No \n at end } #[test] pub fn printraw_running_test_indirect() { assert_eq!(parse_and_run("((funcall #'printraw 1 2 #t))"), "\n12True"); // Note: No \n at end } #[test] pub fn print_running_test() { assert_eq!(parse_and_run("((print 1 2 #t))"), "\n12True\n"); } #[test] pub fn range_running_test() { assert_eq!(parse_and_run("((print (range 5)) (print (range 1 5)) (print (range 1 5 2)) (print (range 5 1 -1)))"), "\n[0, 1, 2, 3, 4]\n[1, 2, 3, 4]\n[1, 3]\n[5, 4, 3, 2]\n"); } #[test] pub fn range_running_test_indirect() { assert_eq!(parse_and_run("((print (funcall #'range 5)) (print (funcall #'range 1 5)) (print (funcall #'range 1 5 2)) (print (funcall #'range 5 1 -1)))"), "\n[0, 1, 2, 3, 4]\n[1, 2, 3, 4]\n[1, 3]\n[5, 4, 3, 2]\n"); } #[test] pub fn print_running_test_indirect() { assert_eq!(parse_and_run("((funcall #'print 1 2 #t))"), "\n12True\n"); } #[test] pub fn deep_equal_test() { // deep_equal is only available in Godot 3.5 and later, so we need // to disable this test if we're running on an older version. let godot_version = get_godot_version().unwrap().version; if godot_version >= Version::new(3, 5, 0) { assert_eq!(parse_and_run("((print (deep-equal {\"a\" 1} {\"a\" 1})))"), "\nTrue\n"); } } #[test] pub fn cons_accessor_test_1() { assert_eq!(parse_and_run("((print (quote (1 . 2)):car))"), "\n1\n"); assert_eq!(parse_and_run("((print (quote (1 . 2)):cdr))"), "\n2\n"); } #[test] pub fn cons_accessor_test_2() { assert_eq!(parse_and_run("((print (quote ((1 . 2) . (3 . 4))):caar))"), "\n1\n"); assert_eq!(parse_and_run("((print (quote ((1 . 2) . (3 . 4))):cadr))"), "\n2\n"); assert_eq!(parse_and_run("((print (quote ((1 . 2) . (3 . 4))):cdar))"), "\n3\n"); assert_eq!(parse_and_run("((print (quote ((1 . 2) . (3 . 4))):cddr))"), "\n4\n"); } #[test] pub fn cons_accessor_test_3() { assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):caaar))"), "\n1\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):caadr))"), "\n2\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):cadar))"), "\n3\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):caddr))"), "\n4\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):cdaar))"), "\n5\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):cdadr))"), "\n6\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):cddar))"), "\n7\n"); assert_eq!(parse_and_run("((print (quote (((1 . 2) . (3 . 4)) . ((5 . 6) . (7 . 8)))):cdddr))"), "\n8\n"); } #[test] pub fn nodepath_running_test() { assert_eq!(parse_and_run("((let ((x (NodePath (let ((y \"foo/bar\")) y)))) (print (x:get-name 0)) (print (x:get-name 1))))"), "\nfoo\nbar\n"); assert_eq!(parse_and_run("((let ((x (NodePath \"foo/bar\"))) (print (x:get-name 0)) (print (x:get-name 1))))"), "\nfoo\nbar\n"); } #[test] pub fn vector_constant_print_test() { assert_eq!(parse_and_run("((print Vector2:LEFT) (print Vector3:AXIS_Z))"), "\n(-1, 0)\n2\n"); } #[test] pub fn object_constant_value_test() { // We had to hard-code these values in, so validate that they match // what Godot expects. assert_eq!(parse_and_run(r#"((print (= CONNECT_DEFERRED ConnectFlags:DEFERRED)) (print (= CONNECT_PERSIST ConnectFlags:PERSIST)) (print (= CONNECT_ONESHOT ConnectFlags:ONESHOT)) (print (= CONNECT_REFERENCE_COUNTED ConnectFlags:REFERENCE_COUNTED)) (print (= NOTIFICATION_POSTINITIALIZE Notification:POSTINITIALIZE)) (print (= NOTIFICATION_PREDELETE Notification:PREDELETE)))"#), "\nTrue\nTrue\nTrue\nTrue\nTrue\nTrue\n"); } #[test] pub fn thread_macro_test() { assert_eq!(parse_compile_and_output("(-> 1 foo1 (foo1) (foo2 2))"), "return foo2(foo1(foo1(1)), 2)\n"); assert_eq!(parse_compile_and_output("(-> 1)"), "return 1\n"); } #[test] pub fn last_thread_macro_test() { assert_eq!(parse_compile_and_output("(->> 1 foo1 (foo1) (foo2 2))"), "return foo2(2, foo1(foo1(1)))\n"); assert_eq!(parse_compile_and_output("(->> 1)"), "return 1\n"); } #[test] pub fn as_thread_macro_test_1() { assert_eq!(parse_compile_and_output("(as-> 1 v)"), "return 1\n"); } #[test] pub fn as_thread_macro_test_2() { assert_eq!(parse_compile_and_output("(as-> 1 v (foo1 v) (foo2 v 2) (foo2 2 v))"), r#"var v = 1 var v_0 = foo1(v) var v_1 = foo2(v_0, 2) return foo2(2, v_1) "#); } #[test] pub fn as_thread_macro_test_3() { assert_eq!(parse_compile_and_output("(as-> 1 v (foo1 v) (foo2 v v) (foo2 2 v))"), r#"var v = 1 var v_0 = foo1(v) var v_1 = foo2(v_0, v_0) return foo2(2, v_1) "#); } #[test] pub fn list_is_not_shared_running_test() { assert_eq!(parse_and_run(r#" ((let* ((first-list (list 1 2 3 4)) (second-list (apply #'list first-list))) (set first-list:car 0) (print (list->array first-list)) (print (list->array second-list))))"#), "\n[0, 2, 3, 4]\n[1, 2, 3, 4]\n"); } #[test] pub fn array_find_test() { assert_eq!(parse_and_run(r#" ((let ((arr [1 2 3 4 5 6])) (print (array/find (lambda (x) (> x 3)) arr)) (print (array/find (lambda (x) (> x 10)) arr))))"#), "\n4\nNull\n"); } #[test] pub fn list_find_test() { assert_eq!(parse_and_run(r#" ((let ((list '(1 2 3 4 5 6))) (print (list/find (lambda (x) (> x 3)) list)) (print (list/find (lambda (x) (> x 10)) list))))"#), "\n4\nNull\n"); } #[test] pub fn dict_find_test() { assert_eq!(parse_and_run(r#" ((let ((dict {1 2 3 4 5 6})) (print (dict/find (lambda (k v) (> k 1)) dict)) (print (dict/find (lambda (k v) (> v 1)) dict)) (print (dict/find (lambda (k v) (> k 100)) dict))))"#), "\n3\n1\nNull\n"); } #[test] pub fn vector_map_test() { assert_eq!(parse_and_run("((let ((a V{1 3})) (print (vector/map (lambda (x) (+ x 1)) a))))"), "\n(2, 4)\n"); assert_eq!(parse_and_run("((let ((a V{1 3 10})) (print (vector/map (lambda (x) (+ x 2)) a))))"), "\n(3, 5, 12)\n"); } #[test] pub fn vector_map_two_arguments_test() { assert_eq!(parse_and_run("((let ((a V{1 3}) (b V{10 30})) (print (vector/map #'+ a b))))"), "\n(11, 33)\n"); assert_eq!(parse_and_run("((let ((a V{1 3 10}) (b V{9 8 7})) (print (vector/map #'+ a b))))"), "\n(10, 11, 17)\n"); } #[test] pub fn array_concat_running_test() { assert_eq!(parse_and_run("((print (array/concat)) (print (array/concat [1 2] [3 4])))"), "\n[]\n[1, 2, 3, 4]\n"); } // TODO Test gensym at runtime once we can pretty-print symbols ================================================ FILE: tests/test/class_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use gdlisp::ir::identifier::ClassNamespace; use gdlisp::ir::modifier::{ParseError as ModifierParseError, ParseErrorF as ModifierParseErrorF}; use gdlisp::compile::args::Expecting; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use super::common::*; #[test] pub fn empty_class_test() { assert_eq!(parse_compile_decl("((defclass ClassName (Node)))"), r#"extends Reference class ClassName extends Node: func _init(): pass "#); } #[test] pub fn simple_class_test_1() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn _init (y)) (defn foo () 2)))"), r#"extends Reference class ClassName extends Node: func _init(y): pass var x func foo(): return 2 "#); } #[test] pub fn simple_class_test_2() { assert_eq!(parse_compile_decl("((defclass ClassName () (defvar x) (defn _init (y)) (defn foo () 2)))"), r#"extends Reference class ClassName extends Reference: func _init(y): pass var x func foo(): return 2 "#); } #[test] pub fn parent_constructor_class_test_1() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn _init (y) (super y y)) (defn foo () 2)))"), r#"extends Reference class ClassName extends Node: func _init(y).(y, y): pass var x func foo(): return 2 "#); } #[test] pub fn parent_constructor_class_test_2() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn _init (y) (super (progn y))) (defn foo () 2)))"), r#"extends Reference class ClassName extends Node: func _init(y).(y): pass var x func foo(): return 2 "#); } #[test] pub fn parent_constructor_class_test_3() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn _init (y) (super (if y 1 2))) (defn foo () 2)))"), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var y func _init(y): self.y = y self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): var _cond = null if y: _cond = 1 else: if true: _cond = 2 else: _cond = null return _cond func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class ClassName extends Node: func _init(y).(GDLisp.sys_DIV_funcall(_LambdaBlock.new(y), null)): pass var x func foo(): return 2 "#); } #[test] pub fn parent_constructor_class_test_4() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn _init (@x y) (super (if y 1 2))) (defn foo () 2)))"), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var y func _init(y): self.y = y self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): var _cond = null if y: _cond = 1 else: if true: _cond = 2 else: _cond = null return _cond func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class ClassName extends Node: func _init(x_0, y).(GDLisp.sys_DIV_funcall(_LambdaBlock.new(y), null)): self.x = x_0 var x func foo(): return 2 "#); } #[test] pub fn parent_constructor_class_test_5() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x-x) (defn _init (@x-x y) (super (if y 1 2))) (defn foo () 2)))"), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var y func _init(y): self.y = y self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): var _cond = null if y: _cond = 1 else: if true: _cond = 2 else: _cond = null return _cond func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class ClassName extends Node: func _init(x_x_0, y).(GDLisp.sys_DIV_funcall(_LambdaBlock.new(y), null)): self.x_x = x_x_0 var x_x func foo(): return 2 "#); } #[test] pub fn parent_constructor_class_running_test() { assert_eq!(parse_and_run("((defclass A (Reference) (defvar x) (defn _init (x) (set self:x x))) (defclass B (A) (defn _init (y) (super (if y 1 2)))) (print (B:new #t):x))"), "\n1\n"); } #[test] pub fn super_call_in_class_test_1() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn foo () (super:foo))))"#), r#"extends Reference class Foo extends Reference: func _init(): pass func foo(): return .foo() "#); } #[test] pub fn super_call_in_class_test_2() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn foo () (super:bar)) (defn bar () (super:foo))))"#), r#"extends Reference class Foo extends Reference: func _init(): pass func foo(): return .bar() func bar(): return .foo() "#); } #[test] pub fn super_call_in_class_test_3() { assert_eq!(parse_compile_decl(r#"((defclass Foo () main (defn foo () (super:foo)) (defn bar () (super:foo))))"#), r#"extends Reference func _init(): pass func foo(): return .foo() func bar(): return .foo() "#); } #[test] pub fn super_call_in_class_test_4() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn _init () (super:foo))))"#), r#"extends Reference class Foo extends Reference: func _init(): .foo() "#); } #[test] pub fn super_call_in_class_test_5() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn foo-bar () (super:foo-bar))))"#), r#"extends Reference class Foo extends Reference: func _init(): pass func foo_bar(): return .foo_bar() "#); } #[test] pub fn super_call_closed_in_class_test_1() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn foo () (lambda () (super:foo)))))"#), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var _self_0 func _init(_self_0): self._self_0 = _self_0 self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return _self_0.__gdlisp_super_1() func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class Foo extends Reference: func _init(): pass func foo(): return _LambdaBlock.new(self) func __gdlisp_super_1(): return .foo() "#); } #[test] pub fn super_call_closed_in_class_test_2() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn foo () (lambda () (super:foo) (super:bar)))))"#), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var _self_0 func _init(_self_0): self._self_0 = _self_0 self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): _self_0.__gdlisp_super_1() return _self_0.__gdlisp_super_2() func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class Foo extends Reference: func _init(): pass func foo(): return _LambdaBlock.new(self) func __gdlisp_super_1(): return .foo() func __gdlisp_super_2(): return .bar() "#); } #[test] pub fn super_call_closed_in_class_test_3() { assert_eq!(parse_compile_decl(r#"((defclass Foo () (defn foo () (lambda () (super:foo-bar) (super:bar-bar)))))"#), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var _self_0 func _init(_self_0): self._self_0 = _self_0 self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): _self_0.__gdlisp_super_1() return _self_0.__gdlisp_super_2() func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class Foo extends Reference: func _init(): pass func foo(): return _LambdaBlock.new(self) func __gdlisp_super_1(): return .foo_bar() func __gdlisp_super_2(): return .bar_bar() "#); } #[test] pub fn member_var_class_test_1() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn get-x () self:x)))"), r#"extends Reference class ClassName extends Node: func _init(): pass var x func get_x(): return self.x "#); } #[test] pub fn member_var_class_test_2() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"), r#"extends Reference class ClassName extends Node: func _init(x): self.x = x var x func get_x(): return self.x "#); } #[test] pub fn member_var_class_test_3() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvar x 999) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"), r#"extends Reference class ClassName extends Node: func _init(x): self.x = x var x = 999 func get_x(): return self.x "#); } #[test] pub fn member_var_class_test_4() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) main (defvar x (export int 1 2)) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"), r#"extends Node func _init(x): self.x = x export(int, 1, 2) var x func get_x(): return self.x "#); } #[test] pub fn member_var_class_test_5() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defvar x "foo" (export String "foo" "bar")) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"#), r#"extends Node func _init(x): self.x = x export(String, "foo", "bar") var x = "foo" func get_x(): return self.x "#); } #[test] pub fn member_var_class_test_6() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvars x y z)))"), r#"extends Reference class ClassName extends Node: func _init(): pass var x var y var z "#); } #[test] pub fn member_var_class_test_7() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defvar x "foo" (export String "foo" "bar")) (defn _init (x) (set @x x)) (defn get-x () @x)))"#), r#"extends Node func _init(x): self.x = x export(String, "foo", "bar") var x = "foo" func get_x(): return self.x "#); } #[test] pub fn init_member_var_class_test_1() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvars x y z) (defn _init (@x))))"), r#"extends Reference class ClassName extends Node: func _init(x_0): self.x = x_0 var x var y var z "#); } #[test] pub fn init_member_var_class_test_2() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvars x y) (defvar z 10) (defn _init (@x @y))))"), r#"extends Reference class ClassName extends Node: func _init(x_0, y_1): self.x = x_0 self.y = y_1 var x var y var z = 10 "#); } #[test] pub fn init_member_var_class_test_3() { // sys/split is just to force the variable initialization to go into // the constructor rather than be inline on the 'var' line. assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defvars x y) (defvar z (sys/split 3)) (defn _init (@x @y))))"), r#"extends Reference class ClassName extends Node: func _init(x_0, y_1): var _split = 3 self.z = _split self.x = x_0 self.y = y_1 var x var y var z "#); } #[test] pub fn ready_member_var_class_test_1() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defvar x "foo" onready)))"#), r#"extends Node func _init(): pass onready var x = "foo" "#); } #[test] pub fn ready_member_var_class_test_2() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defvar x "foo" (export String) onready)))"#), r#"extends Node func _init(): pass export(String) onready var x = "foo" "#); } #[test] pub fn complicated_member_var_class_test() { assert_eq!( parse_compile_decl("((defclass ClassName (Node) main (defvar x (if 1 2 3)) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"), r#"extends Node func _init(x): var _cond = null if 1: _cond = 2 else: if true: _cond = 3 else: _cond = null self.x = _cond self.x = x var x func get_x(): return self.x "#); } #[test] pub fn complicated_ready_member_var_class_test() { assert_eq!( parse_compile_decl("((defclass ClassName (Node) main (defvar x (if 1 2 3) onready) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"), r#"extends Node func _init(x): self.x = x var x func get_x(): return self.x func _ready(): var _cond = null if 1: _cond = 2 else: if true: _cond = 3 else: _cond = null self.x = _cond "#); } #[test] pub fn bad_member_var_class_test_1() { // Can't have export on inner class assert_eq!( parse_compile_decl_err("((defclass ClassName (Node) (defvar x (export int)) (defn _init (x) (set self:x x)) (defn get-x () self:x)))"), Err(PError::from(GDError::new(GDErrorF::ExportOnInnerClassVar(String::from("x")), SourceOffset(28)))), ); } #[test] pub fn bad_self_static_ref_class_test() { // Can't reference self from static context assert_eq!( parse_compile_decl_err("((defclass ClassName (Node) (defn get-self () static self)))"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("self")), SourceOffset(53)))), ); } #[test] pub fn bad_member_const_class_test() { // Consts must be initialized let result = parse_compile_decl_err("((defclass ClassName (Node) (defconst x)))"); assert_eq!( result, Err(PError::GDError(GDError::new(GDErrorF::WrongNumberArgs(String::from("defconst"), Expecting::exactly(2), 1), SourceOffset(28)))), ); } #[test] pub fn bad_static_constructor_class_test() { // Constructors cannot be static assert_eq!( parse_compile_decl_err("((defclass ClassName (Node) (defn _init () static)))"), Err(PError::from(GDError::new(GDErrorF::StaticConstructor, SourceOffset(29)))), ); } #[test] pub fn bad_nullargs_constructor_class_test() { // Constructors cannot be sys/nullargs assert_eq!( parse_compile_decl_err("((defclass ClassName (Node) (defn _init () sys/nullargs)))"), Err(PError::from(GDError::new(GDErrorF::NullargsConstructor, SourceOffset(29)))), ); } #[test] pub fn bad_super_in_instance_function_test() { // Can't have super in non-constructor method assert_eq!( parse_compile_decl_err("((defclass ClassName (Node) (defn foo () (super 1))))"), Err(PError::from(GDError::new(GDErrorF::BadSuperCall(String::from("(init)")), SourceOffset(42)))), ); } #[test] pub fn signal_class_test_1() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defsignal my-signal)))"), r#"extends Reference class ClassName extends Node: func _init(): pass signal my_signal "#); } #[test] pub fn signal_class_test_2() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defsignal my-signal ())))"), r#"extends Reference class ClassName extends Node: func _init(): pass signal my_signal "#); } #[test] pub fn signal_class_test_3() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defsignal my-signal (foo bar))))"), r#"extends Reference class ClassName extends Node: func _init(): pass signal my_signal(foo, bar) "#); } #[test] pub fn signal_class_test_4() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defsignal my-signal (foo bar)) (defsignal my-other-signal)))"), r#"extends Reference class ClassName extends Node: func _init(): pass signal my_signal(foo, bar) signal my_other_signal "#); } #[test] pub fn const_in_class_test() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defconst x 1)))"), r#"extends Reference class ClassName extends Node: func _init(): pass const x = 1 "#); } #[test] pub fn const_in_main_class_test() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) main (defconst x 1)))"), r#"extends Node func _init(): pass const x = 1 "#); } #[test] pub fn static_in_class_test() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defn foo () static 1)))"), r#"extends Reference class ClassName extends Node: func _init(): pass static func foo(): return 1 "#); } #[test] pub fn nullargs_in_class_test_1() { // No effect since there are no arguments. assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defn foo () sys/nullargs 1)))"), r#"extends Reference class ClassName extends Node: func _init(): pass func foo(): return 1 "#); } #[test] pub fn nullargs_in_class_test_2() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defn foo (x y z) sys/nullargs 1)))"), r#"extends Reference class ClassName extends Node: func _init(): pass func foo(x = null, y = null, z = null): return 1 "#); } #[test] pub fn nullargs_in_class_test_3() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defn foo (x y z) sys/nullargs static 1)))"), r#"extends Reference class ClassName extends Node: func _init(): pass static func foo(x = null, y = null, z = null): return 1 "#); } #[test] pub fn nullargs_in_class_test_4() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) (defn foo (x y z) static sys/nullargs 1)))"), r#"extends Reference class ClassName extends Node: func _init(): pass static func foo(x = null, y = null, z = null): return 1 "#); } #[test] pub fn static_in_main_class_test() { assert_eq!(parse_compile_decl("((defclass ClassName (Node) main (defn foo () static 1)))"), r#"extends Node func _init(): pass static func foo(): return 1 "#); } #[test] pub fn simple_self_closure_class_test() { assert_eq!(parse_compile_decl("((defclass Foo (Node) (defn test () (lambda () self))))"), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var _self_0 func _init(_self_0): self._self_0 = _self_0 self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return _self_0 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class Foo extends Node: func _init(): pass func test(): return _LambdaBlock.new(self) "#); } #[test] pub fn labels_self_closure_class_test() { assert_eq!(parse_compile_decl("((defclass Foo (Node) (defn test () (labels ((foo (x) (foo self))) (foo 76)))))"), r#"extends Reference class _Labels extends Reference: var _self_0 func _init(_self_0): self._self_0 = _self_0 func _fn_foo_1(x): return _fn_foo_1(_self_0) class Foo extends Node: func _init(): pass func test(): var _locals = _Labels.new(self) return _locals._fn_foo_1(76) "#); } #[test] pub fn labels_self_closure_class_with_contrived_const_test() { assert_eq!(parse_compile_decl("((defconst _Labels 10) (defclass Foo (Node) (defn test () (labels ((foo (x) (foo self))) (foo 76)))))"), r#"extends Reference const _Labels = 10 class _Labels_0 extends Reference: var _self_0 func _init(_self_0): self._self_0 = _self_0 func _fn_foo_1(x): return _fn_foo_1(_self_0) class Foo extends Node: func _init(): pass func test(): var _locals = _Labels_0.new(self) return _locals._fn_foo_1(76) "#); } #[test] pub fn simple_self_run_class_test_1() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defvar x) (defn _init (x) (set self:x x)) (defn double () (* self:x 2))) (let ((foo (Foo:new 100))) (print (foo:double)) (set foo:x 101) (print (foo:double)))) "#), "\n200\n202\n"); } #[test] pub fn simple_self_run_class_test_2() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defvar x) (defn _init (x) (set @x x)) (defn double () (* @x 2))) (let ((foo (Foo:new 100))) (print (foo:double)) (set foo:x 101) (print (foo:double)))) "#), "\n200\n202\n"); } #[test] pub fn simple_self_run_class_test_3() { // Mixing self and @ assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defvar x) (defn _init (x) (set @x x)) (defn double () (* self:x 2))) (let ((foo (Foo:new 100))) (print (foo:double)) (set foo:x 101) (print (foo:double)))) "#), "\n200\n202\n"); } #[test] pub fn self_with_closure_run_class_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defvar x) (defn _init () (set self:x 1)) (defn increment () (lambda () (set self:x (+ self:x 1))))) (let ((fn (let ((tmp (Foo:new))) (tmp:increment)))) (print (funcall fn)) (print (funcall fn)) (print (funcall fn)))) "#), "\n2\n3\n4\n"); } #[test] pub fn macro_in_class_test_1() { assert_eq!(parse_compile_decl(r#" ((defmacro add-one (x) (+ x 1)) (defn example (x) x) (defclass Foo (Reference) (defn _init () (example (add-one 2)))))"#), r#"extends Reference static func add_one(x): return x + 1 static func example(x): return x class Foo extends Reference: func _init(): __gdlisp_outer_class_0.example(3) var __gdlisp_outer_class_0 = load("res://TEST.gd") "#); } #[test] pub fn macro_in_class_test_2() { assert_eq!(parse_and_run(r#" ((defmacro declare-function (name) `(defn ,name () 99)) (defclass Foo (Reference) (declare-function fn1) (declare-function fn2)) (let ((foo (Foo:new))) (print (foo:fn1)) (print (foo:fn2))))"#), "\n99\n99\n"); } #[test] pub fn macro_in_class_test_3() { assert_eq!(parse_and_run(r#" ((defmacro declare-functions () '(progn (defn a () 1) (defn b () 2))) (defclass Foo (Reference) (declare-functions)) (let ((foo (Foo:new))) (print (foo:a)) (print (foo:b))))"#), "\n1\n2\n"); } #[test] pub fn macro_uses_class_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defvar x)) (defmacro through-foo () (let ((foo (Foo:new))) (set foo:x 5) foo:x)) (print (through-foo)))"#), "\n5\n"); } #[test] pub fn reference_static_test_1() { assert_eq!(parse_compile_decl("((defn foo ()) (defclass Foo (Node2D) (defn example () (foo))))"), r#"extends Reference static func foo(): return null class Foo extends Node2D: func _init(): pass func example(): return __gdlisp_outer_class_0.foo() var __gdlisp_outer_class_0 = load("res://TEST.gd") "#); } #[test] pub fn reference_static_test_2() { assert_eq!(parse_compile_decl("((defn foo ()) (defclass Foo (Node2D) main (defn example () (foo))))"), r#"extends Node2D static func foo(): return null func _init(): pass func example(): return foo() "#); } #[test] pub fn reference_static_test_3() { assert_eq!(parse_compile_decl("((defn foo ()) (defclass Foo (Node2D) (defn example () static (foo))))"), r#"extends Reference static func foo(): return null class Foo extends Node2D: func _init(): pass static func example(): return load("res://TEST.gd").foo() "#); } #[test] pub fn reference_static_test_4() { assert_eq!(parse_compile_decl("((defn foo ()) (defclass Foo (Node2D) main (defn example () static (foo))))"), r#"extends Node2D static func foo(): return null func _init(): pass static func example(): return foo() "#); } #[test] pub fn main_class_test_1() { assert_eq!(parse_compile_decl("((defclass Foo (Node2D) main))"), r#"extends Node2D func _init(): pass "#); } #[test] pub fn main_class_test_2() { assert_eq!(parse_compile_decl("((defclass Foo (Node2D) main (defn foo () 1)))"), r#"extends Node2D func _init(): pass func foo(): return 1 "#); } #[test] pub fn main_class_test_3() { assert_eq!(parse_compile_decl("((defclass Foo (Node2D) main (defn foo () 1)) (defn run () Foo))"), r#"extends Node2D func _init(): pass func foo(): return 1 static func run(): return load("res://TEST.gd") "#); } #[test] pub fn main_class_test_4() { assert_eq!(parse_compile_decl("((defclass Foo () main))"), r#"extends Reference func _init(): pass "#); } #[test] pub fn macro_uses_main_class_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) main (defvar x)) (defmacro through-foo () (let ((foo (Foo:new))) (set foo:x 5) foo:x)) (print (through-foo)))"#), "\n5\n"); } #[test] pub fn reference_to_const_in_class_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defconst CONSTANT 100)) (print Foo:CONSTANT) (print (Foo:new):CONSTANT))"#), "\n100\n100\n"); } #[test] pub fn reference_to_static_in_class_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defn foo () static 98)) (print (Foo:foo)) (print ((Foo:new):foo)))"#), "\n98\n98\n"); } #[test] pub fn reference_to_outer_in_class_test_1() { let output = parse_and_run(r#" ((defn outer () 100) (defclass Foo (Reference) (defn foo () (outer))) (print ((Foo:new):foo)))"#); assert_eq!(output, "\n100\n"); } #[test] pub fn reference_to_outer_in_class_test_2() { let output = parse_and_run(r#" ((defn outer () 100) (defclass Foo (Reference) (defn foo () static (outer))) (print ((Foo:new):foo)) (print (Foo:foo)))"#); assert_eq!(output, "\n100\n100\n"); } #[test] pub fn reference_to_outer_in_class_test_3() { let output = parse_and_run(r#" ((defn outer () 100) (defclass Foo (Reference) main (defn foo () (outer))) (print ((Foo:new):foo)))"#); assert_eq!(output, "\n100\n"); } #[test] pub fn reference_to_outer_in_class_test_4() { let output = parse_and_run(r#" ((defn outer () 100) (defclass Foo (Reference) main (defn foo () static (outer))) (print ((Foo:new):foo)) (print (Foo:foo)))"#); assert_eq!(output, "\n100\n100\n"); } #[test] pub fn constructor_with_parent_class_test_1() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defn _init (x) (print x))) (defclass Bar (Foo) (defn _init (x) (super (+ x 1)))) (Bar:new 10))"#); assert_eq!(output, "\n11\n"); } #[test] pub fn constructor_with_parent_class_test_2() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defn _init (x) (print x))) (defclass Bar (Foo) (defn _init (x) (super (if (> x 10) (+ x 1) (- x 1))))) (Bar:new 20) (Bar:new 3))"#); assert_eq!(output, "\n21\n2\n"); } #[test] pub fn constructor_with_parent_class_test_3() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defvar z) (defn _init (z) (set self:z z))) (defclass Bar (Foo) (defn _init () (super self))) (let ((bar (Bar:new))) (print (= bar bar:z)) (set bar:z nil)))"#); assert_eq!(output, "\nTrue\n"); } #[test] pub fn constructor_with_parent_class_test_4() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defvar z) (defn _init (z) (set self:z z))) (defclass Bar (Foo) (defn _init (x) (super (lambda (y) (+ x y))))) (let ((bar (Bar:new 64))) (print (funcall bar:z 1)) (print (funcall bar:z -1))))"#); assert_eq!(output, "\n65\n63\n"); } #[test] pub fn constructor_with_parent_class_test_5() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defvar z) (defn _init (z) (set self:z z))) (defclass Bar (Foo) (defn _init (x) (super (flet ((f (y) (+ x y))) #'f)))) (let ((bar (Bar:new 64))) (print (funcall bar:z 1)) (print (funcall bar:z -1))))"#); assert_eq!(output, "\n65\n63\n"); } #[test] pub fn get_node_on_self_class_test() { assert_eq!(parse_compile_decl(r#" ((defclass Foo (Spatial) main (defn test () $Target/Node))) "#), r#"extends Spatial func _init(): pass func test(): return $Target/Node "#); } #[test] pub fn get_node_on_explicit_target_class_test() { assert_eq!(parse_compile_decl(r#" ((defclass Foo (Spatial) main (defn test (x) x:$Target/Node))) "#), r#"extends Spatial func _init(): pass func test(x): return x.get_node("Target/Node") "#); } #[test] pub fn nonsense_modifier_class_test_1() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo (Node) main main))"#), Err(PError::from(ModifierParseError::new(ModifierParseErrorF::UniquenessError(String::from("main")), SourceOffset(27)))), ); } #[test] pub fn nonsense_modifier_class_test_2() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo (Node) public public))"#), Err(PError::from(ModifierParseError::new(ModifierParseErrorF::UniquenessError(String::from("visibility")), SourceOffset(29)))), ); } #[test] pub fn nonsense_modifier_class_test_3() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo (Node) public private))"#), Err(PError::from(ModifierParseError::new(ModifierParseErrorF::UniquenessError(String::from("visibility")), SourceOffset(29)))), ); } #[test] pub fn nonsense_modifier_class_test_4() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo (Node) public (defn example () static static)))"#), Err(PError::from(ModifierParseError::new(ModifierParseErrorF::UniquenessError(String::from("static")), SourceOffset(53)))), ); } #[test] pub fn duplicate_main_class_test() { assert_eq!( parse_compile_decl_err("((defclass Foo (Node) main) (defclass Bar (Node) main))"), Err(PError::from(GDError::new(GDErrorF::DuplicateMainClass, SourceOffset(29)))), ); } #[test] pub fn no_self_in_scope_test() { assert_eq!( parse_compile_and_output_err("@test"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("self")), SourceOffset(0)))), ); } #[test] pub fn duplicate_constructor_test() { assert_eq!( parse_compile_decl_err("((defclass Foo (Node) (defn _init ()) (defn _init ())))"), Err(PError::from(GDError::new(GDErrorF::DuplicateConstructor, SourceOffset(39)))), ); } #[test] pub fn class_setget_test_1() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) (defvar x)))"#), r#"extends Reference class ClassName extends Node: func _init(): pass var x "#); } #[test] pub fn class_setget_test_2() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) (defn (get x) () 10)))"#), r#"extends Reference class ClassName extends Node: func _init(): pass func __gdlisp_get_x(): return 10 func __gdlisp_set_x(_unused): push_error("Cannot assign to nonexistent field \'x\'") var x setget __gdlisp_set_x, __gdlisp_get_x "#); } #[test] pub fn class_setget_test_3() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) (defn (set x) (a))))"#), r#"extends Reference class ClassName extends Node: func _init(): pass func __gdlisp_set_x(a): pass func __gdlisp_get_x(): push_error("Cannot access nonexistent field \'x\'") var x setget __gdlisp_set_x, __gdlisp_get_x "#); } #[test] pub fn class_setget_test_4() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) (defn (set x) (a)) (defn (get x) () 10)))"#), r#"extends Reference class ClassName extends Node: func _init(): pass func __gdlisp_set_x(a): pass func __gdlisp_get_x(): return 10 var x setget __gdlisp_set_x, __gdlisp_get_x "#); } #[test] pub fn class_setget_test_5() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defn (set x) (a)) (defn (get x) () 10)))"#), r#"extends Node func _init(): pass func __gdlisp_set_x(a): pass func __gdlisp_get_x(): return 10 var x setget __gdlisp_set_x, __gdlisp_get_x "#); } #[test] pub fn class_setget_test_6() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defn (set x-y) (a)) (defn (get x-y) () 10)))"#), r#"extends Node func _init(): pass func __gdlisp_set_x_y(a): pass func __gdlisp_get_x_y(): return 10 var x_y setget __gdlisp_set_x_y, __gdlisp_get_x_y "#); } #[test] pub fn class_setget_test_7() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defn (set x-y) (a))))"#), r#"extends Node func _init(): pass func __gdlisp_set_x_y(a): pass func __gdlisp_get_x_y(): push_error("Cannot access nonexistent field \'x_y\'") var x_y setget __gdlisp_set_x_y, __gdlisp_get_x_y "#); } #[test] pub fn class_setget_test_8() { assert_eq!(parse_compile_decl(r#"((defclass ClassName (Node) main (defn (get x-y) () 10)))"#), r#"extends Node func _init(): pass func __gdlisp_get_x_y(): return 10 func __gdlisp_set_x_y(_unused): push_error("Cannot assign to nonexistent field \'x_y\'") var x_y setget __gdlisp_set_x_y, __gdlisp_get_x_y "#); } #[test] pub fn class_setget_conflict_test_1() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defn (set x) (a)) (defn (set x) (b))))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("__gdlisp_set_x")), SourceOffset(130))))); } #[test] pub fn class_setget_conflict_test_2() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defn (get x) ()) (defn (get x) ())))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("__gdlisp_get_x")), SourceOffset(129))))); } #[test] pub fn class_setget_conflict_test_3() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defvar x) (defn (get x) ())))"#), Err(PError::from(GDError::new(GDErrorF::FieldAccessorConflict(String::from("x")), SourceOffset(70))))); } #[test] pub fn class_setget_conflict_test_4() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defvar x) (defn (set x) (a))))"#), Err(PError::from(GDError::new(GDErrorF::FieldAccessorConflict(String::from("x")), SourceOffset(70))))); } #[test] pub fn class_setget_bad_signature_1() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defn (get x) (a))))"#), Err(PError::from(GDError::new(GDErrorF::BadGetterArguments(String::from("x")), SourceOffset(70))))); } #[test] pub fn class_setget_bad_signature_2() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defn (get x) () static)))"#), Err(PError::from(GDError::new(GDErrorF::BadGetterArguments(String::from("x")), SourceOffset(70))))); } #[test] pub fn class_setget_bad_signature_3() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defn (set x) (a) static)))"#), Err(PError::from(GDError::new(GDErrorF::BadSetterArguments(String::from("x")), SourceOffset(70))))); } #[test] pub fn class_setget_bad_signature_4() { assert_eq!(parse_compile_decl_err(r#"((defclass ClassName (Node) (defn (set x) ())))"#), Err(PError::from(GDError::new(GDErrorF::BadSetterArguments(String::from("x")), SourceOffset(70))))); } #[test] pub fn class_setget_runner_test_1() { assert_eq!(parse_and_run(r#"((defclass ClassName (Reference) (defvar private-field) (defn _init () (set @private-field 1)) (defn (set x) (a) (set @private-field a)) (defn (get x) () @private-field)) (let ((foo (ClassName:new))) (set foo:x 92) (print foo:x)))"#), "\n92\n"); } #[test] pub fn class_setget_runner_test_2() { assert_eq!(parse_and_run(r#"((defclass ClassName (Reference) main (defvar private-field) (defn _init () (set @private-field 1)) (defn (set x) (a) (set @private-field a)) (defn (get x) () @private-field)) (let ((foo (ClassName:new))) (set foo:x 92) (print foo:x)))"#), "\n92\n"); } #[test] pub fn super_call_runner_test_1() { assert_eq!(parse_and_run(r#"((defclass Foo (Reference) (defn foo () 99)) (defclass Bar (Foo) (defn foo () 101) (defn call-foo () (super:foo))) (let ((bar (Bar:new))) (print (bar:foo)) (print (bar:call-foo))))"#), "\n101\n99\n"); } #[test] pub fn super_call_runner_test_2() { assert_eq!(parse_and_run(r#"((defclass Foo (Reference) (defn foo () 99)) (defclass Bar (Foo) (defn foo () 101) (defn call-foo () (lambda () (super:foo)))) (let* ((bar (Bar:new)) (delegator (bar:call-foo))) (print (bar:foo)) (print (funcall delegator))))"#), "\n101\n99\n"); } #[test] pub fn super_call_runner_test_3() { assert_eq!(parse_and_run(r#"((defclass Foo (Reference) (defvar x) (defn _init (x) (set @x x)) (defn foo () 99)) (defclass Bar (Foo) (defn _init () (super (lambda () (super:foo)))) (defn foo () 101)) (let* ((bar (Bar:new))) (print (bar:foo)) (print (funcall bar:x))))"#), "\n101\n99\n"); } #[test] pub fn bad_super_call_test() { assert_eq!( parse_compile_decl_err("((defn foo () (super:foo)))"), Err(PError::from(GDError::new(GDErrorF::BadSuperCall(String::from("foo")), SourceOffset(14)))), ); } #[test] pub fn builtin_patched_class_test() { // Make sure the patched classes (`PATCHED_CLASS_NAMES` in // `class_loader.rs`) are being loaded correctly. assert_eq!(parse_and_run("((print (instance? (File:new) File)) (print (= (typeof (File:new)) File)))"), "\nTrue\nTrue\n"); } #[test] pub fn builtin_singleton_class_test() { assert_eq!(parse_and_run("((print (typeof Engine):name) (print _Engine:name) (print (Engine:get_class)))"), "\n_Engine\n_Engine\n_Engine\n"); } #[test] pub fn lambdas_inside_class_and_out_test() { // This is a regression test for issue #139. let result = parse_compile_decl(r#" ((defclass Foo () (defn foo () (lambda () 1))) (defn bar () (lambda () 2))) "#); assert_eq!(result, r#"extends Reference class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return 1 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class Foo extends Reference: func _init(): pass func foo(): return _LambdaBlock.new() class _LambdaBlock_0 extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return 2 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") static func bar(): return _LambdaBlock_0.new() "#); } #[test] pub fn lambdas_inside_class_static_and_out_test() { // This is a regression test for issue #139. let result = parse_compile_decl(r#" ((defclass Foo () (defn foo () (lambda () 1)) (defn bar () static (lambda () 2))) (defn baz () (lambda () 3))) "#); assert_eq!(result, r#"extends Reference class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return 1 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class _LambdaBlock_0 extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return 2 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") class Foo extends Reference: func _init(): pass func foo(): return _LambdaBlock.new() static func bar(): return _LambdaBlock_0.new() class _LambdaBlock_1 extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return 3 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") static func baz(): return _LambdaBlock_1.new() "#); } ================================================ FILE: tests/test/collection_conversion_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::parse_and_run; #[test] fn array_roundtrip_test() { assert_eq!(parse_and_run(" ((let ((arr [10 20 30 40])) (let ((arr1 (list->array (array->list arr)))) (print (elt arr1 0)) (print (elt arr1 1)) (print (elt arr1 2)) (print (elt arr1 3))))) "), "\n10\n20\n30\n40\n"); } #[test] fn array_list_length_test() { assert_eq!(parse_and_run("((print (len (array->list [9 10 11]))))"), "\n3\n"); } #[test] fn list_array_length_test() { assert_eq!(parse_and_run("((print (len (list->array '(9 10 11)))))"), "\n3\n"); } #[test] fn array_varargs_test_1() { assert_eq!(parse_and_run("((defn foo (&arr arr) (elt arr 0)) (print (foo 10 20 30)))"), "\n10\n"); } #[test] fn array_varargs_test_2() { assert_eq!(parse_and_run("((let ((foo (lambda (&arr arr) (elt arr 0)))) (print (funcall foo 10 20 30))))"), "\n10\n"); } ================================================ FILE: tests/test/common/import.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . // Some infrastructure for spoofing files to test import syntax. use gdlisp::pipeline::resolver::NameResolver; use std::collections::HashMap; use std::path::Path; use std::io::{self, Read, Write}; use std::cmp::min; // Behaves like the read instance on &[u8] but takes ownership of its // string. #[derive(Clone)] struct StringReader { string: String, position: usize } #[derive(Default)] pub struct MockFileLoader { files: HashMap, } impl MockFileLoader { pub fn new() -> MockFileLoader { MockFileLoader::default() } pub fn add_file(&mut self, name: &str, contents: &str) { self.files.insert(name.to_owned(), contents.to_owned()); } } impl StringReader { pub fn new(s: String) -> StringReader { StringReader { string: s, position: 0 } } } impl Read for StringReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { let bytes = self.string.as_bytes(); let to_copy = min(buf.len(), bytes.len() - self.position); buf[0..to_copy].copy_from_slice(&bytes[self.position..self.position+to_copy]); self.position += to_copy; Ok(to_copy) } } impl NameResolver for MockFileLoader { fn resolve_input_path(&self, filename: &Path) -> io::Result> { let filename = filename.file_name().unwrap().to_string_lossy().to_owned(); let file_contents = StringReader::new(self.files.get(&*filename).unwrap().to_owned()); Ok(Box::new(file_contents)) } fn resolve_output_path(&self, _filename: &Path) -> io::Result> { Ok(Box::new(io::sink())) } } ================================================ FILE: tests/test/common/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . #![allow(dead_code)] extern crate gdlisp; pub mod import; use gdlisp::compile::Compiler; use gdlisp::compile::stmt_wrapper; use gdlisp::compile::names::fresh::FreshNameGenerator; use gdlisp::compile::body::builder::{CodeBuilder, StmtBuilder}; use gdlisp::compile::body::class_scope::OutsideOfClass; use gdlisp::compile::symbol_table::SymbolTable; use gdlisp::compile::symbol_table::local_var::LocalVar; use gdlisp::compile::symbol_table::function_call::{FnCall, FnScope, FnSpecs}; use gdlisp::compile::symbol_table::call_magic::CallMagic; use gdlisp::compile::preload_resolver::DefaultPreloadResolver; use gdlisp::runner::godot::GodotCommand; use gdlisp::runner::into_gd_file::IntoGDFile; use gdlisp::runner::path::{RPathBuf, PathSrc}; use gdlisp::runner::version::VersionInfo; use gdlisp::AST_PARSER; use gdlisp::ir; use gdlisp::ir::incremental::IncCompiler; use gdlisp::ir::main_function::{StaticMainFunctionHandler, DisallowMainFunctionHandler}; use gdlisp::gdscript::library; use gdlisp::gdscript::decl; use gdlisp::gdscript::class_extends::ClassExtends; use gdlisp::gdscript::spacing::SpacedDeclPrinter; use gdlisp::pipeline::Pipeline; use gdlisp::pipeline::error::{PError, IOError}; use gdlisp::pipeline::config::ProjectConfig; use gdlisp::pipeline::source::SourceOffset; use tempfile::{Builder, TempDir}; use std::process::{Output, Stdio}; use std::path::PathBuf; use std::fs::{File, copy}; use std::io::{self, Write}; use std::str::FromStr; pub const TEST_FUNCTION_NAME: &'static str = "run_test"; pub const BEGIN_GDLISP_TESTS: &'static str = "__BEGIN_GDLISP_TESTS__"; /// A type containing string output from both `stdout` and `stderr`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StringOutput { pub stdout: String, pub stderr: String, } /* fn template_contents

>(filename: P) -> String { format!(r#" extends SceneTree func _init(): var file = load("{}") print("{}") file.run() "#, filename.as_ref().display(), BEGIN_GDLISP_TESTS) } pub fn write_to_file(data: &T) -> io::Result where T : IntoGDFile + ?Sized { let mut tmp = Builder::new() .prefix("__gdlisp_test") .suffix(".gd") .rand_bytes(5) .tempfile()?; data.write_to_gd(&mut tmp)?; tmp.flush()?; Ok(tmp) } pub fn run_temporary(data: &T) -> io::Result where T : IntoGDFile + ?Sized { let temp_file = write_to_file(data)?; let runner_text = template_contents(temp_file.path()); runner::run_with_temporary(&runner_text) } */ fn main_function_handler() -> StaticMainFunctionHandler { StaticMainFunctionHandler::new(TEST_FUNCTION_NAME.to_owned()) } pub fn dummy_config() -> ProjectConfig { ProjectConfig { root_directory: PathBuf::from_str(".").unwrap(), // Infallible optimizations: false, godot_version: VersionInfo::default(), } } pub fn dummy_pipeline() -> Pipeline { let mut pipeline = Pipeline::new(dummy_config()); let path = RPathBuf::new(PathSrc::Res, PathBuf::from("TEST.lisp")).unwrap(); pipeline.set_currently_loading_file(path); pipeline } fn bind_helper_symbols(_table: &mut SymbolTable) { // Does nothing right now. May remove this later. } pub fn dump_files(dir: &mut TempDir, data: &T) -> io::Result<()> where T : IntoGDFile + ?Sized { // The target file itself let mut target_file = File::create(dir.path().join("TEST.gd"))?; data.write_to_gd(&mut target_file)?; // The runner shim let mut temp_file = File::create(dir.path().join("main.tscn"))?; write!(temp_file, r#" [gd_scene load_steps=2 format=2] [ext_resource path="res://main.gd" type="Script" id=1] [node name="main" type="Node"] script = ExtResource( 1 ) "#)?; let mut temp_scr_file = File::create(dir.path().join("main.gd"))?; write!(temp_scr_file, r#" extends Node func _ready(): var file = load("res://TEST.gd") print("{}") file.run_test() get_tree().quit() "#, BEGIN_GDLISP_TESTS)?; // The GDLisp.gd file copy("GDLisp.gd", dir.path().join("GDLisp.gd"))?; // Project file let mut project_file = File::create(dir.path().join("project.godot"))?; write!(project_file, r#" config_version=4 [application] run/main_scene="res://main.tscn" [autoload] GDLisp="*res://GDLisp.gd" "#)?; Ok(()) } fn parse_and_run_err_impl(input: &str, runner: &mut GodotCommand) -> Result { let value = AST_PARSER.parse(input)?; let used_names = value.all_symbols(); let mut table = SymbolTable::new(); bind_helper_symbols(&mut table); library::bind_builtins(&mut table, false); let mut pipeline = dummy_pipeline(); let (decls, _macros) = ir::compile_and_check(&mut pipeline, &value, &main_function_handler())?; let mut compiler = Compiler::new(FreshNameGenerator::new(used_names), Box::new(DefaultPreloadResolver), decls.minimalist_flag); let mut builder = CodeBuilder::new(ClassExtends::SimpleIdentifier(String::from("Reference"))); compiler.frame(&mut pipeline, &mut builder, &mut table, &mut OutsideOfClass).compile_toplevel(&decls)?; let mut temp_dir = Builder::new().prefix("__gdlisp_test").rand_bytes(5).tempdir().map_err(|err| IOError::new(err, SourceOffset(0)))?; let code_output = builder.build(); // println!("{}", code_output.to_gd()); dump_files(&mut temp_dir, &code_output).map_err(|err| IOError::new(err, SourceOffset(0)))?; runner.project_dir(temp_dir.path()).output() .map_err(|err| PError::from(IOError::new(err, SourceOffset(0)))) } fn strip_cr(input: &str) -> String { // Windows outputs \r\n and Linux outputs \n. We strip \r so our // tests can look for \n in every case. input.replace("\r", "") } pub fn parse_and_run_err(input: &str) -> Result { let mut runner = GodotCommand::base(); let Output { stdout, .. } = parse_and_run_err_impl(input, &mut runner)?; let result = String::from_utf8_lossy(&stdout); match result.find(BEGIN_GDLISP_TESTS) { None => Ok(strip_cr(&result)), Some(idx) => Ok(strip_cr(&result[idx + BEGIN_GDLISP_TESTS.bytes().count()..])), } } pub fn parse_and_run(input: &str) -> String { parse_and_run_err(input).unwrap() } pub fn parse_and_run_with_stderr_err(input: &str) -> Result { let mut runner = GodotCommand::base(); runner.stderr(Stdio::piped()); let Output { stdout, stderr, .. } = parse_and_run_err_impl(input, &mut runner)?; let stdout = String::from_utf8_lossy(&stdout); let stderr = String::from_utf8_lossy(&stderr); let stdout = match stdout.find(BEGIN_GDLISP_TESTS) { None => strip_cr(&stdout), Some(idx) => strip_cr(&stdout[idx + BEGIN_GDLISP_TESTS.bytes().count()..]), }; let stderr = strip_cr(&stderr); Ok(StringOutput { stdout, stderr }) } pub fn parse_and_run_with_stderr(input: &str) -> StringOutput { parse_and_run_with_stderr_err(input).unwrap() } fn bind_helper_symbols_comp(table: &mut SymbolTable) { // Binds a few helper names to the symbol table for the sake of // debugging. table.set_fn(String::from("foo"), FnCall::file_constant(FnSpecs::new(0, 0, None), FnScope::Global, String::from("foo")), CallMagic::DefaultCall); table.set_fn(String::from("foo1"), FnCall::file_constant(FnSpecs::new(1, 0, None), FnScope::Global, String::from("foo1")), CallMagic::DefaultCall); table.set_fn(String::from("foo2"), FnCall::file_constant(FnSpecs::new(2, 0, None), FnScope::Global, String::from("foo2")), CallMagic::DefaultCall); table.set_fn(String::from("bar"), FnCall::file_constant(FnSpecs::new(0, 0, None), FnScope::Global, String::from("bar")), CallMagic::DefaultCall); table.set_var(String::from("foobar"), LocalVar::read(String::from("foobar"))); table.set_var(String::from("glob"), LocalVar::file_constant(String::from("glob"))); } pub fn parse_compile_and_output_err(input: &str) -> Result { parse_compile_and_output_err_h(input).map(|x| x.0) } pub fn parse_compile_and_output_err_h(input: &str) -> Result<(String, String), PError> { let value = AST_PARSER.parse(input)?; let used_names = value.all_symbols(); let mut compiler = Compiler::new(FreshNameGenerator::new(used_names), Box::new(DefaultPreloadResolver), false); let mut table = SymbolTable::new(); bind_helper_symbols_comp(&mut table); library::bind_builtins(&mut table, false); let mut pipeline = dummy_pipeline(); let mut builder = StmtBuilder::new(); let value = { let mut icompiler = IncCompiler::new(value.all_symbols()); icompiler.bind_builtin_macros(&mut pipeline); let expr = icompiler.compile_expr(&mut pipeline, &value)?; ir::loops::check_expr(&expr)?; expr }; { let mut class_scope = OutsideOfClass; let mut frame = compiler.frame(&mut pipeline, &mut builder, &mut table, &mut class_scope); let () = frame.compile_stmt(&mut stmt_wrapper::Return, &value)?; } let (stmts, helpers) = builder.build(); let a = stmts.into_iter().map(|stmt| stmt.to_gd(0)).collect::(); let b = output_decls(helpers); Ok((a, b)) } fn output_decls(decls: Vec) -> String { let printer = SpacedDeclPrinter::new(); let mut string = String::new(); printer.write_gd(&mut string, decls.iter()).unwrap(); string } pub fn parse_compile_and_output(input: &str) -> String { parse_compile_and_output_err(input).unwrap() } pub fn parse_compile_and_output_h(input: &str) -> (String, String) { parse_compile_and_output_err_h(input).unwrap() } pub fn parse_compile_decl_err(input: &str) -> Result { let value = AST_PARSER.parse(input)?; let used_names = value.all_symbols(); let mut table = SymbolTable::new(); library::bind_builtins(&mut table, false); let mut pipeline = dummy_pipeline(); let mut builder = CodeBuilder::new(ClassExtends::SimpleIdentifier("Reference".to_owned())); let (decls, _macros) = ir::compile_and_check(&mut pipeline, &value, &DisallowMainFunctionHandler)?; let mut compiler = Compiler::new(FreshNameGenerator::new(used_names), Box::new(DefaultPreloadResolver), decls.minimalist_flag); compiler.frame(&mut pipeline, &mut builder, &mut table, &mut OutsideOfClass).compile_toplevel(&decls)?; let class = builder.build(); Ok(class.to_gd()) } pub fn parse_compile_decl(input: &str) -> String { parse_compile_decl_err(input).unwrap() } ================================================ FILE: tests/test/concurrency_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; use std::thread; // Mostly failed attempts at reproducing #44, but the test is still // technically good, so I'm leaving it. #[test] fn concurrent_runs_test() { fn thread_fn(_n: u32) { assert_eq!(parse_compile_and_output("(if 1 2 3)"), "var _cond = null\nif 1:\n _cond = 2\nelse:\n if true:\n _cond = 3\n else:\n _cond = null\nreturn _cond\n"); assert_eq!(parse_and_run("((let ((r (Reference:new))) (print (= (typeof r) Reference))))"), "\nTrue\n"); } let runs = 4; let join_handles: Vec<_> = (0..runs).map(|n| thread::spawn(move || thread_fn(n))).collect(); // Let them run concurrently thread::yield_now(); // Now join the handles for handle in join_handles { handle.join().unwrap(); } } ================================================ FILE: tests/test/cond_if_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::{parse_compile_and_output, parse_and_run}; #[test] pub fn if_tests_expr() { assert_eq!(parse_compile_and_output("(if 1 2 3)"), "var _cond = null\nif 1:\n _cond = 2\nelse:\n if true:\n _cond = 3\n else:\n _cond = null\nreturn _cond\n"); assert_eq!(parse_compile_and_output("(if 1 2)"), "var _cond = null\nif 1:\n _cond = 2\nelse:\n if true:\n _cond = null\n else:\n _cond = null\nreturn _cond\n"); assert_eq!(parse_compile_and_output("(if 1 2 ())"), "var _cond = null\nif 1:\n _cond = 2\nelse:\n if true:\n _cond = null\n else:\n _cond = null\nreturn _cond\n"); assert_eq!(parse_compile_and_output("(if 1 (foo) (bar))"), "var _cond = null\nif 1:\n _cond = foo()\nelse:\n if true:\n _cond = bar()\n else:\n _cond = null\nreturn _cond\n"); } #[test] pub fn if_tests_stmt() { assert_eq!(parse_compile_and_output("(progn (if 1 2 3) 1)"), "if 1:\n pass\nelse:\n if true:\n pass\n else:\n pass\nreturn 1\n"); assert_eq!(parse_compile_and_output("(progn (if 1 (foo) (bar)) 1)"), "if 1:\n foo()\nelse:\n if true:\n bar()\n else:\n pass\nreturn 1\n"); } #[test] pub fn cond_tests_expr() { assert_eq!(parse_compile_and_output("(cond ((bar) (foo)) (foobar (bar)))"), "var _cond = null\nif bar():\n _cond = foo()\nelse:\n if foobar:\n _cond = bar()\n else:\n _cond = null\nreturn _cond\n"); } #[test] pub fn cond_tests_stmt() { assert_eq!(parse_compile_and_output("(progn (cond ((bar) (foo)) (foobar (bar))) 1)"), "if bar():\n foo()\nelse:\n if foobar:\n bar()\n else:\n pass\nreturn 1\n"); } #[test] pub fn cond_tests_abbr_expr() { assert_eq!(parse_compile_and_output("(cond ((foo) (foo)) ((bar)))"), "var _cond = null\nif foo():\n _cond = foo()\nelse:\n var _cond_0 = bar()\n if _cond_0:\n _cond = _cond_0\n else:\n _cond = null\nreturn _cond\n"); } #[test] pub fn cond_tests_abbr_stmt() { assert_eq!(parse_compile_and_output("(progn (cond ((foo) (foo)) ((bar))) 1)"), "if foo():\n foo()\nelse:\n var _cond = bar()\n if _cond:\n pass\n else:\n pass\nreturn 1\n"); } #[test] pub fn or_test() { let result0 = parse_compile_and_output("(or 1 2 3)"); assert_eq!(result0, r#"var _cond = null var _cond_1 = 1 if _cond_1: _cond = _cond_1 else: var _cond_0 = 2 if _cond_0: _cond = _cond_0 else: if true: _cond = 3 else: _cond = null return _cond "#); } #[test] pub fn and_test() { let result0 = parse_compile_and_output("(and 1 2 3)"); assert_eq!(result0, r#"var _cond = null if !1: _cond = false else: if !2: _cond = false else: if true: _cond = 3 else: _cond = null return _cond "#); } #[test] pub fn when_test() { let result0 = parse_compile_and_output("(when 1 (foo) (bar))"); assert_eq!(result0, r#"var _cond = null if 1: foo() _cond = bar() else: _cond = null return _cond "#); } #[test] pub fn unless_test() { let result0 = parse_compile_and_output("(unless 1 (foo) (bar))"); assert_eq!(result0, r#"var _cond = null if 1: _cond = null else: if true: foo() _cond = bar() else: _cond = null return _cond "#); } #[test] fn if_test_1() { let output = parse_and_run(r#" ((let ((x 1)) (print (if x 10 20)))) "#); assert_eq!(output, "\n10\n"); } #[test] fn if_test_2() { let output = parse_and_run(r#" ((let ((x 0)) (print (if x 10 20)))) "#); assert_eq!(output, "\n20\n"); } ================================================ FILE: tests/test/const_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use super::common::*; #[test] pub fn const_test() { assert_eq!(parse_compile_decl("((defconst A 10))"), r#"extends Reference const A = 10 "#); assert_eq!(parse_compile_decl("((defconst A \"foo\"))"), r#"extends Reference const A = "foo" "#); } #[test] pub fn const_test_nonconst() { assert_eq!( parse_compile_decl_err("((defconst B (list->array 1)))"), Err(PError::from(GDError::new(GDErrorF::NotConstantEnough(String::from("B")), SourceOffset(13)))), ); } #[test] pub fn const_test_nonconst_in_class() { assert_eq!( parse_compile_decl_err("((defclass Foo (Reference) (defconst B (list->array 1))))"), Err(PError::from(GDError::new(GDErrorF::NotConstantEnough(String::from("B")), SourceOffset(39)))), ); } #[test] pub fn const_test_nonconst_in_enum() { assert_eq!( parse_compile_decl_err("((defenum Foo (A (list->array 1))))"), Err(PError::from(GDError::new(GDErrorF::NotConstantEnough(String::from("Foo")), SourceOffset(17)))), ); } #[test] pub fn const_test_run() { let output = parse_and_run(r#" ((defconst A 100) (print A)) "#); assert_eq!(output, "\n100\n"); } #[test] pub fn builtin_const_test() { // I don't care what this outputs; I just want to know that Godot // recognizes all of the constants I'm compiling these to. parse_and_run(r#"([ ;; GDScript primitive constants PI INF ;; GDScript builtin names BUTTON_LEFT CORNER_BOTTOM_LEFT ERR_BUSY ERR_BUG FAILED HALIGN_CENTER HORIZONTAL JOY_ANALOG_L2 JOY_BUTTON_10 KEY_A KEY_AE KEY_SYSREQ OP_MAX OP_XOR PROPERTY_HINT_DIR PROPERTY_HINT_ENUM PROPERTY_USAGE_GROUP PROPERTY_USAGE_NETWORK SPKEY VALIGN_TOP VERTICAL OK MIDI_MESSAGE_AFTERTOUCH METHOD_FLAG_CONST METHOD_FLAG_NORMAL MARGIN_TOP MARGIN_RIGHT ;; Our custom enums Mouse:LEFT Margin:BOTTOM Corner:TOP_RIGHT Orientation:VERTICAL HAlign:LEFT VAlign:TOP Key:A Key:THORN Key:EXCLAM Key:KP_8 Key:KP-9 KeyMask:CTRL Joy:BUTTON-13 Joy:R3 Joy:ANALOG-R2 MidiMessage:NOTE_ON Orientation:HORIZONTAL PropertyHint:ENUM PropertyUsage:GROUP Op:MAX Type:NIL Err:OK Err:FAILED MethodFlag:METHOD_FLAGS_DEFAULT ])"#); } #[test] pub fn builtin_type_const_test() { // I don't care what this outputs; I just want to know that Godot // recognizes all of the constants I'm compiling these to. parse_and_run(r#"([ ;; GDLisp type names Null Int Bool Float String Vector2 Rect2 Vector3 Transform2D Plane Quat AABB Basis Transform Color NodePath RID Object Dictionary Array PoolByteArray PoolIntArray PoolStringArray PoolRealArray PoolVector2Array PoolVector3Array PoolColorArray Any AnyRef AnyVal Number BaseArray Nothing ;; GDScript original names TYPE_NIL TYPE_BOOL TYPE_INT TYPE_REAL TYPE_STRING TYPE_VECTOR2 TYPE_RECT2 TYPE_VECTOR3 TYPE_TRANSFORM2D TYPE_PLANE TYPE_QUAT TYPE_AABB TYPE_BASIS TYPE_TRANSFORM TYPE_COLOR TYPE_NODE_PATH TYPE_RID TYPE_OBJECT TYPE_DICTIONARY TYPE_ARRAY TYPE_RAW_ARRAY TYPE_INT_ARRAY TYPE_REAL_ARRAY TYPE_STRING_ARRAY TYPE_VECTOR2_ARRAY TYPE_VECTOR3_ARRAY TYPE_COLOR_ARRAY ])"#); } ================================================ FILE: tests/test/declaration_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use gdlisp::ir::identifier::ClassNamespace; use gdlisp::ir::modifier::{ParseError as ModifierParseError, ParseErrorF as ModifierParseErrorF}; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use super::common::{parse_compile_decl, parse_compile_decl_err}; #[test] pub fn empty_file_test() { // TODO Should not have init here (empty class exception, which I // don't think is needed anymore); also applies in macro_test::simple_minimalist_test assert_eq!(parse_compile_decl("()"), r#"extends Reference func _init(): pass "#); } #[test] pub fn simple_function_declaration_test() { assert_eq!(parse_compile_decl("((defn foo (x) x))"), r#"extends Reference static func foo(x): return x "#); } #[test] pub fn lambda_in_function_declaration_test() { assert_eq!(parse_compile_decl("((defn foo (x) (lambda () x) x))"), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var x func _init(x): self.x = x self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return x func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") static func foo(x): return x "#); } #[test] pub fn closed_rw_in_function_declaration_test() { assert_eq!(parse_compile_decl("((defn foo (x) (lambda () (set x 1)) x))"), r#"extends Reference class _LambdaBlock extends GDLisp.Function: var x func _init(x): self.x = x self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): x.contents = 1 return x.contents func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") static func foo(x): x = GDLisp.Cell.new(x) return x.contents "#); } #[test] pub fn mutually_recursive_test() { assert_eq!(parse_compile_decl("((defn foo () (bar)) (defn bar () (foo)))"), r#"extends Reference static func foo(): return bar() static func bar(): return foo() "#); } #[test] pub fn nonexistent_function_test() { assert_eq!( parse_compile_decl_err("((defn foo () (bar)))"), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("bar")), SourceOffset(14)))), ); } #[test] pub fn progn_decl_test() { assert_eq!(parse_compile_decl("((progn (progn (defn foo () ()) (defn bar () ()))))"), r#"extends Reference static func foo(): return null static func bar(): return null "#); } #[test] pub fn declare_value_test_1() { assert_eq!(parse_compile_decl("((sys/declare value x) (defn foo () x))"), r#"extends Reference static func foo(): return x "#); } #[test] pub fn declare_value_test_2() { assert_eq!(parse_compile_decl("((sys/declare superglobal x) (defn foo () x))"), r#"extends Reference static func foo(): return x "#); } #[test] pub fn declare_value_test_3() { assert_eq!(parse_compile_decl("((sys/declare superglobal x) (defconst y x))"), r#"extends Reference const y = x "#); } #[test] pub fn declare_value_test_4() { assert_eq!(parse_compile_decl("((sys/declare constant x) (defconst y x))"), r#"extends Reference const y = x "#); } #[test] pub fn declare_value_non_const_test() { assert_eq!( parse_compile_decl_err("((sys/declare value x) (defconst y x))"), Err(PError::from(GDError::new(GDErrorF::NotConstantEnough(String::from("y")), SourceOffset(35)))), ); } #[test] pub fn declare_function_test_1() { assert_eq!(parse_compile_decl("((sys/declare function f ()) (defn foo () (f)))"), r#"extends Reference static func foo(): return f() "#); } #[test] pub fn declare_function_test_2() { assert_eq!(parse_compile_decl("((sys/declare function f (a &opt b)) (defn foo () (f 1) (f 1 2)))"), r#"extends Reference static func foo(): f(1, null) return f(1, 2) "#); } #[test] pub fn declare_function_test_3() { assert_eq!(parse_compile_decl("((sys/declare superfunction f (a &opt b)) (defn foo () (f 1) (f 1 2)))"), r#"extends Reference static func foo(): f(1, null) return f(1, 2) "#); } #[test] pub fn nonsense_modifier_function_test() { assert_eq!( parse_compile_decl_err(r#"((defn foo () public private 1))"#), Err(PError::from(ModifierParseError::new(ModifierParseErrorF::UniquenessError(String::from("visibility")), SourceOffset(21)))), ); } #[test] pub fn declare_function_inner_test_1() { assert_eq!(parse_compile_decl("((sys/declare function f ()) (defclass Foo (Reference) (defn _init () (f))))"), r#"extends Reference class Foo extends Reference: func _init(): __gdlisp_outer_class_0.f() var __gdlisp_outer_class_0 = load("res://TEST.gd") "#); } #[test] pub fn declare_function_inner_test_2() { assert_eq!(parse_compile_decl("((sys/declare superfunction f ()) (defclass Foo (Reference) (defn _init () (f))))"), r#"extends Reference class Foo extends Reference: func _init(): f() "#); } #[test] pub fn duplicate_const_test() { assert_eq!( parse_compile_decl_err(r#"((defconst A 1) (defconst A 1))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(17)))), ); } #[test] pub fn duplicate_const_in_main_class_test() { assert_eq!( parse_compile_decl_err(r#"((defconst A 1) (defclass Foo () main (defconst A 1)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateNameConflictInMainClass(ClassNamespace::Value, String::from("A")), SourceOffset(39)))), ); } #[test] pub fn duplicate_const_with_var_in_main_class_test() { assert_eq!( parse_compile_decl_err(r#"((defconst A 1) (defclass Foo () main (defvar A 1)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateNameConflictInMainClass(ClassNamespace::Value, String::from("A")), SourceOffset(39)))), ); } #[test] pub fn duplicate_const_in_class_test() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defconst A 1) (defconst A 1)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(34)))), ); } #[test] pub fn duplicate_var_in_class_test() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defvar A 1) (defvar A 1)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(32)))), ); } #[test] pub fn duplicate_var_const_in_class_test() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defvar A 1) (defconst A 1)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(32)))), ); } #[test] pub fn duplicate_fn_in_class_test() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defn foo ()) (defn foo ())))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("foo")), SourceOffset(33)))), ); } #[test] pub fn duplicate_static_fn_in_class_test_1() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defn foo ()) (defn foo () static)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("foo")), SourceOffset(33)))), ); } #[test] pub fn duplicate_static_fn_in_class_test_2() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defn foo () static) (defn foo () static)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("foo")), SourceOffset(40)))), ); } #[test] pub fn duplicate_signal_in_class_test() { assert_eq!( parse_compile_decl_err(r#"((defclass Foo () (defsignal foo) (defsignal foo ())))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Signal, String::from("foo")), SourceOffset(35)))), ); } #[test] pub fn duplicate_fn_test() { assert_eq!( parse_compile_decl_err(r#"((defn foo () 1) (defn foo () 2))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("foo")), SourceOffset(18)))), ); } #[test] pub fn duplicate_fn_main_class_test() { assert_eq!( parse_compile_decl_err(r#"((defn foo () 1) (defclass Foo (Reference) main (defn foo () 2)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateNameConflictInMainClass(ClassNamespace::Function, String::from("foo")), SourceOffset(49)))), ); } #[test] pub fn duplicate_fn_main_class_test_static() { assert_eq!( parse_compile_decl_err(r#"((defn foo () 1) (defclass Foo (Reference) main (defn foo () static 2)))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateNameConflictInMainClass(ClassNamespace::Function, String::from("foo")), SourceOffset(49)))), ); } #[test] pub fn overlapping_namespaces_test() { assert_eq!(parse_compile_decl("((defn foo ()) (defconst foo 1))"), r#"extends Reference static func foo(): return null const foo = 1 "#); } #[test] pub fn overlapping_namespaces_in_class_test() { assert_eq!(parse_compile_decl("((defclass Foo () (defsignal foo) (defn foo ()) (defconst foo 1)))"), r#"extends Reference class Foo extends Reference: func _init(): pass signal foo func foo(): return null const foo = 1 "#); } #[test] pub fn duplicate_const_in_lambda_class_test() { assert_eq!( parse_compile_decl_err(r#"((defn foo () (new Reference (defconst A 1) (defconst A 1))))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(45)))), ); } #[test] pub fn duplicate_var_in_lambda_class_test() { assert_eq!( parse_compile_decl_err(r#"((defn foo () (new Reference (defvar A 1) (defvar A 1))))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(43)))), ); } #[test] pub fn duplicate_var_const_in_lambda_class_test() { assert_eq!( parse_compile_decl_err(r#"((defn foo () (new Reference (defvar A 1) (defconst A 1))))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Value, String::from("A")), SourceOffset(43)))), ); } #[test] pub fn duplicate_fn_in_lambda_class_test() { assert_eq!( parse_compile_decl_err(r#"((defn xyz () (new Reference (defn foo ()) (defn foo ()))))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("foo")), SourceOffset(44)))), ); } #[test] pub fn duplicate_signal_in_lambda_class_test() { assert_eq!( parse_compile_decl_err(r#"((defn xyz () (new Reference (defsignal foo) (defsignal foo ()))))"#), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Signal, String::from("foo")), SourceOffset(46)))), ); } #[test] pub fn declare_value_reserved_word_test_1() { assert_eq!(parse_compile_decl("((sys/declare superglobal while) (defconst y while))"), r#"extends Reference const y = _while "#); } #[test] pub fn declare_value_reserved_word_test_2() { // Invalid syntax in GDScript, but that's what you get for using a // `sys/` primitive irresponsibly ¯\_(ツ)_/¯ assert_eq!(parse_compile_decl("((sys/declare superglobal (x while)) (defconst y x))"), r#"extends Reference const y = while "#); } #[test] pub fn declare_function_reserved_word_test_1() { assert_eq!(parse_compile_decl("((sys/declare superfunction self ()) (defn foo () (self)))"), r#"extends Reference static func foo(): return _self() "#); } #[test] pub fn declare_function_reserved_word_test_2() { // Invalid syntax in GDScript, but that's what you get for using a // `sys/` primitive irresponsibly ¯\_(ツ)_/¯ assert_eq!(parse_compile_decl("((sys/declare superfunction (x self) ()) (defn foo () (x)))"), r#"extends Reference static func foo(): return self() "#); } #[test] pub fn declare_value_custom_name_test_1() { assert_eq!(parse_compile_decl("((sys/declare superglobal (x pizza)) (defconst y x))"), r#"extends Reference const y = pizza "#); } #[test] pub fn declare_value_custom_name_test_2() { assert_eq!(parse_compile_decl("((sys/declare superglobal (x pepperoni-pizza)) (defconst y x))"), r#"extends Reference const y = pepperoni_pizza "#); } #[test] pub fn declare_function_custom_name_test_1() { assert_eq!(parse_compile_decl("((sys/declare superfunction (x pizza) ()) (defn foo () (x)))"), r#"extends Reference static func foo(): return pizza() "#); } #[test] pub fn declare_function_custom_name_test_2() { assert_eq!(parse_compile_decl("((sys/declare superfunction (x pepperoni-pizza) ()) (defn foo () (x)))"), r#"extends Reference static func foo(): return pepperoni_pizza() "#); } #[test] pub fn bad_sys_declare_test() { assert_eq!( parse_compile_decl_err("((sys/declare not-a-valid-declaration-type foobar))"), Err(PError::from(GDError::new(GDErrorF::BadSysDeclare(String::from("not-a-valid-declaration-type")), SourceOffset(1)))), ); } ================================================ FILE: tests/test/dependencies_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::dummy_pipeline; use gdlisp::ir; use gdlisp::ir::declaration_table::DeclarationTable; use gdlisp::ir::depends::Dependencies; use gdlisp::ir::identifier::{IdLike, Id, Namespace}; use gdlisp::ir::main_function::DisallowMainFunctionHandler; use gdlisp::AST_PARSER; use gdlisp::pipeline::source::SourceOffset; use std::collections::{HashMap, HashSet}; // TODO Test some dependency analysis that involves importing multiple files. fn dependencies_of<'a>(input: &str, target_name: &(dyn IdLike + 'a), pos: SourceOffset) -> Dependencies { let ast = AST_PARSER.parse(input).unwrap(); let (toplevel, _macros) = ir::compile_and_check(&mut dummy_pipeline(), &ast, &DisallowMainFunctionHandler).unwrap(); ir::scope::check_scopes(&toplevel).unwrap(); let table = DeclarationTable::from(toplevel.decls); Dependencies::identify(&table, &HashSet::new(), target_name, pos) } fn make_deps(known: Vec<(&str, usize)>, unknown: Vec<(&str, usize)>) -> Dependencies { Dependencies { known: known.into_iter().map(|(x, p)| (Id::new(Namespace::Function, x.to_owned()), SourceOffset::from(p))).collect(), imports: HashMap::new(), unknown: unknown.into_iter().map(|(x, p)| (Id::new(Namespace::Function, x.to_owned()), SourceOffset::from(p))).collect(), } } #[test] pub fn dependencies_test_empty() { assert_eq!(dependencies_of(r#" ((defn foo () ()) (defn bar () ()) (defn baz () ()))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(10)), make_deps(vec!(("baz", 10)), vec!())); } #[test] pub fn dependencies_test_forward() { assert_eq!(dependencies_of(r#" ((defn foo () ()) (defn bar () ()) (defn baz () (bar)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(999)), make_deps(vec!(("baz", 999), ("bar", 63)), vec!())); } #[test] pub fn dependencies_test_backward() { assert_eq!(dependencies_of(r#" ((defn foo () ()) (defn bar () (baz)) (defn baz () ()))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(9)), make_deps(vec!(("baz", 9)), vec!())); } #[test] pub fn dependencies_test_two() { assert_eq!(dependencies_of(r#" ((defn foo () ()) (defn bar () ()) (defn baz () (foo) (bar)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(0)), make_deps(vec!(("baz", 0), ("bar", 69), ("foo", 63)), vec!())); } #[test] pub fn dependencies_test_transitive() { assert_eq!(dependencies_of(r#" ((defn foo () (bar)) (defn bar () ()) (defn baz () (foo)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(0)), make_deps(vec!(("baz", 0), ("bar", 19), ("foo", 66)), vec!())); } #[test] pub fn dependencies_test_recursion() { assert_eq!(dependencies_of(r#" ((defn foo () (bar)) (defn bar () (bar)) (defn baz () (foo) (baz)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(999)), make_deps(vec!(("baz", 75), ("bar", 19), ("foo", 69)), vec!())); } #[test] pub fn dependencies_test_cycle() { assert_eq!(dependencies_of(r#" ((defn foo () (baz)) (defn bar () (foo)) (defn baz () (bar)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(999)), make_deps(vec!(("baz", 19), ("bar", 69), ("foo", 44)), vec!())); } #[test] pub fn dependencies_test_unknowns_1() { assert_eq!(dependencies_of(r#" ((defn foo () (aaa)) (defn bar () (foo)) (defn baz () (bar)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(999)), make_deps(vec!(("baz", 999), ("bar", 69), ("foo", 44)), vec!(("aaa", 19)))); } #[test] pub fn dependencies_test_unknowns_2() { assert_eq!(dependencies_of(r#" ((defn foo () (aaa)) (defn bar () (bbb)) (defn baz () (bar)))"#, &*Id::build(Namespace::Function, "baz"), SourceOffset(999)), make_deps(vec!(("baz", 999), ("bar", 69)), vec!(("bbb", 44)))); } ================================================ FILE: tests/test/dictionary_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::{parse_compile_and_output, parse_and_run}; #[test] fn dict_test_1() { assert_eq!(parse_compile_and_output("{}"), "return {}\n"); } #[test] fn dict_test_2() { assert_eq!(parse_compile_and_output("{1 2 3 4}"), "return {1: 2, 3: 4}\n"); } #[test] fn dict_test_running() { let result = parse_and_run(r#"((let ((a {"b" 3})) (print (dict/elt a "b"))))"#); assert_eq!(result, "\n3\n"); } #[test] fn dict_test_set_running() { let result = parse_and_run(r#"((let ((a {"b" 3})) (set (dict/elt a "c") 100) (print (dict/elt a "b")) (print (dict/elt a "c")) (set (dict/elt a "b") -3) (print (dict/elt a "b"))))"#); assert_eq!(result, "\n3\n100\n-3\n"); } ================================================ FILE: tests/test/enum_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use super::common::*; #[test] pub fn empty_enum_test() { assert_eq!(parse_compile_decl("((defenum MyEnum))"), r#"extends Reference enum MyEnum { } "#); } #[test] pub fn unvalued_enum_test() { assert_eq!(parse_compile_decl("((defenum MyEnum A B C))"), r#"extends Reference enum MyEnum { A, B, C, } "#); } #[test] pub fn valued_enum_test() { assert_eq!(parse_compile_decl("((defenum MyEnum (A 1) (B 2) (C 3)))"), r#"extends Reference enum MyEnum { A = 1, B = 2, C = 3, } "#); } #[test] pub fn mixed_enum_test() { assert_eq!(parse_compile_decl("((defenum MyEnum A (B 2) (C 3)))"), r#"extends Reference enum MyEnum { A, B = 2, C = 3, } "#); } #[test] pub fn enum_runner_test() { let result = parse_and_run("((defenum MyEnum (A 1) (B 2) (C 3)) (print MyEnum:A) (print MyEnum:B) (print MyEnum:C))"); assert_eq!(result, "\n1\n2\n3\n"); } #[test] pub fn invalid_enum_test() { assert_eq!( parse_compile_decl_err("((defenum MyEnum (A 1) (B 2) (C 3)) (defn foo () MyEnum:D))"), Err(PError::from(GDError::new(GDErrorF::NoSuchEnumValue(String::from("MyEnum"), String::from("D")), SourceOffset(49)))), ); } #[test] pub fn builtin_enum_test() { assert_eq!(parse_compile_decl("((defn foo () Mouse:LEFT))"), r#"extends Reference static func foo(): return GDLisp.Mouse.LEFT "#); } ================================================ FILE: tests/test/error_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use gdlisp::compile::symbol_table::local_var::VarNameIntoExtendsError; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use gdlisp::sxp::ast::AST; use super::common::*; #[test] pub fn bad_call_compile_test_1() { assert_eq!( parse_compile_and_output_err("(5 6)"), Err(PError::from(GDError::new(GDErrorF::CannotCall(AST::from_value(5, SourceOffset(1))), SourceOffset(1)))), ); } #[test] pub fn bad_call_compile_test_2() { assert_eq!( parse_compile_and_output_err("(() 6)"), Err(PError::from(GDError::new(GDErrorF::CannotCall(AST::nil(SourceOffset(1))), SourceOffset(1)))), ); } #[test] pub fn cannot_extend_local() { assert_eq!( parse_compile_and_output_err(r#"(let ((x 1)) (let ((y (new x))) y))"#), Err(PError::from(GDError::from_value(VarNameIntoExtendsError::CannotExtendLocal(String::from("x")), SourceOffset(22)))), ); } #[test] pub fn cannot_extend_current_file() { assert_eq!( parse_compile_decl_err(r#"((defclass MyMainClass () main) (defclass MySubclass (MyMainClass)))"#), Err(PError::from(GDError::from_value(VarNameIntoExtendsError::CannotExtendCurrentFile(String::from("res://TEST.gd")), SourceOffset(33)))), ); } ================================================ FILE: tests/test/even_odd_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::parse_and_run; #[test] fn even_odd_test_labels() { let output = parse_and_run(r#" ((labels ((is-even (x) (if (= x 0) #t (is-odd (- x 1)))) (is-odd (x) (if (= x 0) #f (is-even (- x 1))))) (let ((x 0)) (while (< x 11) (print (is-even x)) (print (is-odd x)) (set x (+ x 1)))))) "#); assert_eq!(output, "\nTrue\nFalse\nFalse\nTrue\nTrue\nFalse\nFalse\nTrue\nTrue\nFalse\nFalse\nTrue\nTrue\nFalse\nFalse\nTrue\nTrue\nFalse\nFalse\nTrue\nTrue\nFalse\n"); } ================================================ FILE: tests/test/factorial_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::parse_and_run; #[test] fn factorial_test_global() { let output = parse_and_run(r#" ((defn fact (x) (if (<= x 1) 1 (* x (fact (- x 1))))) (let ((x 0)) (while (< x 6) (print (fact x)) (set x (+ x 1))))) "#); assert_eq!(output, "\n1\n1\n2\n6\n24\n120\n"); } #[test] fn factorial_test_labels() { let output = parse_and_run(r#" ((labels ((fact (x) (if (<= x 1) 1 (* x (fact (- x 1)))))) (let ((x 0)) (while (< x 6) (print (fact x)) (set x (+ x 1)))))) "#); assert_eq!(output, "\n1\n1\n2\n6\n24\n120\n"); } ================================================ FILE: tests/test/flet_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::{parse_compile_and_output_h, parse_and_run}; #[test] fn simple_flet_test_1() { let output = parse_and_run("((flet ((f (x) x)) (print (f 1))))"); assert_eq!(output, "\n1\n"); } #[test] fn simple_flet_test_2() { let output = parse_and_run("((flet ((f (x) (* x 2))) (print (f 1)) (print (f 2))))"); assert_eq!(output, "\n2\n4\n"); } #[test] fn simple_flet_test_3() { let output = parse_and_run("((defn foo (x) (* x 2)) (flet ((f (x) (foo x))) (print (f 1)) (print (f 2))))"); assert_eq!(output, "\n2\n4\n"); } #[test] fn closed_flet_test_1() { let output = parse_and_run(r#" ((let ((x 0)) (flet ((f () (set x (+ x 1)))) (print (f)) (print (f)))))"#); assert_eq!(output, "\n1\n2\n"); } #[test] fn closed_flet_test_2() { let output = parse_and_run(r#" ((let ((g (let ((x 0)) (flet ((f () (set x (+ x 1)))) (lambda () (f)))))) (print (funcall g)) (print (funcall g)))) "#); assert_eq!(output, "\n1\n2\n"); } #[test] fn nested_flet_test() { let output = parse_and_run(r#" ((let ((g (let ((x 0)) (flet ((f () (set x (+ x 1)))) (flet ((g () (f))) (function g)))))) (print (funcall g)) (print (funcall g)))) "#); assert_eq!(output, "\n1\n2\n"); } #[test] pub fn semiglobal_flet_test() { let result0 = parse_compile_and_output_h("(flet ((f (x) (+ x 1))) (f 10))"); assert_eq!(result0.0, "return _flet(10)\n"); assert_eq!(result0.1, "static func _flet(x):\n return x + 1\n"); } #[test] pub fn semiglobal_flet_test_indirect() { let result0 = parse_compile_and_output_h("(flet ((f (x) (+ x 1))) (funcall (function f) 10))"); assert_eq!(result0.0, "return GDLisp.funcall(_FunctionRefBlock.new(), GDLisp.cons(10, null))\n"); assert_eq!(result0.1, r#"static func _flet(x): return x + 1 class _FunctionRefBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(arg0): return load("res://TEST.gd")._flet(arg0) func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn local_flet_test() { let result0 = parse_compile_and_output_h(r#" (let ((x 1)) (flet ((f () (+ x 1))) (f))) "#); assert_eq!(result0.0, "var x = 1\nvar _flet = _LambdaBlock.new(x)\nreturn _flet.call_func()\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: var x func _init(x): self.x = x self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return x + 1 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); } #[test] pub fn local_flet_test_indirect() { let result0 = parse_compile_and_output_h(r#" (let ((x 1)) (flet ((f () (+ x 1))) (funcall (function f)))) "#); assert_eq!(result0.0, "var x = 1\nvar _flet = _LambdaBlock.new(x)\nreturn GDLisp.funcall(_flet, null)\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: var x func _init(x): self.x = x self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return x + 1 func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); } #[test] pub fn local_flet_closure_test() { let result0 = parse_compile_and_output_h(r#" (let ((x 1)) (flet ((f () x)) (let ((g (lambda () (f)))) (funcall g)))) "#); assert_eq!(result0.0, "var x = 1\nvar _flet = _LambdaBlock.new(x)\nvar g = _LambdaBlock_0.new(_flet)\nreturn GDLisp.funcall(g, null)\n"); } ================================================ FILE: tests/test/floating_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::parse_and_run; #[test] fn floating_test() { // I'm using Rust's built-in to_string on f32 to get floating-point // output. I want to make sure nothing funny happens on the Godot // side when I do that. let output = parse_and_run(r#" ((let (x) (set x 1.0000) (set x -0.29199) (set x 3e19) (set x +3e19) (set x -4.1E3) (set x -4.1E+3) (set x 1e-2))) "#); assert_eq!(output, "\n"); } ================================================ FILE: tests/test/for_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use gdlisp::ir::loops::error::LoopPrimitiveError; #[test] fn for_loop_test() { let output = parse_and_run(r#" ((for variable 3 (print variable))) "#); assert_eq!(output, "\n0\n1\n2\n"); } #[test] pub fn for_tests() { assert_eq!(parse_compile_and_output("(for x 1)"), "for x in 1:\n pass\nreturn null\n"); assert_eq!(parse_compile_and_output("(for x 1 2)"), "for x in 1:\n pass\nreturn null\n"); assert_eq!(parse_compile_and_output("(for x 1 (foo))"), "for x in 1:\n foo()\nreturn null\n"); assert_eq!(parse_compile_and_output("(for x-y 1 (foo))"), "for x_y in 1:\n foo()\nreturn null\n"); } #[test] pub fn for_test_with_break() { assert_eq!(parse_compile_and_output("(for i 1 (break))"), r#"for i in 1: break return null "#); } #[test] pub fn for_test_with_continue() { assert_eq!(parse_compile_and_output("(for i 1 (continue))"), r#"for i in 1: continue return null "#); } #[test] pub fn bad_break_in_for_loop_lambda_test() { assert_eq!(parse_compile_and_output_err("(for i 1 (lambda () (break)))"), Err(PError::from(LoopPrimitiveError::break_error(SourceOffset(20)).in_closure()))); } #[test] pub fn bad_continue_in_for_loop_lambda_test() { assert_eq!(parse_compile_and_output_err("(for i 1 (lambda () (continue)))"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(20)).in_closure()))); } #[test] pub fn bad_continue_in_for_loop_lambda_class_test() { assert_eq!(parse_compile_and_output_err("(for i 1 (new Reference (defn foo () (continue))))"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(37)).in_closure()))); } #[test] pub fn bad_continue_in_for_loop_iter_test() { assert_eq!(parse_compile_and_output_err("(for i (continue) 2)"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(7))))); } ================================================ FILE: tests/test/import_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use super::common::import::MockFileLoader; use super::common::dummy_config; use gdlisp::ir::identifier::{Id, Namespace}; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::Pipeline; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; fn setup_simple_file_loader(loader: &mut MockFileLoader) { loader.add_file("example.lisp", "(defn one () 1) (defn two () 2)"); } fn load_and_output_simple_file(input: &str) -> String { load_and_output_simple_file_err(input).unwrap() } fn load_and_output_simple_file_err(input: &str) -> Result { let mut loader = MockFileLoader::new(); setup_simple_file_loader(&mut loader); loader.add_file("main.lisp", input); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0))?.gdscript.to_gd(); Ok(result) } #[test] fn qualified_import_test() { assert_eq!(load_and_output_simple_file(r#" (use "res://example.lisp") (defn run () (example/one) (example/two)) "#), r#"extends Node const example_0 = preload("res://example.gd") static func run(): example_0.one() return example_0.two() "#); } #[test] fn aliased_import_test() { assert_eq!(load_and_output_simple_file(r#" (use "res://example.lisp" as example-name) (defn run () (example-name/one) (example-name/two)) "#), r#"extends Node const example_name_0 = preload("res://example.gd") static func run(): example_name_0.one() return example_name_0.two() "#); } #[test] fn restricted_import_test() { assert_eq!(load_and_output_simple_file(r#" (use "res://example.lisp" (one)) (defn run () (one)) "#), r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return _Import_0.one() "#); } #[test] fn restricted_import_test_failed() { assert_eq!( load_and_output_simple_file_err(r#" (use "res://example.lisp" (one)) (defn run () (two)) "#), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("two")), SourceOffset(67)))), ); } #[test] fn restricted_import_alias_test() { assert_eq!(load_and_output_simple_file(r#" (use "res://example.lisp" ((one as my-one))) (defn run () (my-one)) "#), r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return _Import_0.one() "#); } #[test] fn restricted_import_alias_test_failed() { assert_eq!( load_and_output_simple_file_err(r#" (use "res://example.lisp" ((one as my-one))) (defn run () (one)) "#), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("one")), SourceOffset(79)))), ); } #[test] fn open_import_test() { assert_eq!(load_and_output_simple_file(r#" (use "res://example.lisp" open) (defn run () (one) (two)) "#), r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): _Import_0.one() return _Import_0.two() "#); } #[test] fn nonexistent_import_test() { assert_eq!( load_and_output_simple_file_err(r#" (use "res://example.lisp" (nonexistent-function-name)) "#), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("nonexistent-function-name") }), SourceOffset(12)))), ); } #[test] fn macro_uses_other_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn add-one (x) (+ x 1))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defmacro f (x) (add-one x)) (defn run () (f 43)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func f(x): return _Import_0.add_one(x) static func run(): return 44 "#); } #[test] fn import_declared_value_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(sys/declare value a public)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn f (x) a) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func f(x): return _Import_0.a "#); } #[test] fn import_declared_constant_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(sys/declare constant a public)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defconst y a) (defn f (x) a) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") const y = _Import_0.a static func f(x): return _Import_0.a "#); } #[test] fn import_declared_superglobal_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(sys/declare superglobal a public)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defconst y a) (defn f (x) a) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") const y = a static func f(x): return a "#); } #[test] fn import_declared_function_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(sys/declare function a () public)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn f (x) (a)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func f(x): return _Import_0.a() "#); } #[test] fn import_declared_superfunction_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(sys/declare superfunction a () public)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn f (x) (a)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func f(x): return a() "#); } #[test] fn imported_name_to_class_var_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(sys/declare value A public) (defconst B 100)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defclass Main (Reference) main (defvar a A) (defvar b B) (defvar a1 A onready) (defvar b1 B onready)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Reference const _Import_0 = preload("res://example.gd") func _init(): self.a = _Import_0.A var a var b = _Import_0.B var a1 onready var b1 = _Import_0.B func _ready(): self.a1 = _Import_0.A "#); } #[test] fn cyclic_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(use \"res://b.lisp\" open) (defn foo (x) (bar))"); loader.add_file("b.lisp", "(use \"res://a.lisp\" open) (defn bar (x) (foo))"); loader.add_file("main.lisp", r#" (use "res://b.lisp" open) (defn run () (bar)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)); let error_filename = String::from(if cfg!(windows) { ".\\b.lisp" } else { "./b.lisp" }); assert_eq!(result.map(|_| ()), Err(PError::from( GDError::new(GDErrorF::CyclicImport(error_filename), SourceOffset(0)), ))); } #[test] fn macro_uses_preload_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn add-one (x) (+ x 1))"); // Note: We have to explicitly import the file, even if we later // load it using `preload`. Macro expansion only uses imports for // resolution. I may loosen this constraint later, but for right now // it is required. loader.add_file("main.lisp", r#" (use "res://example.lisp") (defmacro f (x) ((preload "res://example.lisp"):add-one x)) (defn run () (f 43)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const example_0 = preload("res://example.gd") static func f(x): return preload("res://example.gd").add_one(x) static func run(): return 44 "#); } #[test] fn macro_uses_preload_without_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn add-one (x) (+ x 1))"); loader.add_file("main.lisp", r#" (defmacro f (x) ((preload "res://example.lisp"):add-one x)) (defn run () (f 43)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)); let expected_error_path = String::from("res://example.gd"); assert_eq!(result.map(|_| ()), Err(PError::from( GDError::new(GDErrorF::NoSuchFile(expected_error_path), SourceOffset(22)), ))); } #[test] fn symbol_macro_uses_other_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn add-one (x) (+ x 1))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (define-symbol-macro x (add-one 2)) (defn run () x) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func __gdlisp_SymbolMacroFunction_x(): return _Import_0.add_one(2) static func run(): return 3 "#); } #[test] fn macro_from_other_file_import_test_1() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defmacro add-one (x) (+ x 1))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn run () (add-one 43)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return 44 "#); } #[test] fn macro_from_other_file_import_test_2() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn outer () 44) (defclass Foo (Reference) (defn go () (outer))) (defmacro go () ((Foo:new):go))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn run () (go)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return 44 "#); } #[test] fn macro_from_other_file_import_test_3() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn outer () 44) (defclass Foo (Reference) (defn go () static (outer))) (defmacro go () (Foo:go))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn run () (go)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return 44 "#); } #[test] fn symbol_macro_from_other_file_import_test_1() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(define-symbol-macro x 10)"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn run () x) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return 10 "#); } #[test] fn symbol_macro_from_other_file_import_test_2() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn outer () 44) (defclass Foo (Reference) (defn go () (outer))) (define-symbol-macro go ((Foo:new):go))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn run () go) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return 44 "#); } #[test] fn symbol_macro_from_other_file_import_test_3() { let mut loader = MockFileLoader::new(); loader.add_file("example.lisp", "(defn outer () 44) (defclass Foo (Reference) (defn go () static (outer))) (define-symbol-macro go (Foo:go))"); loader.add_file("main.lisp", r#" (use "res://example.lisp" open) (defn run () go) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://example.gd") static func run(): return 44 "#); } #[test] fn macro_several_files_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn add-one-f (x) (+ x 1))"); loader.add_file("b.lisp", r#"(use "res://a.lisp" open) (defmacro f (x) (add-one-f x))"#); loader.add_file("c.lisp", r#"(use "res://b.lisp" open) (defmacro g (x) (+ x (f 87)))"#); loader.add_file("main.lisp", r#" (use "res://c.lisp") (defn run () (c/g 3)) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const c_0 = preload("res://c.gd") static func run(): return 91 "#); } #[test] fn symbol_macro_several_files_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn add-one-f (x) (+ x 1))"); loader.add_file("b.lisp", r#"(use "res://a.lisp" open) (define-symbol-macro f (add-one-f 3))"#); loader.add_file("c.lisp", r#"(use "res://b.lisp" open) (define-symbol-macro g f)"#); loader.add_file("main.lisp", r#" (use "res://c.lisp") (defn run () c/g) "#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const c_0 = preload("res://c.gd") static func run(): return 4 "#); } #[test] fn main_class_import_test_1() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass Foo (Reference) main)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" open) (defn run () Foo)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") static func run(): return _Import_0 "#); } #[test] fn main_class_import_test_2() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass Foo (Reference) main) (defconst VALUE 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" open) (defn run () [Foo VALUE])"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") static func run(): return [_Import_0, _Import_0.VALUE] "#); } #[test] fn import_declare_test_failed_1() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(sys/declare value a)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" open) (defn run () a)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("a")), SourceOffset(39)))), ); } #[test] fn import_declare_test_failed_2() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(sys/declare value a)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (a))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("a") }), SourceOffset(5)))), ); } #[test] fn public_fn_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo () public 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp") (defn run () (a/foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const a_0 = preload("res://a.gd") static func run(): return a_0.foo() "#); } #[test] fn private_fn_import_test_1() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo () private 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp")"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const a_0 = preload("res://a.gd") "#); } #[test] fn private_fn_import_test_2() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo () private 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" open)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") "#); } #[test] fn private_fn_import_test_3() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo () private 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" open) (defn run () (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("foo")), SourceOffset(39)))), ); } #[test] fn private_fn_import_test_4() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo () private 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn public_macro_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defmacro foo () public 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") "#); } #[test] fn private_macro_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defmacro foo () private 1)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn public_const_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defconst foo 1 public)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") "#); } #[test] fn private_const_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defconst foo 1 private)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn public_enum_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defenum foo public A B)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") "#); } #[test] fn private_enum_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defenum foo private A B)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn public_class_import_test_1() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass foo (Node) public (defvar example 1))"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") "#); } #[test] fn public_class_import_test_2() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass foo (Node) main public (defvar example 1))"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") "#); } #[test] fn private_class_import_test_1() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass foo (Node) private (defvar example 1))"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn private_class_import_test_2() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass foo (Node) main private (defvar example 1))"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn private_lazy_val_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(deflazy foo 10 private)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn private_symbol_macro_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(define-symbol-macro foo 10 private)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::UnknownImportedName(Id { namespace: Namespace::Function, name: String::from("foo") }), SourceOffset(5)))), ); } #[test] fn lazy_val_import_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(deflazy foo 10)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo)) (defn run () foo)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") static func run(): return load("res://a.gd")._lazy_0() "#); } #[test] fn lazy_val_import_run_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(deflazy foo 10)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo)) (defmacro bar () foo) (defn run () (bar))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") static func bar(): return load("res://a.gd")._lazy_0() static func run(): return 10 "#); } #[test] fn ambiguous_import_namespace_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo ()) (defconst foo 10)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); assert_eq!( pipeline.load_file("main.lisp", SourceOffset(0)).map(|_| ()), Err(PError::from(GDError::new(GDErrorF::AmbiguousNamespace(String::from("foo")), SourceOffset(5)))), ); } #[test] fn ambiguous_import_namespace_disambiguate_value_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo ()) (defconst foo 10)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" ((foo value))) (defn bar () foo)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") static func bar(): return _Import_0.foo "#); } #[test] fn ambiguous_import_namespace_disambiguate_function_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defn foo ()) (defconst foo 10)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" ((foo function))) (defn bar () (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") static func bar(): return _Import_0.foo() "#); } #[test] fn inner_class_extends_inner_class_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass A (Reference))"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (A)) (defclass B (A))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") class B extends _Import_0.A: func _init(): pass "#); } #[test] fn inner_class_extends_main_class_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass A (Reference) main)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (A)) (defclass B (A))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends Node const _Import_0 = preload("res://a.gd") class B extends _Import_0: func _init(): pass "#); } #[test] fn main_class_extends_inner_class_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass A (Reference))"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (A)) (defclass B (A) main)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends "res://a.gd".A const _Import_0 = preload("res://a.gd") func _init(): pass "#); } #[test] fn main_class_extends_main_class_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", "(defclass A (Reference) main)"); loader.add_file("main.lisp", r#"(use "res://a.lisp" (A)) (defclass B (A) main)"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap().gdscript.to_gd(); assert_eq!(result, r#"extends "res://a.gd" const _Import_0 = preload("res://a.gd") func _init(): pass "#); } #[test] fn load_non_gdlisp_resource_in_macro_test() { let mut loader = MockFileLoader::new(); loader.add_file("a.lisp", r#"(use "res://some_resource.png") (defmacro foo () some_resource)"#); loader.add_file("main.lisp", r#"(use "res://a.lisp" (foo)) (defn bar () (foo))"#); let mut pipeline = Pipeline::with_resolver(dummy_config(), Box::new(loader)); let result = pipeline.load_file("main.lisp", SourceOffset(0)).unwrap_err(); assert_eq!(result, PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("some_resource")), SourceOffset(49)))); } ================================================ FILE: tests/test/labels_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::{parse_compile_and_output_h, parse_and_run, parse_compile_decl}; #[test] fn simple_labels_test_1() { let output = parse_and_run("((labels ((f (x) x)) (print (f 1))))"); assert_eq!(output, "\n1\n"); } #[test] fn labels_test_outer_scope_static_1() { let output = parse_and_run("((defn foo (x) (+ x 10)) (labels ((f (x) (if #f (f x) (foo x)))) (print (f 1))))"); assert_eq!(output, "\n11\n"); } #[test] fn labels_test_outer_scope_static_2() { let output = parse_and_run("((defn foo (x) (+ x 10)) (labels ((f (x) (if #f (f x) (foo x)))) (print (funcall #'f 1))))"); assert_eq!(output, "\n11\n"); } #[test] fn labels_test_outer_scope_static_3() { let output = parse_and_run("((defn foo (x y) (+ x y)) (let ((y 1)) (labels ((f (x) (foo x y))) (print (f 5)))))"); assert_eq!(output, "\n6\n"); } #[test] fn labels_test_outer_scope_static_4() { let output = parse_and_run("((defn foo (x y) (+ x y)) (let ((y 1)) (labels ((f (x) (foo x y))) (print (funcall #'f 5)))))"); assert_eq!(output, "\n6\n"); } #[test] fn labels_test_outer_scope_static_5() { let output = parse_and_run("((defn foo (x y) (+ x y)) (labels ((f (x) (foo x 1))) (print (f 5))))"); assert_eq!(output, "\n6\n"); } #[test] fn labels_test_outer_scope_static_6() { let output = parse_and_run("((defn foo (x y) (+ x y)) (labels ((f (x) (foo x 1))) (print (funcall #'f 5))))"); assert_eq!(output, "\n6\n"); } #[test] pub fn semiglobal_labels_test() { let result0 = parse_compile_and_output_h("(labels ((f (x) (+ x 1))) (f 10))"); assert_eq!(result0.0, "return _flet(10)\n"); assert_eq!(result0.1, "static func _flet(x):\n return x + 1\n"); } #[test] pub fn semiglobal_labels_test_indirect() { let result0 = parse_compile_and_output_h("(labels ((f (x) (+ x 1))) (funcall (function f) 10))"); assert_eq!(result0.0, "return GDLisp.funcall(_FunctionRefBlock.new(), GDLisp.cons(10, null))\n"); assert_eq!(result0.1, r#"static func _flet(x): return x + 1 class _FunctionRefBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(arg0): return load("res://TEST.gd")._flet(arg0) func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn recursive_single_labels_test() { let result0 = parse_compile_and_output_h("(labels ((f (x) (f x))) (f 1))"); assert_eq!(result0.0, "var _locals = _Labels.new()\nreturn _locals._fn_f_0(1)\n"); assert_eq!(result0.1, r#"class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_f_0(x) "#); } #[test] pub fn recursive_single_labels_with_contrived_local_name_test() { let result = parse_compile_decl("((defconst x 0) (defn test () (let ((x 1)) (labels ((f (x_0) (f x_0) (f x))) (f 1)))))"); assert_eq!(result, r#"extends Reference const x = 0 class _Labels extends Reference: var x_0 func _init(x_0): self.x_0 = x_0 func _fn_f_0(x_0_0): _fn_f_0(x_0_0) return _fn_f_0(x_0) static func test(): var x_0 = 1 var _locals = _Labels.new(x_0) return _locals._fn_f_0(1) "#); } #[test] pub fn recursive_double_labels_test() { let result0 = parse_compile_and_output_h("(labels ((f (x) (g x)) (g (x) (f x))) (g (f 1)))"); assert_eq!(result0.0, "var _locals = _Labels.new()\nreturn _locals._fn_g_1(_locals._fn_f_0(1))\n"); assert_eq!(result0.1, r#"class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_g_1(x) func _fn_g_1(x): return _fn_f_0(x) "#); } #[test] pub fn recursive_single_with_extra_beginning_labels_test() { let result0 = parse_compile_and_output_h("(labels ((f (x) (f (g x))) (g (x) 10)) (f 1))"); assert_eq!(result0.0, "var _locals = _Labels.new()\nreturn _locals._fn_f_1(1)\n"); assert_eq!(result0.1, r#"static func _flet(x): return 10 class _Labels extends Reference: func _init(): pass func _fn_f_1(x): return _fn_f_1(__gdlisp_outer_class_0._flet(x)) var __gdlisp_outer_class_0 = load("res://TEST.gd") "#); } #[test] pub fn recursive_single_with_extra_end_labels_test() { let result0 = parse_compile_and_output_h("(labels ((f (x) (f x)) (g (x) (f x))) (g 1))"); assert_eq!(result0.0, "var _locals = _Labels.new()\nvar _flet = _LambdaBlock.new(_locals)\nreturn _flet.call_func(1)\n"); assert_eq!(result0.1, r#"class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_f_0(x) class _LambdaBlock extends GDLisp.Function: var _locals func _init(_locals): self._locals = _locals self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(x): return _locals._fn_f_0(x) func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn recursive_single_indirect_labels_test() { let result0 = parse_compile_and_output_h("(labels ((f (x) (f x))) (funcall (function f) 1))"); assert_eq!(result0.0, "var _locals = _Labels.new()\nreturn GDLisp.funcall(_FunctionRefBlock.new(_locals), GDLisp.cons(1, null))\n"); assert_eq!(result0.1, r#"class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_f_0(x) class _FunctionRefBlock extends GDLisp.Function: var _locals func _init(_locals): self._locals = _locals self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(arg0): return _locals._fn_f_0(arg0) func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn recursive_single_labels_decl_test_1() { let result = parse_compile_decl("((defn test () (labels ((f (x) (f x))) (f 1))))"); assert_eq!(result, r#"extends Reference class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_f_0(x) static func test(): var _locals = _Labels.new() return _locals._fn_f_0(1) "#); } #[test] pub fn recursive_single_labels_decl_test_2() { // Contrived introduction of a constant named _locals. The // contextual name generator should detect this and work around it. let result = parse_compile_decl("((defconst _locals 0) (defn test () (labels ((f (x) (f x))) (f 1))))"); assert_eq!(result, r#"extends Reference const _locals = 0 class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_f_0(x) static func test(): var _locals_0 = _Labels.new() return _locals_0._fn_f_0(1) "#); } #[test] pub fn contrived_nested_labels_test() { // Contrived introduction of a constant named _locals. The // contextual name generator should detect this and work around it. let result = parse_compile_decl(r#"((defn test () (labels ((f (x) (f x))) (labels ((g (x) (f x) (g x))) (f 1) (g 2))) (labels ((f (x) (f x))) (labels ((g (x) (f x) (g x))) (f 1) (g 2)))))"#); assert_eq!(result, r#"extends Reference class _Labels extends Reference: func _init(): pass func _fn_f_0(x): return _fn_f_0(x) class _Labels_0 extends Reference: var _locals func _init(_locals): self._locals = _locals func _fn_g_1(x): _locals._fn_f_0(x) return _fn_g_1(x) class _Labels_1 extends Reference: func _init(): pass func _fn_f_2(x): return _fn_f_2(x) class _Labels_2 extends Reference: var _locals_1 func _init(_locals_1): self._locals_1 = _locals_1 func _fn_g_3(x): _locals_1._fn_f_2(x) return _fn_g_3(x) static func test(): var _locals = _Labels.new() var _locals_0 = _Labels_0.new(_locals) _locals._fn_f_0(1) _locals_0._fn_g_1(2) var _locals_1 = _Labels_1.new() var _locals_2 = _Labels_2.new(_locals_1) _locals_1._fn_f_2(1) return _locals_2._fn_g_3(2) "#); } ================================================ FILE: tests/test/lambda_class_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use gdlisp::ir::identifier::ClassNamespace; use super::common::*; #[test] pub fn basic_lambda_class_test() { let result0 = parse_compile_and_output_h("(new Node)"); assert_eq!(result0.0, "return _AnonymousClass.new()\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(): pass "#); let result1 = parse_compile_and_output_h("(new Node (defn foo (x) (+ x 1)))"); assert_eq!(result1.0, "return _AnonymousClass.new()\n"); assert_eq!(result1.1, r#"class _AnonymousClass extends Node: func _init(): pass func foo(x): return x + 1 "#); } #[test] pub fn constructor_lambda_class_test() { let result0 = parse_compile_and_output_h("(new Node (defvar x) (defn _init () (set self:x 1)) (defn foo () self:x))"); assert_eq!(result0.0, "return _AnonymousClass.new()\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(): self.x = 1 var x func foo(): return self.x "#); } #[test] pub fn constructor_args_lambda_class_test() { let result0 = parse_compile_and_output_h("(new (Node 99) (defvar x) (defvar y) (defn _init (y) (set self:x 1) (set self:y y)) (defn foo () [self:x self:y]))"); assert_eq!(result0.0, "return _AnonymousClass.new(99)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(y): self.x = 1 self.y = y var x var y func foo(): return [self.x, self.y] "#); } #[test] pub fn constructor_init_args_lambda_class_test() { let result0 = parse_compile_and_output_h("(new (Node 99) (defvar x) (defvar y) (defn _init (@x y) (set self:y y)) (defn foo () [self:x self:y]))"); assert_eq!(result0.0, "return _AnonymousClass.new(99)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(x_0, y): self.x = x_0 self.y = y var x var y func foo(): return [self.x, self.y] "#); } #[test] pub fn closure_lambda_class_test() { let result0 = parse_compile_and_output_h("(let ((a 1)) (new Node (defn foo (x) (+ x a))))"); assert_eq!(result0.0, "var a = 1\nreturn _AnonymousClass.new(a)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(a): self.a = a var a func foo(x): return x + a "#); } #[test] pub fn closure_and_args_lambda_class_test() { let result0 = parse_compile_and_output_h("(let ((a 1)) (new (Node 77) (defvar z) (defn _init (z) (set self:z z)) (defn foo () (+ self:z a))))"); assert_eq!(result0.0, "var a = 1\nreturn _AnonymousClass.new(a, 77)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(a, z): self.a = a self.z = z var a var z func foo(): return self.z + a "#); } #[test] pub fn closure_and_init_args_lambda_class_test_1() { let result0 = parse_compile_and_output_h("(let ((a 1)) (new (Node 77) (defvar z) (defvar t) (defn _init (z @t) (set self:z z)) (defn foo () (+ self:z a))))"); assert_eq!(result0.0, "var a = 1\nreturn _AnonymousClass.new(a, 77)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(a, z, t_0): self.a = a self.t = t_0 self.z = z var a var z var t func foo(): return self.z + a "#); } #[test] pub fn closure_and_init_args_lambda_class_test_2() { let result0 = parse_compile_and_output_h("(let ((a 1)) (new (Node 77) (defvar w (sys/split 1)) (defvar z) (defvar t) (defn _init (z @t) (set self:z z)) (defn foo () (+ self:z a))))"); assert_eq!(result0.0, "var a = 1\nreturn _AnonymousClass.new(a, 77)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(a, z, t_0): var _split = 1 self.w = _split self.a = a self.t = t_0 self.z = z var a var w var z var t func foo(): return self.z + a "#); } #[test] pub fn closure_and_args_lambda_class_with_name_conflict_test() { let result0 = parse_compile_and_output_h("(let ((z 1)) (new (Node 77) (defvar z) (defn _init (z) (set self:z z)) (defn foo () (+ self:z z))))"); assert_eq!(result0.0, "var z = 1\nreturn _AnonymousClass.new(z, 77)\n"); assert_eq!(result0.1, r#"class _AnonymousClass extends Node: func _init(z_0, z_1): self.z_0 = z_0 self.z = z_1 var z_0 var z func foo(): return self.z + z_0 "#); } #[test] pub fn closure_and_args_lambda_class_with_argument_name_conflict_test() { let result0 = parse_compile_decl("((defn foo (z) (new (Node 77) (defvar z) (defn _init (z) (set self:z z)) (defn foo () (+ self:z z)))))"); assert_eq!(result0, r#"extends Reference class _AnonymousClass extends Node: func _init(z_0, z_1): self.z_0 = z_0 self.z = z_1 var z_0 var z func foo(): return self.z + z_0 static func foo(z): return _AnonymousClass.new(z, 77) "#); } #[test] pub fn capture_self_lambda_class_test() { let result = parse_compile_decl("((defclass Foo (Reference) (defn f () (new Reference (defn g () self)))))"); assert_eq!(result, r#"extends Reference class _AnonymousClass extends Reference: func _init(): pass func g(): return self class Foo extends Reference: func _init(): pass func f(): return _AnonymousClass.new() "#); } #[test] pub fn constructor_uses_outer_ref_lambda_class_test() { let result = parse_compile_decl("((defn foo () 1) (defn bar () (new Reference (defn _init () (foo)))))"); assert_eq!(result, r#"extends Reference static func foo(): return 1 class _AnonymousClass extends Reference: func _init(): __gdlisp_outer_class_0.foo() var __gdlisp_outer_class_0 = load("res://TEST.gd") static func bar(): return _AnonymousClass.new() "#); } #[test] pub fn constructor_uses_outer_ref_lambda_class_test_run() { let result = parse_and_run("((defn foo () (print 10)) (new Reference (defn _init () (foo))))"); assert_eq!(result, "\n10\n"); } #[test] pub fn bad_static_in_lambda_class_test() { assert_eq!( parse_compile_and_output_err("(new (Node) (defn example () static 1))"), Err(PError::from(GDError::new(GDErrorF::StaticOnLambdaClass(String::from("example")), SourceOffset(13)))), ); } #[test] pub fn bad_const_in_lambda_class_test() { assert_eq!( parse_compile_and_output_err("(new (Node) (defconst FOO 1))"), Err(PError::from(GDError::new(GDErrorF::StaticOnLambdaClass(String::from("FOO")), SourceOffset(13)))), ); } #[test] pub fn lambda_class_running_test_1() { let output = parse_and_run("((let ((x (new Reference (defn foo () 100)))) (print (x:foo))))"); assert_eq!(output, "\n100\n"); } #[test] pub fn lambda_class_running_test_2() { let output = parse_and_run("((let ((x (let ((y 99)) (new Reference (defn foo () y))))) (print (x:foo))))"); assert_eq!(output, "\n99\n"); } #[test] pub fn lambda_class_running_test_3() { let output = parse_and_run(r#" ((let ((inst (let ((x 768)) (flet ((f () x)) (new Reference (defn foo () (f))))))) (print (inst:foo)))) "#); assert_eq!(output, "\n768\n"); } #[test] pub fn lambda_class_running_test_4() { let output = parse_and_run(r#" ((let ((inst (let ((x 768)) (labels ((f () (if #f (f) x))) (new Reference (defn foo () (funcall #'f))))))) (print (inst:foo)))) "#); assert_eq!(output, "\n768\n"); } #[test] pub fn lambda_class_running_test_5() { // Note: This one requires x to be wrapped in a Cell. let output = parse_and_run(r#" ((let ((x 1)) (let ((foo (new Reference (defn increment () (set x (+ x 1)))))) (print x) (print (foo:increment)) (print x)))) "#); assert_eq!(output, "\n1\n2\n2\n"); } #[test] pub fn lambda_class_running_test_6() { let output = parse_and_run("((let ((x (let ((y 99)) (new (Reference 1) (defvar z) (defn _init (z) (set self:z z)) (defn foo () (+ self:z y)))))) (print (x:foo))))"); assert_eq!(output, "\n100\n"); } #[test] pub fn lambda_class_access_static_fn_test_1() { let output = parse_and_run("((defn foo () 100) (let ((x (new Reference (defn test () (foo))))) (print (x:test))))"); assert_eq!(output, "\n100\n"); } #[test] pub fn lambda_class_access_static_fn_test_2() { let output = parse_and_run("((defn foo () 100) (let ((x (new Reference (defvar x) (defn _init () (set self:x (foo)))))) (print x:x)))"); assert_eq!(output, "\n100\n"); } #[test] pub fn lambda_class_super_test_1() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defn foo () 99)) (let ((x (new Foo (defn foo () (+ (super:foo) 2))))) (print (x:foo)))) "#); assert_eq!(output, "\n101\n"); } #[test] pub fn lambda_class_super_test_2() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defn foo () 99)) (let ((x (new Foo (defn foo () (lambda () (+ 2 (super:foo))))))) (print (funcall (x:foo))))) "#); assert_eq!(output, "\n101\n"); } #[test] pub fn lambda_class_init_test() { let output = parse_and_run(r#" ((defclass Foo (Reference) (defvar x) (defn _init (@x))) (let ((foo (new (Foo 100) (defn _init (x) (super x))))) (print foo:x))) "#); assert_eq!(output, "\n100\n"); } #[test] pub fn lambda_class_duplicate_constructor_test() { assert_eq!( parse_compile_decl_err("((defn function-name () (new Node (defn _init ()) (defn _init ()))))"), Err(PError::from(GDError::new(GDErrorF::DuplicateConstructor, SourceOffset(51)))), ); } #[test] pub fn lambda_class_duplicate_name_test() { assert_eq!( parse_compile_decl_err("((defn function-name () (new Node (defn foo ()) (defn foo ()))))"), Err(PError::from(GDError::new(GDErrorF::DuplicateName(ClassNamespace::Function, String::from("foo")), SourceOffset(49)))), ); } #[test] pub fn initialized_var_in_lambda_class_test() { let result = parse_compile_and_output_h("(new (Node) (defvar example []))"); assert_eq!(result.0, "return _AnonymousClass.new()\n"); assert_eq!(result.1, r#"class _AnonymousClass extends Node: func _init(): pass var example = [] "#); } ================================================ FILE: tests/test/lambda_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; #[test] fn simple_lambda_running_test() { let output = parse_and_run("((print (funcall (lambda (a) a) 1)))"); assert_eq!(output, "\n1\n"); } #[test] fn closure_lambda_running_test() { let output = parse_and_run(r#" ((let ((a (let ((x 99)) (let ((f (lambda (z) x))) f)))) (print (funcall a ())))) "#); assert_eq!(output, "\n99\n"); } #[test] fn modifying_closure_test() { let output = parse_and_run(r#" ((let ((a 0)) (funcall (lambda () (set a 1))) (print a))) "#); assert_eq!(output, "\n1\n"); } #[test] fn modifying_closure_inside_lambda_test() { // Basically, modifying_closure_test stuffed inside an extra lambda layer and then called. let output = parse_and_run(r#" ((let ((f (lambda (x) (funcall (lambda () (set x 1))) x))) (print (funcall f 0)))) "#); assert_eq!(output, "\n1\n"); } #[test] fn lambda_access_outer_function_test() { let output = parse_and_run(r#" ((defn foo () 100) (let ((f (lambda () (foo)))) (print (funcall f)))) "#); assert_eq!(output, "\n100\n"); } #[test] fn simple_function_ref_run_test() { let output = parse_and_run(r#" ((defn foo () 100) (let ((f #'foo)) (print (funcall f)))) "#); assert_eq!(output, "\n100\n"); } #[test] fn nonexistent_var_in_lambda_test() { let output = parse_compile_and_output_err(r#"(lambda () x)"#); assert_eq!(output, Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("x")), SourceOffset(11))))); } #[test] fn nonexistent_fn_in_lambda_test() { let output = parse_compile_and_output_err(r#"(lambda () (abc))"#); assert_eq!(output, Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("abc")), SourceOffset(11))))); } #[test] pub fn basic_lambda_test() { let result0 = parse_compile_and_output_h("(lambda ())"); assert_eq!(result0.0, "return _LambdaBlock.new()\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return null func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); let result1 = parse_compile_and_output_h("(lambda (a) a)"); assert_eq!(result1.0, "return _LambdaBlock.new()\n"); assert_eq!(result1.1, r#"class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(a): return a func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); let result2 = parse_compile_and_output_h("(progn (lambda (a) a) 1)"); assert_eq!(result2.0, "return 1\n"); assert_eq!(result2.1, r#"class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(a): return a func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn closure_lambda_test() { let result0 = parse_compile_and_output_h("(let (a) (lambda () a))"); assert_eq!(result0.0, "var a = null\nreturn _LambdaBlock.new(a)\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: var a func _init(a): self.a = a self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return a func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); } #[test] pub fn non_closure_lambda_test() { let result0 = parse_compile_and_output_h("(let (a) (lambda () (let (a) a)))"); assert_eq!(result0.0, "var a = null\nreturn _LambdaBlock.new()\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): var a = null return a func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); } #[test] pub fn basic_funcall_test() { assert_eq!(parse_compile_and_output("(funcall 1)"), "return GDLisp.funcall(1, null)\n"); assert_eq!(parse_compile_and_output("(progn (funcall 1) 2)"), "GDLisp.funcall(1, null)\nreturn 2\n"); assert_eq!(parse_compile_and_output("(funcall 1 2 3)"), "return GDLisp.funcall(1, GDLisp.cons(2, GDLisp.cons(3, null)))\n"); } #[test] pub fn funcall_lambda_test() { let result0 = parse_compile_and_output_h("(let ((f (lambda (a) a))) (funcall f 100))"); assert_eq!(result0.0, "var f = _LambdaBlock.new()\nreturn GDLisp.funcall(f, GDLisp.cons(100, null))\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(a): return a func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn function_ref_test() { let result0 = parse_compile_and_output_h("(function foo1)"); assert_eq!(result0.0, "return _FunctionRefBlock.new()\n"); assert_eq!(result0.1, r#"class _FunctionRefBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(arg0): return load("res://TEST.gd").foo1(arg0) func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); let result1 = parse_compile_and_output_h("#'foo1"); assert_eq!(result1.0, "return _FunctionRefBlock.new()\n"); assert_eq!(result1.1, r#"class _FunctionRefBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 1 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(arg0): return load("res://TEST.gd").foo1(arg0) func call_funcv(args): var required_0 = null if args == null: push_error("Not enough arguments") else: required_0 = args.car args = args.cdr if args == null: return call_func(required_0) else: push_error("Too many arguments") "#); } #[test] pub fn outer_class_in_nested_lambda_test() { let result = parse_and_run(r#" ((defn example () 3) (defn foo () (lambda () (lambda () (example)))) (let ((fn (funcall (foo)))) (print (funcall fn)))) "#); assert_eq!(result, "\n3\n"); } #[test] pub fn outer_class_in_nested_lambda_class_test_1() { let result = parse_and_run(r#" ((defn example () 3) (defn foo () (new (Reference) (defn bar () (lambda () (example))))) (let ((x (foo))) (print (funcall (x:bar))))) "#); assert_eq!(result, "\n3\n"); } #[test] pub fn outer_class_in_nested_lambda_class_test_2() { let result = parse_and_run(r#" ((defn example () 3) (defn foo () (lambda () (new (Reference) (defn bar () (example))))) (let ((x (funcall (foo)))) (print (x:bar)))) "#); assert_eq!(result, "\n3\n"); } #[test] pub fn outer_class_in_nested_lambda_class_test_3() { let result = parse_and_run(r#" ((defn example () 3) (defn foo () (new (Reference) (defn bar () (new Reference (defn bar () (example)))))) (let ((x (foo))) (print ((x:bar):bar)))) "#); assert_eq!(result, "\n3\n"); } #[test] pub fn several_nested_lambdas_test() { // This is a regression test for issue #139. let result = parse_and_run(r#" ((defn foo1 () (lambda () (lambda () (lambda ())))) (defn foo2 () (lambda () (lambda ()))) (defn foo3 () (lambda () (lambda () (lambda ())))) (defn foo4 () (lambda ())) (print 1)) "#); assert_eq!(result, "\n1\n"); } #[test] pub fn several_nested_lambda_classes_test() { // This is a regression test for issue #139. let result = parse_and_run(r#" ((defn foo1 () (new Reference (defn foo () (new Reference)))) (defn foo2 () (new Reference)) (defn foo3 () (new Reference)) (print 1)) "#); assert_eq!(result, "\n1\n"); } #[test] pub fn several_nested_labels_test() { // This is a regression test for issue #139. let result = parse_and_run(r#" ((defn foo1 () (labels ((f (x) (f x) (labels ((g (x) (g x))) (f x)))))) (defn foo2 () (labels ((f (x) (f x) (labels ((g (x) (g x))) (f x)))))) (print 1)) "#); assert_eq!(result, "\n1\n"); } ================================================ FILE: tests/test/lazy_val_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use super::common::*; #[test] pub fn simple_lazy_test() { assert_eq!(parse_compile_decl("((deflazy x 100))"), r#"extends Reference static func _lazy_0(): var _this_file_1 = load("res://TEST.gd") var _cond = null if _this_file_1.has_meta("__gdlisp_Lazy__G_3"): _cond = _this_file_1.get_meta("__gdlisp_Lazy__G_3") else: if true: var _value_2 = 100 _this_file_1.set_meta("__gdlisp_Lazy__G_3", _value_2) _cond = _value_2 else: _cond = null return _cond static func __gdlisp_SymbolMacroFunction_x(): return GDLisp.cons(GDLisp.cons(GDLisp.intern("access-slot"), GDLisp.cons(GDLisp.cons(GDLisp.intern("contextual-load"), GDLisp.cons("res://TEST.gd", null)), GDLisp.cons(GDLisp.intern("_lazy_0"), null))), null) "#); } #[test] pub fn simple_private_lazy_test() { // The private modifier should parse and work correctly, but it // doesn't change the output compared to the simple_lazy_test case. assert_eq!(parse_compile_decl("((deflazy x 100 private))"), r#"extends Reference static func _lazy_0(): var _this_file_1 = load("res://TEST.gd") var _cond = null if _this_file_1.has_meta("__gdlisp_Lazy__G_3"): _cond = _this_file_1.get_meta("__gdlisp_Lazy__G_3") else: if true: var _value_2 = 100 _this_file_1.set_meta("__gdlisp_Lazy__G_3", _value_2) _cond = _value_2 else: _cond = null return _cond static func __gdlisp_SymbolMacroFunction_x(): return GDLisp.cons(GDLisp.cons(GDLisp.intern("access-slot"), GDLisp.cons(GDLisp.cons(GDLisp.intern("contextual-load"), GDLisp.cons("res://TEST.gd", null)), GDLisp.cons(GDLisp.intern("_lazy_0"), null))), null) "#); } #[test] pub fn simple_lazy_run_test_1() { assert_eq!(parse_and_run(r#" ((deflazy x 100) (print x)) "#), "\n100\n"); } #[test] pub fn simple_lazy_run_test_2() { // The 3 should only print once assert_eq!(parse_and_run(r#" ((deflazy x (progn (print 3) 100)) (print x) (print x)) "#), "\n3\n100\n100\n"); } // TODO Test lazy vals getting reinitialized after a file is unloaded and reloaded ================================================ FILE: tests/test/let_var_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::{parse_compile_and_output, parse_compile_and_output_h}; #[test] pub fn let_tests() { assert_eq!(parse_compile_and_output("(let () 1)"), "return 1\n"); assert_eq!(parse_compile_and_output("(let (a) 1)"), "var a = null\nreturn 1\n"); assert_eq!(parse_compile_and_output("(let ((a)) 1)"), "var a = null\nreturn 1\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) (foo1 a))"), "var a = 1\nreturn foo1(a)\n"); assert_eq!(parse_compile_and_output("(let ((a 1) (b 2)) (foo2 a b))"), "var a = 1\nvar b = 2\nreturn foo2(a, b)\n"); assert_eq!(parse_compile_and_output("(let ((a (foo) (bar))) (foo1 a))"), "foo()\nvar a = bar()\nreturn foo1(a)\n"); assert_eq!(parse_compile_and_output("(let ((a) b) 1)"), "var a = null\nvar b = null\nreturn 1\n"); assert_eq!(parse_compile_and_output("(let (a (b)) 1)"), "var a = null\nvar b = null\nreturn 1\n"); } #[test] pub fn let_name_trans_tests() { assert_eq!(parse_compile_and_output("(let ((a-b 1)) a-b)"), "var a_b = 1\nreturn a_b\n"); } #[test] pub fn var_shadowing() { assert_eq!(parse_compile_and_output("(let ((a)) (let ((a a)) a))"), "var a = null\nvar a_0 = a\nreturn a_0\n"); } #[test] pub fn inline_if_in_let_test() { assert_eq!(parse_compile_and_output("(let ((a (if (foo) (bar) (foo)))) a)"), "var _cond = null\nif foo():\n _cond = bar()\nelse:\n if true:\n _cond = foo()\n else:\n _cond = null\nvar a = _cond\nreturn a\n"); } #[test] pub fn closure_var_test() { let result0 = parse_compile_and_output_h("(lambda () foobar)"); assert_eq!(result0.0, "return _LambdaBlock.new(foobar)\n"); assert_eq!(result0.1, r#"class _LambdaBlock extends GDLisp.Function: var foobar func _init(foobar): self.foobar = foobar self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return foobar func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); let result1 = parse_compile_and_output_h("(lambda () glob)"); assert_eq!(result1.0, "return _LambdaBlock.new()\n"); assert_eq!(result1.1, r#"class _LambdaBlock extends GDLisp.Function: func _init(): self.__gdlisp_required = 0 self.__gdlisp_optional = 0 self.__gdlisp_rest = 0 func call_func(): return glob func call_funcv(args): if args == null: return call_func() else: push_error("Too many arguments") "#); } #[test] pub fn let_star_test_1() { assert_eq!(parse_compile_and_output("(let* ((a 1) (b a)) b)"), "var a = 1\nvar b = a\nreturn b\n"); } #[test] pub fn let_star_test_2() { assert_eq!(parse_compile_and_output("(let* ((a 1) (a a) (a a)) a)"), "var a = 1\nvar a_0 = a\nvar a_1 = a_0\nreturn a_1\n"); } ================================================ FILE: tests/test/list_operator_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; use gdlisp::pipeline::error::PError; #[test] pub fn simple_length_test() { assert_eq!(parse_compile_and_output("(len ())"), "return GDLisp._len(null)\n"); } #[test] pub fn list_test() { assert_eq!(parse_compile_and_output("(list 1 2 3)"), "return GDLisp.cons(1, GDLisp.cons(2, GDLisp.cons(3, null)))\n"); } #[test] pub fn array_function_test() { assert_eq!(parse_compile_and_output("(array 1 2 3)"), "return [1, 2, 3]\n"); } #[test] pub fn array_test() { assert_eq!(parse_compile_and_output("[]"), "return []\n"); assert_eq!(parse_compile_and_output("[1 2 3]"), "return [1, 2, 3]\n"); assert_eq!(parse_compile_and_output("[2]"), "return [2]\n"); assert_eq!(parse_compile_and_output("[(foo)]"), "return [foo()]\n"); assert_eq!(parse_compile_and_output("(progn [(foo)] [2])"), "[foo()]\nreturn [2]\n"); assert_eq!(parse_compile_and_output("[(if 1 2 3)]"), "var _cond = null\nif 1:\n _cond = 2\nelse:\n if true:\n _cond = 3\n else:\n _cond = null\nreturn [_cond]\n"); } #[test] pub fn reverse_test_1() { let result = parse_and_run(r#" ((print (array/reverse [1 2 3 4]))) "#); assert_eq!(result, "\n[4, 3, 2, 1]\n"); } #[test] pub fn reverse_test_2() { let result = parse_and_run(r#" ((let ((list (list/reverse '(1 2 3 4)))) (print (len list)) (print (list/elt list 0)) (print (list/elt list 1)) (print (list/elt list 2)) (print (list/elt list 3)))) "#); assert_eq!(result, "\n4\n4\n3\n2\n1\n"); } #[test] pub fn map_test_1() { let result = parse_and_run(r#" ((print (array/map (lambda (x) (+ x 1)) [4 5 6]))) "#); assert_eq!(result, "\n[5, 6, 7]\n"); } #[test] pub fn map_test_2() { let result = parse_and_run(r#" ((let ((x (list/map (lambda (x) (+ x 1)) '(4 5 6)))) (print (len x)) (print x:car) (print x:cdr:car) (print x:cdr:cdr:car))) "#); assert_eq!(result, "\n3\n5\n6\n7\n"); } #[test] pub fn filter_test_1() { let result = parse_and_run(r#" ((print (array/filter (lambda (x) (= (mod x 2) 0)) [1 2 3 4 5 6]))) "#); assert_eq!(result, "\n[2, 4, 6]\n"); } #[test] pub fn filter_test_2() { let result = parse_and_run(r#" ((let ((x (list/filter (lambda (x) (= (mod x 2) 0)) '(1 2 3 4 5 6)))) (print (len x)) (print x:car) (print x:cdr:car) (print x:cdr:cdr:car))) "#); assert_eq!(result, "\n3\n2\n4\n6\n"); } #[test] pub fn filter_test_3() { let result = parse_and_run(r#" ((let ((x (list/filter (lambda (x) #f) '(1 2 3 4 5 6)))) (print (len x)))) "#); assert_eq!(result, "\n0\n"); } #[test] pub fn length_test() { assert_eq!(parse_and_run("((print (len nil)))"), "\n0\n"); assert_eq!(parse_and_run("((print (len '(1 2 3 4))))"), "\n4\n"); assert_eq!(parse_and_run("((print (len [1 2 3 4])))"), "\n4\n"); } #[test] pub fn append_test_1() { let result = parse_and_run(r#" ((print (list->array (append)))) "#); assert_eq!(result, "\n[]\n"); } #[test] pub fn append_test_2() { let result = parse_and_run(r#" ((print (list->array (append '(1 2 3 4))))) "#); assert_eq!(result, "\n[1, 2, 3, 4]\n"); } #[test] pub fn append_test_3() { let result = parse_and_run(r#" ((print (list->array (append '(1 2 3 4) '(5 6 7 8))))) "#); assert_eq!(result, "\n[1, 2, 3, 4, 5, 6, 7, 8]\n"); } #[test] pub fn append_test_4() { let result = parse_and_run(r#" ((print (list->array (append '(1 2 3 4) () '(5 6 7 8) () ())))) "#); assert_eq!(result, "\n[1, 2, 3, 4, 5, 6, 7, 8]\n"); } #[test] pub fn array_running_test() { let result = parse_and_run(r#" ((print (array 1 2 3))) "#); assert_eq!(result, "\n[1, 2, 3]\n"); } #[test] pub fn array_syntax_running_test() { let result = parse_and_run(r#" ((print [1 2 3])) "#); assert_eq!(result, "\n[1, 2, 3]\n"); } #[test] pub fn array_running_test_indirect() { let result = parse_and_run(r#" ((print (funcall #'array 1 2 3))) "#); assert_eq!(result, "\n[1, 2, 3]\n"); } #[test] pub fn dict_running_test() { let result = parse_and_run(r#" ((print (dict 1 2 3 4))) "#); assert_eq!(result, "\n{1:2, 3:4}\n"); } #[test] pub fn dict_syntax_running_test() { let result = parse_and_run(r#" ((print {1 2 3 4})) "#); assert_eq!(result, "\n{1:2, 3:4}\n"); } #[test] pub fn dict_running_test_indirect() { let result = parse_and_run(r#" ((print (funcall #'dict 1 2 3 4))) "#); assert_eq!(result, "\n{1:2, 3:4}\n"); } #[test] pub fn dict_syntax_odd_error_test() { let result = parse_compile_and_output_err(r#" ((print {1})) "#); assert!(matches!(result, Err(PError::ParseError(_)))); } #[test] pub fn list_tail_test_1() { let result = parse_and_run(r#" ((print (list->array (list/tail '(1 2 3 4) 0)))) "#); assert_eq!(result, "\n[1, 2, 3, 4]\n"); } #[test] pub fn list_tail_test_2() { let result = parse_and_run(r#" ((print (list->array (list/tail '(1 2 3 4) 2)))) "#); assert_eq!(result, "\n[3, 4]\n"); } #[test] pub fn list_elt_test_1() { let result = parse_and_run(r#" ((print (list/elt '(1 2 3 4) 0))) "#); assert_eq!(result, "\n1\n"); } #[test] pub fn list_elt_test_2() { let result = parse_and_run(r#" ((print (list/elt '(1 2 3 4) 2))) "#); assert_eq!(result, "\n3\n"); } #[test] pub fn list_foreach_test() { let result = parse_and_run(r#" ((let ((foo '(1 2 3 4 5)) (total 0)) (list/for x foo (set total (+ total x))) (print total))) "#); assert_eq!(result, "\n15\n"); } #[test] pub fn fold_test_1() { let result = parse_and_run(r#" ((let ((foo '(1 2 3 4 5))) (print (list/fold #'+ foo)) (print (list/fold #'+ foo 100)))) "#); assert_eq!(result, "\n15\n115\n"); } #[test] pub fn fold_test_2() { let result = parse_and_run(r#" ((let ((foo [1 2 3 4 5])) (print (array/fold #'+ foo)) (print (array/fold #'+ foo 100)))) "#); assert_eq!(result, "\n15\n115\n"); } ================================================ FILE: tests/test/macro_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use gdlisp::ir::modifier::{ParseError as ModifierParseError, ParseErrorF as ModifierParseErrorF}; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::compile::args::Expecting; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use super::common::*; #[test] pub fn simple_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo (x) x))"), r#"extends Reference static func foo(x): return x "#) } #[test] pub fn constant_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo () 10) (defn bar () (foo)))"), r#"extends Reference static func foo(): return 10 static func bar(): return 10 "#); } #[test] pub fn symbol_macro_test() { assert_eq!(parse_compile_decl("((define-symbol-macro foo 10) (defn bar () foo))"), r#"extends Reference static func __gdlisp_SymbolMacroFunction_foo(): return 10 static func bar(): return 10 "#); } #[test] pub fn builtin_symbol_macro_test() { assert_eq!(parse_compile_decl("((defn run () [PI SPKEY (let ((PI 1)) PI)]))"), r#"extends Reference static func run(): var _PI = 1 return [PI, SPKEY, _PI] "#); } #[test] pub fn arithmetic_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo (x) (+ x 100)) (defn run () (foo 9)))"), r#"extends Reference static func foo(x): return x + 100 static func run(): return 109 "#); } #[test] pub fn quote_macro_test() { assert_eq!(parse_compile_decl("((defmacro my-quote (x) (cons 'quote (cons x ()))) (defn run () (my-quote abc)))"), r#"extends Reference static func my_quote(x): return GDLisp.cons(GDLisp.intern("quote"), GDLisp.cons(x, null)) static func run(): return GDLisp.intern("abc") "#); } #[test] pub fn macro_in_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo (x) (+ x 1)) (defmacro bar () (foo 0)) (defn run () (bar)))"), r#"extends Reference static func foo(x): return x + 1 static func bar(): return 1 static func run(): return 1 "#); } #[test] pub fn macro_from_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo (x) (+ x 1)) (defmacro bar (x) (cons 'foo (cons x ()))) (defn baz () (bar 2)))"), r#"extends Reference static func foo(x): return x + 1 static func bar(x): return GDLisp.cons(GDLisp.intern("foo"), GDLisp.cons(x, null)) static func baz(): return 3 "#); } #[test] pub fn bad_args_macro_test() { assert_eq!( parse_compile_decl_err("((defmacro foo (x) (+ x 1)) (defmacro bar (x) (cons 'foo (cons x ()))) (defn run () (bar)))"), Err(PError::from(GDError::new(GDErrorF::WrongNumberArgs(String::from("bar"), Expecting::exactly(1), 0), SourceOffset(84)))), ); } #[test] pub fn rest_args_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo (&rest x) x) (defn bar () (foo + 1 2)))"), r#"extends Reference static func foo(x): return x static func bar(): return 1 + 2 "#); } #[test] pub fn optional_args_macro_test_1() { assert_eq!(parse_compile_decl("((defmacro foo (&opt x) x) (defn bar () (foo 1)))"), r#"extends Reference static func foo(x): return x static func bar(): return 1 "#); } #[test] pub fn optional_args_macro_test_2() { assert_eq!(parse_compile_decl("((defmacro foo (&opt x) x) (defn bar () (foo)))"), r#"extends Reference static func foo(x): return x static func bar(): return null "#); } #[test] pub fn to_decl_macro_test() { assert_eq!(parse_compile_decl("((defmacro foo () '(defn bar () ())) (foo))"), r#"extends Reference static func foo(): return GDLisp.cons(GDLisp.intern("defn"), GDLisp.cons(GDLisp.intern("bar"), GDLisp.cons(null, GDLisp.cons(null, null)))) static func bar(): return null "#); } #[test] pub fn to_progn_macro_test() { assert_eq!(parse_and_run("((defmacro foo () '(progn (defmacro bar (x) x) (print (bar 3)))) (foo))"), "\n3\n"); } #[test] pub fn symbol_macro_run_test() { assert_eq!(parse_and_run("((define-symbol-macro foo (progn 2)) (print foo))"), "\n2\n"); } #[test] pub fn symbol_macro_shadowing_test() { assert_eq!(parse_and_run("((define-symbol-macro foo (progn 2)) (print foo) (print (let ((foo 3)) foo)) (print foo))"), "\n2\n3\n2\n"); } #[test] pub fn symbol_macro_to_macro_test_1() { assert_eq!(parse_and_run("((define-symbol-macro foo '(bar)) (defmacro bar () 9) (print foo))"), "\n9\n"); } #[test] pub fn symbol_macro_to_macro_test_2() { assert_eq!(parse_and_run("((define-symbol-macro foo '(bar)) (defmacro bar () 9) (print (flet ((bar () 10)) foo)))"), "\n10\n"); } #[test] pub fn symbol_macro_to_macro_test_3() { assert_eq!(parse_and_run("((define-symbol-macro foo '(bar)) (defmacro bar () 9) (print (macrolet ((bar () 10)) foo)))"), "\n10\n"); } #[test] pub fn symbol_macro_to_symbol_macro_test_1() { assert_eq!(parse_and_run("((define-symbol-macro foo 'bar) (define-symbol-macro bar 9) (print foo))"), "\n9\n"); } #[test] pub fn symbol_macro_to_symbol_macro_test_2() { assert_eq!(parse_and_run("((define-symbol-macro foo 'bar) (define-symbol-macro bar 9) (print (let ((bar 10)) foo)))"), "\n10\n"); } #[test] pub fn symbol_macro_to_symbol_macro_test_3() { assert_eq!(parse_and_run("((define-symbol-macro foo 'bar) (define-symbol-macro bar 9) (print (flet ((bar () 10)) foo)))"), "\n9\n"); } #[test] pub fn macro_to_symbol_macro_test_1() { assert_eq!(parse_and_run("((defmacro foo () 'bar) (define-symbol-macro bar 9) (print (foo)))"), "\n9\n"); } #[test] pub fn macro_to_symbol_macro_test_2() { assert_eq!(parse_and_run("((defmacro foo () 'bar) (define-symbol-macro bar 9) (print (let ((bar 10)) (foo))))"), "\n10\n"); } #[test] pub fn macro_uses_symbol_macro_test() { assert_eq!(parse_and_run("((define-symbol-macro bar 9) (defmacro foo () bar) (print (let ((bar 10)) (foo))))"), "\n9\n"); } #[test] pub fn symbol_macrolet_out_of_scope_test() { assert_eq!(parse_and_run("((defconst foo 3) (defn bar () foo) (symbol-macrolet ((foo 2)) (print foo) (print (bar))))"), "\n2\n3\n"); } #[test] pub fn macrolet_basic_test() { let result = parse_compile_and_output("(macrolet ((foo () 100)) (foo))"); assert_eq!(result, "return 100\n"); } #[test] pub fn symbol_macrolet_basic_test() { let result = parse_compile_and_output("(symbol-macrolet ((foo 100)) foo)"); assert_eq!(result, "return 100\n"); } #[test] pub fn macrolet_shadowing_test() { let result = parse_compile_and_output("(macrolet ((foo () 100)) [(foo) (macrolet ((foo () 99)) (foo)) (foo)])"); assert_eq!(result, "return [100, 99, 100]\n"); } #[test] pub fn symbol_macrolet_shadowing_test() { let result = parse_compile_and_output("(symbol-macrolet ((foo 100)) [foo (symbol-macrolet ((foo 99)) foo) foo])"); assert_eq!(result, "return [100, 99, 100]\n"); } #[test] pub fn macrolet_global_shadowing_test() { let result = parse_compile_decl("((defmacro foo () 100) (defn run () [(foo) (macrolet ((foo () 99)) (foo)) (foo)]))"); assert_eq!(result, r#"extends Reference static func foo(): return 100 static func run(): return [100, 99, 100] "#); } #[test] pub fn symbol_macrolet_global_shadowing_test() { let result = parse_compile_decl("((defconst foo 100) (defn run () [foo (symbol-macrolet ((foo 99)) foo) foo]))"); assert_eq!(result, r#"extends Reference const foo = 100 static func run(): return [foo, 99, foo] "#); } #[test] pub fn symbol_macro_shared_name_with_function_test() { assert_eq!(parse_and_run(r#"((define-symbol-macro foo 100) (defn foo () 101) (print foo) (print (foo)))"#), "\n100\n101\n"); } #[test] pub fn symbol_macro_shared_name_with_macro_test() { assert_eq!(parse_and_run(r#"((define-symbol-macro foo 100) (defmacro foo () 101) (print foo) (print (foo)))"#), "\n100\n101\n"); } #[test] pub fn macrolet_global_function_shadowing_test_1() { let result = parse_compile_decl("((defn foo () 100) (defn run () [(foo) (macrolet ((foo () 99)) (foo)) (foo)]))"); assert_eq!(result, r#"extends Reference static func foo(): return 100 static func run(): return [foo(), 99, foo()] "#); } #[test] pub fn macrolet_global_function_shadowing_test_2() { let result = parse_compile_decl("((defn foo () 100) (defn run () [(foo) (macrolet ((foo () (foo))) (foo)) (foo)]))"); assert_eq!(result, r#"extends Reference static func foo(): return 100 static func run(): return [foo(), 100, foo()] "#); } #[test] pub fn symbol_macrolet_global_function_shadowing_test_1() { let result = parse_compile_decl("((defconst foo 100) (defn run () [foo (symbol-macrolet ((foo 99)) foo) foo]))"); assert_eq!(result, r#"extends Reference const foo = 100 static func run(): return [foo, 99, foo] "#); } #[test] pub fn symbol_macrolet_global_function_shadowing_test_2() { let result = parse_compile_decl("((defconst foo 100) (defn run () [foo (symbol-macrolet ((foo foo)) foo) foo]))"); assert_eq!(result, r#"extends Reference const foo = 100 static func run(): return [foo, 100, foo] "#); } #[test] pub fn symbol_macrolet_global_function_shadowing_test_3() { let result = parse_compile_decl("((define-symbol-macro foo 100) (defn run () [foo (symbol-macrolet ((foo foo)) foo) foo]))"); assert_eq!(result, r#"extends Reference static func __gdlisp_SymbolMacroFunction_foo(): return 100 static func run(): return [100, 100, 100] "#); } #[test] pub fn symbol_macrolet_global_function_shadowing_test_4() { let result = parse_compile_decl("((define-symbol-macro foo 100) (defn run () [foo (symbol-macrolet ((foo 99)) foo) foo]))"); assert_eq!(result, r#"extends Reference static func __gdlisp_SymbolMacroFunction_foo(): return 100 static func run(): return [100, 99, 100] "#); } #[test] pub fn flet_global_macro_shadowing_test() { let result = parse_compile_decl("((defmacro foo () 100) (defn run () [(foo) (flet ((foo () 99)) (foo)) (foo)]))"); assert_eq!(result, r#"extends Reference static func foo(): return 100 static func _flet(): return 99 static func run(): return [100, _flet(), 100] "#); } #[test] pub fn closure_macrolet_test_1() { assert_eq!( parse_compile_and_output_err("(let ((a 1)) (macrolet ((foo () a)) (foo)))"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("a")), SourceOffset(32)))), ); } #[test] pub fn closure_macrolet_test_2() { assert_eq!( parse_compile_and_output_err("(flet ((f () 1)) (macrolet ((foo () (f))) (foo)))"), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("f")), SourceOffset(36)))), ); } #[test] pub fn labels_global_macro_shadowing_test() { let result = parse_compile_decl("((defmacro foo () 100) (defn run () [(foo) (labels ((foo () (foo))) (foo)) (foo)]))"); assert_eq!(result, r#"extends Reference static func foo(): return 100 class _Labels extends Reference: func _init(): pass func _fn_foo_0(): return _fn_foo_0() static func run(): var _locals = _Labels.new() return [100, _locals._fn_foo_0(), 100] "#); } #[test] pub fn gensym_test_1() { let result = parse_compile_decl("((defmacro foo (a) (let ((x (gensym))) `(let ((,x ,a)) [,x ,x]))) (defn bar () (foo 10)))"); assert_eq!(result, r#"extends Reference static func foo(a): var x = GDLisp.gensym(null) var _quasiquote = null var _quasiquote_0 = GDLisp.cons(x, null) return GDLisp.cons(GDLisp.intern("let"), GDLisp.cons(GDLisp.cons(GDLisp.cons(x, GDLisp.cons(a, _quasiquote)), null), GDLisp.cons(GDLisp.cons(GDLisp.intern("array"), GDLisp.cons(x, _quasiquote_0)), null))) static func bar(): var _G_0 = 10 return [_G_0, _G_0] "#); } #[test] pub fn gensym_test_2() { let result = parse_compile_decl("((defmacro foo (a) (let ((x (gensym))) `(let ((,x ,a)) [,x ,x]))) (defn bar () [(foo 10) '_G_0]))"); assert_eq!(result, r#"extends Reference static func foo(a): var x = GDLisp.gensym(null) var _quasiquote = null var _quasiquote_0 = GDLisp.cons(x, null) return GDLisp.cons(GDLisp.intern("let"), GDLisp.cons(GDLisp.cons(GDLisp.cons(x, GDLisp.cons(a, _quasiquote)), null), GDLisp.cons(GDLisp.cons(GDLisp.intern("array"), GDLisp.cons(x, _quasiquote_0)), null))) static func bar(): var _G_1 = 10 return [[_G_1, _G_1], GDLisp.intern("_G_0")] "#); } #[test] pub fn macro_inner_class_test_1() { let result = parse_compile_decl("((defclass Foo (Reference)) (defmacro foo () (Foo:new) 1) (defn run () (foo)))"); assert_eq!(result, r#"extends Reference class Foo extends Reference: func _init(): pass static func foo(): Foo.new() return 1 static func run(): return 1 "#); } #[test] pub fn macro_inner_class_test_2() { let result = parse_compile_decl("((defclass Foo (Reference) (defn g () (f))) (defn f () 9) (defmacro foo () ((Foo:new):g)) (defn run () (foo)))"); assert_eq!(result, r#"extends Reference class Foo extends Reference: func _init(): pass func g(): return __gdlisp_outer_class_0.f() var __gdlisp_outer_class_0 = load("res://TEST.gd") static func f(): return 9 static func foo(): return Foo.new().g() static func run(): return 9 "#); } #[test] pub fn macro_inner_class_test_3() { let result = parse_compile_decl("((defclass Foo (Reference) (defn g () static (f))) (defn f () 9) (defmacro foo () (Foo:g)) (defn run () (foo)))"); assert_eq!(result, r#"extends Reference class Foo extends Reference: func _init(): pass static func g(): return load("res://TEST.gd").f() static func f(): return 9 static func foo(): return Foo.g() static func run(): return 9 "#); } #[test] pub fn macro_inner_class_test_4() { // Can we do it with inheritance in the way? let result = parse_compile_decl(r#" ((defn add-one (x) (+ x 1)) (defclass Foo (Reference) (defn f () (add-one 10))) (defclass Bar (Foo) (defn g () (add-one 5))) (defmacro foo () (let ((x (Bar:new))) (+ (x:f) (x:g)))) (defn run-test () (foo))) "#); assert_eq!(result, r#"extends Reference static func add_one(x): return x + 1 class Foo extends Reference: func _init(): pass func f(): return __gdlisp_outer_class_0.add_one(10) var __gdlisp_outer_class_0 = load("res://TEST.gd") class Bar extends Foo: func _init(): pass func g(): return __gdlisp_outer_class_1.add_one(5) var __gdlisp_outer_class_1 = load("res://TEST.gd") static func foo(): var x = Bar.new() return x.f() + x.g() static func run_test(): return 17 "#); } #[test] pub fn nonsense_modifier_macro_test() { assert_eq!( parse_compile_decl_err(r#"((defmacro foo () public private 1))"#), Err(PError::from(ModifierParseError::new(ModifierParseErrorF::UniquenessError(String::from("visibility")), SourceOffset(25)))), ); } #[test] pub fn macro_in_minimalist_test() { assert_eq!( parse_compile_decl_err("((sys/nostdlib) (defmacro foo () 10) (foo))"), Err(PError::from(GDError::new(GDErrorF::MacroInMinimalistError(String::from("foo")), SourceOffset(37)))), ); } #[test] pub fn simple_minimalist_test() { assert_eq!(parse_compile_decl("((sys/nostdlib))"), r#"extends Reference func _init(): pass "#); } #[test] pub fn quit_macro_test() { let result = parse_compile_and_output("(quit)"); assert_eq!(result, "return GDLisp.get_tree().quit()\n"); } #[test] pub fn recursive_macro_test() { assert_eq!( parse_compile_decl_err("((defmacro foo () (foo)))"), Err(PError::from(GDError::new(GDErrorF::MacroBeforeDefinitionError(String::from("foo")), SourceOffset(18)))), ); } #[test] pub fn recursive_macrolet_test() { assert_eq!( parse_compile_decl_err("((macrolet ((foo () (foo))) ()))"), Err(PError::from(GDError::new(GDErrorF::NoSuchFn(String::from("foo")), SourceOffset(20)))), ); } #[test] pub fn bad_order_macro_test() { assert_eq!( parse_compile_decl_err("((defn bar () (foo)) (defmacro foo () 1))"), Err(PError::from(GDError::new(GDErrorF::MacroBeforeDefinitionError(String::from("foo")), SourceOffset(14)))), ); } #[test] pub fn bad_preload_in_macro_test() { assert_eq!( parse_compile_decl_err(r#"((defmacro foo () (sys/context-filename "res://not-a-real-file.lisp")))"#), Err(PError::from(GDError::new(GDErrorF::ContextualFilenameUnresolved, SourceOffset(52)))), ); } ================================================ FILE: tests/test/meta_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::parse_and_run; use gdlisp::gdscript::metadata; #[test] fn meta_test_cons() { let code = format!(r#"((let ((arg (cons 1 2))) (print (bool (arg:get-meta "{}")))))"#, metadata::CONS_META); let output = parse_and_run(&code); assert_eq!(output, "\nTrue\n"); } #[test] fn meta_test_cons_quoted() { let code = format!(r#"((let ((arg '(1 . 2))) (print (bool (arg:get-meta "{}")))))"#, metadata::CONS_META); let output = parse_and_run(&code); assert_eq!(output, "\nTrue\n"); } #[test] fn meta_test_symbol() { let code = format!(r#"((let ((arg (intern "sym"))) (print (bool (arg:get-meta "{}")))))"#, metadata::SYMBOL_META); let output = parse_and_run(&code); assert_eq!(output, "\nTrue\n"); } #[test] fn meta_test_symbol_quoted() { let code = format!(r#"((let ((arg 'sym)) (print (bool (arg:get-meta "{}")))))"#, metadata::SYMBOL_META); let output = parse_and_run(&code); assert_eq!(output, "\nTrue\n"); } #[test] fn meta_test_symbol_not_on_cons() { let code = format!(r#"((let ((arg '(1 . 2))) (print (arg:has-meta "{}"))))"#, metadata::SYMBOL_META); let output = parse_and_run(&code); assert_eq!(output, "\nFalse\n"); } #[test] fn meta_test_cons_not_on_symbol() { let code = format!(r#"((let ((arg 'sym)) (print (arg:has-meta "{}"))))"#, metadata::CONS_META); let output = parse_and_run(&code); assert_eq!(output, "\nFalse\n"); } ================================================ FILE: tests/test/mod.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . pub mod common; pub mod builtin_function_test; pub mod class_test; pub mod collection_conversion_test; pub mod concurrency_test; pub mod cond_if_test; pub mod const_test; pub mod declaration_test; pub mod dependencies_test; pub mod dictionary_test; pub mod enum_test; pub mod error_test; pub mod even_odd_test; pub mod factorial_test; pub mod flet_test; pub mod floating_test; pub mod for_test; pub mod import_test; pub mod labels_test; pub mod lambda_class_test; pub mod lambda_test; pub mod lazy_val_test; pub mod let_var_test; pub mod list_operator_test; pub mod macro_test; pub mod meta_test; pub mod name_translation_test; pub mod object_test; pub mod operator_test; pub mod quoting_test; pub mod signal_test; pub mod simple_expr_test; pub mod string_test; pub mod type_checking_test; pub mod typecast_test; pub mod while_test; ================================================ FILE: tests/test/name_translation_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] pub fn translate_local_vars_test_1() { assert_eq!(parse_compile_decl("((defn foo () (let ((x 1)) x)))"), r#"extends Reference static func foo(): var x = 1 return x "#); } #[test] pub fn translate_local_vars_test_2() { assert_eq!(parse_compile_decl("((defn foo () (let* ((x 1) (x 2)) x)))"), r#"extends Reference static func foo(): var x = 1 var x_0 = 2 return x_0 "#); } #[test] pub fn translate_local_vars_test_3() { assert_eq!(parse_compile_decl("((defn foo () (let* ((x-y 1) (x-y 2)) x-y)))"), r#"extends Reference static func foo(): var x_y = 1 var x_y_0 = 2 return x_y_0 "#); } #[test] pub fn translate_arguments_test() { assert_eq!(parse_compile_decl("((defn foo (a-b) a-b))"), r#"extends Reference static func foo(a_b): return a_b "#); } #[test] pub fn translate_constructor_arguments_test_1() { assert_eq!(parse_compile_decl("((defclass Foo (Reference) (defn _init (a-b))))"), r#"extends Reference class Foo extends Reference: func _init(a_b): pass "#); } #[test] pub fn translate_constructor_arguments_test_2() { assert_eq!(parse_compile_decl("((defclass Foo (Reference) (defn _init (a-b) (super a-b))))"), r#"extends Reference class Foo extends Reference: func _init(a_b).(a_b): pass "#); } #[test] pub fn translate_function_name_test() { assert_eq!(parse_compile_decl("((defn foo-bar ()))"), r#"extends Reference static func foo_bar(): return null "#); } #[test] pub fn translate_macro_name_test() { assert_eq!(parse_compile_decl("((defmacro foo-bar ()))"), r#"extends Reference static func foo_bar(): return null "#); } #[test] pub fn translate_const_name_test() { assert_eq!(parse_compile_decl("((defconst FOO-BAR 1))"), r#"extends Reference const FOO_BAR = 1 "#); } #[test] pub fn translate_class_name_test() { assert_eq!(parse_compile_decl("((defclass Foo-Bar (Reference)))"), r#"extends Reference class Foo_Bar extends Reference: func _init(): pass "#); } #[test] pub fn translate_enum_name_test_1() { assert_eq!(parse_compile_decl("((defenum Foo-Bar A-B))"), r#"extends Reference enum Foo_Bar { A_B, } "#); } #[test] pub fn translate_enum_name_test_2() { assert_eq!(parse_compile_decl("((defenum Foo-Bar (A-B 1)))"), r#"extends Reference enum Foo_Bar { A_B = 1, } "#); } #[test] pub fn translate_declare_name_test() { assert_eq!(parse_compile_decl("((sys/declare function foo-bar ()) (defn run () (foo-bar)))"), r#"extends Reference static func run(): return foo_bar() "#); } ================================================ FILE: tests/test/object_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . extern crate gdlisp; use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use super::common::*; #[test] pub fn empty_object_test() { assert_eq!(parse_compile_decl("((defobject Foo (Reference)))"), r#"extends Reference class _AnonymousClass extends Reference: func _init(): pass static func _lazy_0(): var _this_file_1 = load("res://TEST.gd") var _cond = null if _this_file_1.has_meta("__gdlisp_Lazy__G_3"): _cond = _this_file_1.get_meta("__gdlisp_Lazy__G_3") else: if true: var _value_2 = _AnonymousClass.new() _this_file_1.set_meta("__gdlisp_Lazy__G_3", _value_2) _cond = _value_2 else: _cond = null return _cond static func __gdlisp_SymbolMacroFunction_Foo(): return GDLisp.cons(GDLisp.cons(GDLisp.intern("access-slot"), GDLisp.cons(GDLisp.cons(GDLisp.intern("contextual-load"), GDLisp.cons("res://TEST.gd", null)), GDLisp.cons(GDLisp.intern("_lazy_0"), null))), null) "#); } #[test] pub fn main_object_test() { assert_eq!( parse_compile_decl_err("((defobject Foo (Reference) main))"), Err(PError::from(GDError::new(GDErrorF::DottedListError, SourceOffset(1)))), ); } #[test] pub fn simple_self_run_object_test_1() { assert_eq!(parse_and_run(r#" ((defobject Foo (Reference) (defvar x 1) (defn _init ()) (defn double () (* self:x 2))) (print Foo:x) (print (Foo:double)) (set Foo:x 10) (print (Foo:double))) "#), "\n1\n2\n20\n"); } #[test] pub fn simple_self_run_object_test_2() { assert_eq!(parse_and_run(r#" ((defobject Foo Reference (defvar x 1) (defn _init ()) (defn double () (* self:x 2))) (print Foo:x) (print (Foo:double)) (set Foo:x 10) (print (Foo:double))) "#), "\n1\n2\n20\n"); } #[test] pub fn simple_self_run_object_test_3() { assert_eq!(parse_and_run(r#" ((defobject Foo (Reference) public (defvar x 1) (defn _init ()) (defn double () (* self:x 2))) (print Foo:x) (print (Foo:double)) (set Foo:x 10) (print (Foo:double))) "#), "\n1\n2\n20\n"); } #[test] pub fn simple_self_run_object_test_4() { assert_eq!(parse_and_run(r#" ((defobject Foo (Reference) private (defvar x 1) (defn _init ()) (defn double () (* self:x 2))) (print Foo:x) (print (Foo:double)) (set Foo:x 10) (print (Foo:double))) "#), "\n1\n2\n20\n"); } #[test] pub fn empty_object_run_test() { assert_eq!(parse_and_run(r#" ((defobject Foo (Reference)) Foo (print 1)) "#), "\n1\n"); } #[test] pub fn self_with_closure_run_object_test() { assert_eq!(parse_and_run(r#" ((defobject Foo (Reference) (defvar x 1) (defn increment () (lambda () (set self:x (+ self:x 1))))) (let ((fn (Foo:increment))) (print (funcall fn)) (print (funcall fn)) (print (funcall fn)))) "#), "\n2\n3\n4\n"); } #[test] pub fn macro_in_object_test_1() { assert_eq!(parse_and_run(r#" ((defmacro declare-fn (name) `(defn ,name () 99)) (defobject Foo (Reference) (declare-fn aa) (declare-fn bb)) (print (Foo:aa)) (print (Foo:bb))) "#), "\n99\n99\n"); } #[test] pub fn macro_in_object_test_2() { assert_eq!(parse_and_run(r#" ((defmacro my-value () 630) (defobject Foo (Reference) (defn foo () (my-value))) (print (Foo:foo))) "#), "\n630\n"); } #[test] pub fn macro_in_object_test_3() { assert_eq!(parse_and_run(r#" ((defmacro declare-fns () `(progn (defn a () 67) (defn b () 68))) (defobject Foo (Reference) (declare-fns)) (print (Foo:a)) (print (Foo:b))) "#), "\n67\n68\n"); } /* ///// #[test] pub fn macro_uses_object_test() { assert_eq!(parse_and_run(r#" ((defobject Foo (Reference) (defvar x)) (defmacro through-foo () (set Foo:x 5) Foo:x) (print (through-foo)))"#), "\n5\n"); } */ /* ///// #[test] pub fn reference_to_outer_in_object_test_1() { // See Issue #85 let output = parse_and_run(r#" ((defn outer () 100) (defobject Foo (Reference) (defn foo () (outer))) (print (Foo:foo)) (set (elt Foo "__gdlisp_outer_class_1") nil))"#); // Nasty hack to break the cyclic reference (have to use elt and a string or GDLisp will catch on to what I'm doing) assert_eq!(output, "\n100\n"); } */ #[test] pub fn initialization_of_object_test() { // Make sure that a singleton object is only initialized once, even // if we reference it a couple of times. let output = parse_and_run(r#" ((defobject Foo (Reference) (defn _init () (print "Initializing")) (defn foo () 18)) (print "Start") (print (Foo:foo)) (print "Middle") (print (Foo:foo)) (print "End"))"#); assert_eq!(output, "\nStart\nInitializing\n18\nMiddle\n18\nEnd\n"); } #[test] pub fn object_uses_gdlisp_functions_test() { let output = parse_and_run(r#" ((defobject MyObject (Reference) (defvar example-var [1 2 3 4]) (defn sum () (array/fold #'+ @example-var 0))) (print (MyObject:sum))) "#); assert_eq!(output, "\n10\n"); } ================================================ FILE: tests/test/operator_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] fn addition_test_1() { assert_eq!(parse_and_run("((print (+)))"), "\n0\n"); } #[test] fn addition_test_2() { assert_eq!(parse_and_run("((print (+ 1)))"), "\n1\n"); } #[test] fn addition_test_3() { assert_eq!(parse_and_run("((print (+ 1 5)))"), "\n6\n"); } #[test] fn addition_test_4() { assert_eq!(parse_and_run("((print (+ 1 5 2)))"), "\n8\n"); } #[test] fn addition_test_5() { assert_eq!(parse_and_run("((print (+ 1 5 2 -4)))"), "\n4\n"); } #[test] fn addition_test_6() { assert_eq!(parse_and_run("((print (+ (vector 1 1) (vector 2 3))))"), "\n(3, 4)\n"); } #[test] fn addition_test_7() { assert_eq!(parse_and_run("((print (+ (vector 1 1))))"), "\n(1, 1)\n"); } #[test] fn addition_test_indirect_1() { assert_eq!(parse_and_run("((print (funcall (function +) 1 5 2 -4)))"), "\n4\n"); } #[test] fn addition_test_indirect_2() { assert_eq!(parse_and_run("((print (funcall (function +))))"), "\n0\n"); } #[test] fn addition_test_indirect_3() { assert_eq!(parse_and_run("((print (funcall (function +) 3)))"), "\n3\n"); } #[test] fn addition_test_indirect_4() { assert_eq!(parse_and_run("((print (apply (function +) 1 2 '(3 4))))"), "\n10\n"); } #[test] fn multiplication_test_1() { assert_eq!(parse_and_run("((print (*)))"), "\n1\n"); } #[test] fn multiplication_test_2() { assert_eq!(parse_and_run("((print (* 4)))"), "\n4\n"); } #[test] fn multiplication_test_3() { assert_eq!(parse_and_run("((print (* 4 3)))"), "\n12\n"); } #[test] fn multiplication_test_4() { assert_eq!(parse_and_run("((print (* 4 3 2)))"), "\n24\n"); } #[test] fn multiplication_test_5() { assert_eq!(parse_and_run("((print (* 4 (vector 1 2) 2 (vector 2 3))))"), "\n(16, 48)\n"); } #[test] fn multiplication_test_indirect_1() { assert_eq!(parse_and_run("((print (funcall (function *) 4 3 2)))"), "\n24\n"); } #[test] fn multiplication_test_indirect_2() { assert_eq!(parse_and_run("((print (funcall (function *))))"), "\n1\n"); } #[test] fn multiplication_test_indirect_3() { assert_eq!(parse_and_run("((print (funcall (function *) 3)))"), "\n3\n"); } #[test] fn subtraction_test_1() { assert_eq!(parse_and_run("((print (- 4)))"), "\n-4\n"); } #[test] fn subtraction_test_2() { assert_eq!(parse_and_run("((print (- 4 3)))"), "\n1\n"); } #[test] fn subtraction_test_3() { assert_eq!(parse_and_run("((print (- 4 3 2)))"), "\n-1\n"); } #[test] fn subtraction_test_indirect_1() { assert_eq!(parse_and_run("((print (funcall (function -) 4 3 2)))"), "\n-1\n"); } #[test] fn subtraction_test_indirect_2() { assert_eq!(parse_and_run("((print (funcall (function -) 3)))"), "\n-3\n"); } #[test] fn division_test_1() { assert_eq!(parse_and_run("((print (/ 4.0)))"), "\n0.25\n"); } #[test] fn division_test_2() { assert_eq!(parse_and_run("((print (/ 4)))"), "\n0\n"); } #[test] fn division_test_3() { assert_eq!(parse_and_run("((print (/ 4 2)))"), "\n2\n"); } #[test] fn division_test_4() { assert_eq!(parse_and_run("((print (/ 4 2 2)))"), "\n1\n"); } #[test] fn division_test_5() { assert_eq!(parse_and_run("((print (/ (vector 4 2 4) 2 2)))"), "\n(1, 0.5, 1)\n"); } #[test] fn division_test_indirect_1() { assert_eq!(parse_and_run("((print (funcall (function /) 4.0)))"), "\n0.25\n"); } #[test] fn division_test_indirect_2() { assert_eq!(parse_and_run("((print (funcall (function /) 4)))"), "\n0\n"); } #[test] fn division_test_indirect_3() { assert_eq!(parse_and_run("((print (funcall (function /) 3 2)))"), "\n1\n"); } #[test] fn division_test_indirect_4() { assert_eq!(parse_and_run("((print (funcall (function /) 3.0 2)))"), "\n1.5\n"); } #[test] fn eq_test_1() { assert_eq!(parse_and_run("((print (= 4)))"), "\nTrue\n"); } #[test] fn eq_test_2() { assert_eq!(parse_and_run("((print (= 4 4)))"), "\nTrue\n"); } #[test] fn eq_test_3() { assert_eq!(parse_and_run("((print (= 2 2 4)))"), "\nFalse\n"); } #[test] fn eq_test_stateful_1() { assert_eq!(parse_and_run("((print (= (print 1))))"), "\n1\nTrue\n"); } #[test] fn eq_test_stateful_2() { assert_eq!(parse_and_run("((print (= (print 1) (print 2) (print 3))))"), "\n1\n2\n3\nTrue\n"); } #[test] fn eq_test_indirect_1() { assert_eq!(parse_and_run("((print (funcall (function =) 1)))"), "\nTrue\n"); } #[test] fn eq_test_indirect_2() { assert_eq!(parse_and_run("((print (funcall (function =) 1 2)))"), "\nFalse\n"); } #[test] fn eq_test_indirect_3() { assert_eq!(parse_and_run("((print (funcall (function =) 1 1 1)))"), "\nTrue\n"); } #[test] fn lt_test_indirect() { assert_eq!(parse_and_run("((print (funcall (function <) 1 2 3)))"), "\nTrue\n"); } #[test] fn gt_test_indirect() { assert_eq!(parse_and_run("((print (funcall (function >) 3 2 1)))"), "\nTrue\n"); } #[test] fn le_test_indirect() { assert_eq!(parse_and_run("((print (funcall (function <=) 1 2 2 3)))"), "\nTrue\n"); } #[test] fn ge_test_indirect() { assert_eq!(parse_and_run("((print (funcall (function >=) 3 2 2 1)))"), "\nTrue\n"); } #[test] fn ne_test_1() { assert_eq!(parse_and_run("((print (/= 1 1 1)))"), "\nFalse\n"); } #[test] fn ne_test_2() { assert_eq!(parse_and_run("((print (/= 1 3 2)))"), "\nTrue\n"); } #[test] fn ne_test_3() { assert_eq!(parse_and_run("((print (/= 1 2 1)))"), "\nFalse\n"); } #[test] fn not_test_1() { assert_eq!(parse_and_run("((print (not #t)))"), "\nFalse\n"); } #[test] fn not_test_2() { assert_eq!(parse_and_run("((print (not #f)))"), "\nTrue\n"); } #[test] pub fn addition_compile_test() { assert_eq!(parse_compile_and_output("(+)"), "return 0\n"); assert_eq!(parse_compile_and_output("(+ 1)"), "return 1\n"); assert_eq!(parse_compile_and_output("(+ 1 2)"), "return 1 + 2\n"); assert_eq!(parse_compile_and_output("(+ 1 2 3)"), "return 1 + 2 + 3\n"); } #[test] pub fn multiplication_compile_test() { assert_eq!(parse_compile_and_output("(*)"), "return 1\n"); assert_eq!(parse_compile_and_output("(* 2)"), "return 2\n"); assert_eq!(parse_compile_and_output("(* 2 3)"), "return 2 * 3\n"); assert_eq!(parse_compile_and_output("(* 2 3 4)"), "return 2 * 3 * 4\n"); } #[test] pub fn subtraction_compile_test() { assert_eq!(parse_compile_and_output("(- 2)"), "return -2\n"); assert_eq!(parse_compile_and_output("(- 2 3)"), "return 2 - 3\n"); assert_eq!(parse_compile_and_output("(- 2 3 4)"), "return 2 - 3 - 4\n"); } #[test] pub fn division_compile_test() { assert_eq!(parse_compile_and_output("(/ 2)"), "return 1 / 2\n"); assert_eq!(parse_compile_and_output("(/ 2 3)"), "return 2 / 3\n"); assert_eq!(parse_compile_and_output("(/ 2 3 4)"), "return 2 / 3 / 4\n"); assert_eq!(parse_compile_and_output("(/ 2.0 3 4.0)"), "return 2e0 / 3 / 4e0\n"); } #[test] pub fn eq_compile_test() { assert_eq!(parse_compile_and_output("(= 1)"), "return true\n"); assert_eq!(parse_compile_and_output("(= 1 2)"), "return 1 == 2\n"); assert_eq!(parse_compile_and_output("(= 1 2 3)"), "return 1 == 2 && 2 == 3\n"); } #[test] pub fn eq_compile_test_stateful() { assert_eq!(parse_compile_and_output("(= (foo))"), "foo()\nreturn true\n"); assert_eq!(parse_compile_and_output("(= (foo) (foo))"), "return foo() == foo()\n"); assert_eq!(parse_compile_and_output("(= (foo) (foo) (foo))"), "var _cmp = foo()\nvar _cmp_0 = foo()\nvar _cmp_1 = foo()\nreturn _cmp == _cmp_0 && _cmp_0 == _cmp_1\n"); } #[test] pub fn cmp_compile_test() { assert_eq!(parse_compile_and_output("(< 1 2)"), "return 1 < 2\n"); assert_eq!(parse_compile_and_output("(> 1 2 3)"), "return 1 > 2 && 2 > 3\n"); assert_eq!(parse_compile_and_output("(<= 1 2)"), "return 1 <= 2\n"); assert_eq!(parse_compile_and_output("(>= 1 2 3)"), "return 1 >= 2 && 2 >= 3\n"); } #[test] pub fn cmp_compile_test_stateful() { assert_eq!(parse_compile_and_output("(< (foo))"), "foo()\nreturn true\n"); assert_eq!(parse_compile_and_output("(<= (foo) (foo))"), "return foo() <= foo()\n"); assert_eq!(parse_compile_and_output("(> (foo) (foo) (foo))"), "var _cmp = foo()\nvar _cmp_0 = foo()\nvar _cmp_1 = foo()\nreturn _cmp > _cmp_0 && _cmp_0 > _cmp_1\n"); assert_eq!(parse_compile_and_output("(>= (foo) (foo))"), "return foo() >= foo()\n"); } #[test] pub fn ne_compile_test() { assert_eq!(parse_compile_and_output("(/= 1)"), "return true\n"); assert_eq!(parse_compile_and_output("(/= (foo))"), "foo()\nreturn true\n"); assert_eq!(parse_compile_and_output("(/= 1 2)"), "return 1 != 2\n"); assert_eq!(parse_compile_and_output("(/= 1 2 3)"), "return GDLisp._DIV__EQ_(1, GDLisp.cons(2, GDLisp.cons(3, null)))\n"); } #[test] pub fn array_subscript_test() { assert_eq!(parse_compile_and_output("(elt 1 2)"), "return 1[2]\n"); } #[test] pub fn dict_subscript_test() { assert_eq!(parse_compile_and_output("(dict/elt 1 2)"), "return 1[2]\n"); } #[test] pub fn array_subscript_test_run() { assert_eq!(parse_and_run("((let ((x [10 20 30])) (print (elt x 2))))"), "\n30\n"); } #[test] pub fn array_subscript_test_run_indirect() { assert_eq!(parse_and_run("((let ((x [10 20 30])) (print (funcall #'elt x 2))))"), "\n30\n"); } #[test] pub fn dict_subscript_test_run() { assert_eq!(parse_and_run("((let ((x {10 20})) (print (dict/elt x 10))))"), "\n20\n"); } #[test] pub fn dict_subscript_test_run_indirect() { assert_eq!(parse_and_run("((let ((x {10 20})) (print (funcall #'dict/elt x 10))))"), "\n20\n"); } #[test] pub fn array_subscript_assign_test_run_1() { assert_eq!(parse_and_run("((let ((x [10 20 30])) (set (elt x 1) 999) (print x)))"), "\n[10, 999, 30]\n"); } #[test] pub fn array_subscript_assign_test_run_2() { assert_eq!(parse_and_run("((let ((x [10 20 30])) (set-elt 999 x 1) (print x)))"), "\n[10, 999, 30]\n"); } #[test] pub fn array_subscript_assign_test_run_3() { assert_eq!(parse_and_run("((let ((x [10 20 30])) (funcall #'set-elt 999 x 1) (print x)))"), "\n[10, 999, 30]\n"); } #[test] pub fn dict_subscript_assign_test_run_1() { assert_eq!(parse_and_run("((let ((x {})) (set (dict/elt x 1) 999) (print x)))"), "\n{1:999}\n"); } #[test] pub fn dict_subscript_assign_test_run_2() { assert_eq!(parse_and_run("((let ((x {2 0})) (set (dict/elt x 2) 1) (print x)))"), "\n{2:1}\n"); } #[test] pub fn min_max_test_1() { assert_eq!(parse_compile_and_output("(min 0 1)"), "return min(0, 1)\n"); assert_eq!(parse_compile_and_output("(max 0 1)"), "return max(0, 1)\n"); } #[test] pub fn min_max_test_2() { assert_eq!(parse_compile_and_output("(min 0)"), "return 0\n"); assert_eq!(parse_compile_and_output("(max 0)"), "return 0\n"); } #[test] pub fn min_max_test_3() { assert_eq!(parse_compile_and_output("(min)"), "return INF\n"); assert_eq!(parse_compile_and_output("(max)"), "return -INF\n"); } #[test] pub fn min_max_test_4() { assert_eq!(parse_compile_and_output("(min 1 2 3)"), "return min(min(1, 2), 3)\n"); assert_eq!(parse_compile_and_output("(max 1 2 3)"), "return max(max(1, 2), 3)\n"); } #[test] pub fn min_max_run_test() { assert_eq!(parse_and_run("((print (min 1 2 3)))"), "\n1\n"); assert_eq!(parse_and_run("((print (max 1 2 3)))"), "\n3\n"); } #[test] pub fn gcd_run_test() { assert_eq!(parse_and_run("((print (gcd)))"), "\n0\n"); assert_eq!(parse_and_run("((print (gcd 100)))"), "\n100\n"); assert_eq!(parse_and_run("((print (gcd 75 50)))"), "\n25\n"); assert_eq!(parse_and_run("((print (gcd 75 10 50)))"), "\n5\n"); } #[test] pub fn lcm_run_test() { assert_eq!(parse_and_run("((print (lcm)))"), "\n1\n"); assert_eq!(parse_and_run("((print (lcm 100)))"), "\n100\n"); assert_eq!(parse_and_run("((print (lcm 75 50)))"), "\n150\n"); assert_eq!(parse_and_run("((print (lcm 2 3 4 5)))"), "\n60\n"); } #[test] fn eq_num_test() { assert_eq!(parse_and_run("((print (= 4 4)) (print (= 4 3)) (print (= 4 4.0)))"), "\nTrue\nFalse\nTrue\n"); } #[test] fn eq_str_test() { assert_eq!(parse_and_run("((print (= \"a\" \"a\")))"), "\nTrue\n"); } #[test] fn eq_symbol_test() { assert_eq!(parse_and_run("((print (= 'a 'a)) (print (= 'b 'c)) (print (= 'b 'B)))"), "\nTrue\nFalse\nFalse\n"); } #[test] fn eq_array_test() { assert_eq!(parse_and_run("((print (= [1] [1])))"), "\nTrue\n"); } #[test] fn eq_dict_test() { assert_eq!(parse_and_run("((print (= {'a 1} {'a 1})))"), "\nFalse\n"); } #[test] fn eq_cons_test() { // Cons cells follow reference semantics, since they're mutable and // (according to GDScript) non-primitive. assert_eq!(parse_and_run("((print (= '(1 2) '(1 2))))"), "\nFalse\n"); } #[test] fn equal_num_test() { assert_eq!(parse_and_run("((print (equal? 4 4)) (print (equal? 4 3)) (print (equal? 4 4.0)))"), "\nTrue\nFalse\nTrue\n"); } #[test] fn equal_str_test() { assert_eq!(parse_and_run("((print (equal? \"a\" \"a\")))"), "\nTrue\n"); } #[test] fn equal_symbol_test() { assert_eq!(parse_and_run("((print (equal? 'a 'a)) (print (equal? 'b 'c)) (print (equal? 'b 'B)))"), "\nTrue\nFalse\nFalse\n"); } #[test] fn equal_array_test() { assert_eq!(parse_and_run("((print (equal? [1] [1])) (print (equal? [1] [1 2])))"), "\nTrue\nFalse\n"); } #[test] fn equal_dict_test() { assert_eq!(parse_and_run("((print (equal? {'a 1} {'a 1})))"), "\nTrue\n"); assert_eq!(parse_and_run("((print (equal? {'a 1} {'a 2})))"), "\nFalse\n"); assert_eq!(parse_and_run("((print (equal? {\"b\" 2 'a 1} {'a 1 \"b\" 2})))"), "\nTrue\n"); } #[test] fn equal_cons_test() { assert_eq!(parse_and_run("((print (equal? '(1 2) '(1 2))))"), "\nTrue\n"); assert_eq!(parse_and_run("((print (equal? '(1 2) '(1))))"), "\nFalse\n"); assert_eq!(parse_and_run("((print (equal? '(1) '(1 2))))"), "\nFalse\n"); } #[test] fn equal_nonmatching_test() { assert_eq!(parse_and_run("((print (equal? '(1 2) [1 2])))"), "\nFalse\n"); assert_eq!(parse_and_run("((print (equal? 0 \"0\")))"), "\nFalse\n"); assert_eq!(parse_and_run("((print (equal? {'a 1} nil)))"), "\nFalse\n"); } #[test] fn equal_nested_test() { assert_eq!(parse_and_run("((print (equal? {'a ['b '(7)]} {'a ['b '(7)]})))"), "\nTrue\n"); } ================================================ FILE: tests/test/quoting_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] pub fn quote_test() { assert_eq!(parse_compile_and_output("(quote 10)"), "return 10\n"); assert_eq!(parse_compile_and_output("(quote (1 2))"), "return GDLisp.cons(1, GDLisp.cons(2, null))\n"); assert_eq!(parse_compile_and_output("(quote (1 . 2))"), "return GDLisp.cons(1, 2)\n"); assert_eq!(parse_compile_and_output("(quote [1 2])"), "return GDLisp.cons(GDLisp.intern(\"array\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); assert_eq!(parse_compile_and_output("(quote {1 2})"), "return GDLisp.cons(GDLisp.intern(\"dict\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); } #[test] pub fn quote_syntax_test() { assert_eq!(parse_compile_and_output("'10"), "return 10\n"); assert_eq!(parse_compile_and_output("'(1 2)"), "return GDLisp.cons(1, GDLisp.cons(2, null))\n"); assert_eq!(parse_compile_and_output("'(1 . 2)"), "return GDLisp.cons(1, 2)\n"); assert_eq!(parse_compile_and_output("'[1 2]"), "return GDLisp.cons(GDLisp.intern(\"array\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); assert_eq!(parse_compile_and_output("'{1 2}"), "return GDLisp.cons(GDLisp.intern(\"dict\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); } #[test] pub fn full_quasiquote_test() { assert_eq!(parse_compile_and_output("(quasiquote 10)"), "return 10\n"); assert_eq!(parse_compile_and_output("(quasiquote (1 2))"), "return GDLisp.cons(1, GDLisp.cons(2, null))\n"); assert_eq!(parse_compile_and_output("(quasiquote (1 . 2))"), "return GDLisp.cons(1, 2)\n"); assert_eq!(parse_compile_and_output("(quasiquote [1 2])"), "return GDLisp.cons(GDLisp.intern(\"array\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); assert_eq!(parse_compile_and_output("(quasiquote {1 2})"), "return GDLisp.cons(GDLisp.intern(\"dict\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); } #[test] pub fn full_quasiquote_syntax_test() { assert_eq!(parse_compile_and_output("`10"), "return 10\n"); assert_eq!(parse_compile_and_output("`(1 2)"), "return GDLisp.cons(1, GDLisp.cons(2, null))\n"); assert_eq!(parse_compile_and_output("`(1 . 2)"), "return GDLisp.cons(1, 2)\n"); assert_eq!(parse_compile_and_output("`[1 2]"), "return GDLisp.cons(GDLisp.intern(\"array\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); assert_eq!(parse_compile_and_output("`{1 2}"), "return GDLisp.cons(GDLisp.intern(\"dict\"), GDLisp.cons(1, GDLisp.cons(2, null)))\n"); } #[test] pub fn partial_quasiquote_test_1() { assert_eq!(parse_compile_and_output("(let ((a 1)) `,a)"), "var a = 1\nreturn a\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) `(a . ,a))"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"a\"), a)\n"); } #[test] pub fn partial_quasiquote_test_2() { assert_eq!(parse_compile_and_output("(let ((a 1)) (quasiquote ,a))"), "var a = 1\nreturn a\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) (quasiquote (a . ,a)))"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"a\"), a)\n"); } #[test] pub fn partial_quasiquote_test_3() { assert_eq!(parse_compile_and_output("(let ((a 1)) (quasiquote (unquote a)))"), "var a = 1\nreturn a\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) (quasiquote (a . (unquote a))))"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"a\"), a)\n"); } #[test] pub fn partial_quasiquote_test_4() { assert_eq!(parse_compile_and_output("(let ((a 1)) `(unquote a))"), "var a = 1\nreturn a\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) `(a . (unquote a)))"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"a\"), a)\n"); } #[test] pub fn array_quasiquote_test() { assert_eq!(parse_compile_and_output("(let ((a 1)) `[a ,a a])"), r#"var a = 1 return GDLisp.cons(GDLisp.intern("array"), GDLisp.cons(GDLisp.intern("a"), GDLisp.cons(a, GDLisp.cons(GDLisp.intern("a"), null)))) "#); } #[test] pub fn dict_quasiquote_key_test() { assert_eq!(parse_compile_and_output("(let ((a 1)) `{,a a})"), r#"var a = 1 return GDLisp.cons(GDLisp.intern("dict"), GDLisp.cons(a, GDLisp.cons(GDLisp.intern("a"), null))) "#); } #[test] pub fn dict_quasiquote_value_test() { assert_eq!(parse_compile_and_output("(let ((a 1)) `{a ,a})"), r#"var a = 1 return GDLisp.cons(GDLisp.intern("dict"), GDLisp.cons(GDLisp.intern("a"), GDLisp.cons(a, null))) "#); } #[test] pub fn vector_quote_test() { assert_eq!(parse_compile_and_output("(let ((a 1)) 'V{a a})"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"vector\"), GDLisp.cons(GDLisp.intern(\"a\"), GDLisp.cons(GDLisp.intern(\"a\"), null)))\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) 'V{a a a})"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"vector\"), GDLisp.cons(GDLisp.intern(\"a\"), GDLisp.cons(GDLisp.intern(\"a\"), GDLisp.cons(GDLisp.intern(\"a\"), null))))\n"); } #[test] pub fn vector_quasiquote_test() { assert_eq!(parse_compile_and_output("(let ((a 1)) `V{a ,a})"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"vector\"), GDLisp.cons(GDLisp.intern(\"a\"), GDLisp.cons(a, null)))\n"); assert_eq!(parse_compile_and_output("(let ((a 1)) `V{a ,a a})"), "var a = 1\nreturn GDLisp.cons(GDLisp.intern(\"vector\"), GDLisp.cons(GDLisp.intern(\"a\"), GDLisp.cons(a, GDLisp.cons(GDLisp.intern(\"a\"), null))))\n"); } #[test] pub fn quasiquote_unquote_spliced_list_test() { assert_eq!(parse_compile_and_output("(let ((a [2 3])) `(1 ,.a 4))"), "var a = [2, 3]\nreturn GDLisp.cons(1, GDLisp.append(GDLisp.cons(GDLisp.sys_DIV_qq_smart_list(a), GDLisp.cons(GDLisp.cons(4, null), null))))\n"); } #[test] pub fn quasiquote_nested_test() { assert_eq!(parse_compile_and_output("``(,a)"), "var _quasiquote = null\nreturn GDLisp.cons(GDLisp.intern(\"quasiquote\"), GDLisp.cons(GDLisp.cons(GDLisp.cons(GDLisp.intern(\"unquote\"), GDLisp.cons(GDLisp.intern(\"a\"), _quasiquote)), null), null))\n"); } #[test] pub fn quasiquote_unquote_spliced_list_test_runner_1() { assert_eq!(parse_and_run("((let ((a [2 3])) (print (list->array `(1 ,.a 4)))))"), "\n[1, 2, 3, 4]\n"); } #[test] pub fn quasiquote_unquote_spliced_list_test_runner_2() { assert_eq!(parse_and_run("((let ((a '(2 3))) (print (list->array `(1 ,.a 4)))))"), "\n[1, 2, 3, 4]\n"); } #[test] pub fn quasiquote_unquote_spliced_array_test() { assert_eq!(parse_compile_and_output("(let ((a [2 3])) `[1 ,.a 4])"), "var a = [2, 3]\nreturn GDLisp.cons(GDLisp.intern(\"array\"), GDLisp.cons(1, GDLisp.append(GDLisp.cons(GDLisp.sys_DIV_qq_smart_list(a), GDLisp.cons(GDLisp.cons(4, null), null)))))\n"); } #[test] pub fn quasiquote_unquote_spliced_array_test_runner_1() { // (list->array ...:cdr) removes the 'array term from the head and // then prints as a Godot array. assert_eq!(parse_and_run("((let ((a [3 4])) (print (list->array (quasiquote [1 2 ,.a 5 6]):cdr))))"), "\n[1, 2, 3, 4, 5, 6]\n"); } #[test] pub fn quasiquote_unquote_spliced_array_test_runner_2() { // (list->array ...:cdr) removes the 'array term from the head and // then prints as a Godot array. assert_eq!(parse_and_run("((let ((a '(3 4))) (print (list->array (quasiquote [1 2 ,.a 5 6]):cdr))))"), "\n[1, 2, 3, 4, 5, 6]\n"); } #[test] pub fn quasiquote_unquote_spliced_array_test_runner_3() { // (list->array ...:cdr) removes the 'array term from the head and // then prints as a Godot array. assert_eq!(parse_and_run("((let ((a '(3 4))) (print (list->array (quasiquote [1 2 ,.a]):cdr))))"), "\n[1, 2, 3, 4]\n"); } #[test] pub fn quasiquote_unquote_spliced_array_test_runner_4() { // (list->array ...:cdr) removes the 'array term from the head and // then prints as a Godot array. assert_eq!(parse_and_run("((let ((a '(3 4))) (print (list->array (quasiquote [1 2 ,.a ,.a]):cdr))))"), "\n[1, 2, 3, 4, 3, 4]\n"); } #[test] pub fn quasiquote_unquote_spliced_array_test_runner_5() { // (list->array ...:cdr) removes the 'array term from the head and // then prints as a Godot array. assert_eq!(parse_and_run("((let ((a '(3 4))) (print (list->array (quasiquote [,.a 10 ,.a]):cdr))))"), "\n[3, 4, 10, 3, 4]\n"); } #[test] pub fn quasiquote_nesting_test() { assert_eq!(parse_compile_and_output("`(1)"), "return GDLisp.cons(1, null)\n"); assert_eq!(parse_compile_and_output("`(1 2)"), "return GDLisp.cons(1, GDLisp.cons(2, null))\n"); assert_eq!(parse_compile_and_output("`(1 2 3)"), "return GDLisp.cons(1, GDLisp.cons(2, GDLisp.cons(3, null)))\n"); assert_eq!(parse_compile_and_output("`(1 2 3 4)"), "return GDLisp.cons(1, GDLisp.cons(2, GDLisp.cons(3, GDLisp.cons(4, null))))\n"); assert_eq!(parse_compile_and_output("`(1 2 3 4 5)"), "var _quasiquote = null\nreturn GDLisp.cons(1, GDLisp.cons(2, GDLisp.cons(3, GDLisp.cons(4, GDLisp.cons(5, _quasiquote)))))\n"); assert_eq!(parse_compile_and_output("`(1 2 3 4 5 6)"), "var _quasiquote = GDLisp.cons(6, null)\nreturn GDLisp.cons(1, GDLisp.cons(2, GDLisp.cons(3, GDLisp.cons(4, GDLisp.cons(5, _quasiquote)))))\n"); } ================================================ FILE: tests/test/signal_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] pub fn basic_connect_to_signal_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defsignal frobnicated)) (let ((foo (Foo:new))) (connect>> foo "frobnicated" (lambda () (print "Received"))) (print "A") (foo:emit-signal "frobnicated") (print "B"))) "#), "\nA\nReceived\nB\n"); } #[test] pub fn signal_multiple_fires_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defsignal frobnicated)) (let ((foo (Foo:new))) (connect>> foo "frobnicated" (lambda () (print "Received"))) (print "A") (foo:emit-signal "frobnicated") (foo:emit-signal "frobnicated") (foo:emit-signal "frobnicated") (print "B"))) "#), "\nA\nReceived\nReceived\nReceived\nB\n"); } #[test] pub fn signal_multiple_fires_oneshot_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defsignal frobnicated)) (let ((foo (Foo:new))) (connect1>> foo "frobnicated" (lambda () (print "Received"))) (print "A") (foo:emit-signal "frobnicated") (foo:emit-signal "frobnicated") (foo:emit-signal "frobnicated") (print "B"))) "#), "\nA\nReceived\nB\n"); } #[test] pub fn signal_connect_disconnect_test() { assert_eq!(parse_and_run(r#" ((defclass Foo (Reference) (defsignal frobnicated)) (let ((foo (Foo:new))) (let ((index (connect>> foo "frobnicated" (lambda () (print "1"))))) (print "A") (foo:emit-signal "frobnicated") (print "B") (disconnect>> foo "frobnicated" index) (foo:emit-signal "frobnicated") (connect>> foo "frobnicated" (lambda () (print "2"))) (print "C") (foo:emit-signal "frobnicated") (print "D")))) "#), "\nA\n1\nB\nC\n2\nD\n"); } ================================================ FILE: tests/test/simple_expr_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use gdlisp::compile::error::{GDError, GDErrorF}; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use gdlisp::ir::expr::Expr; use super::common::*; #[test] pub fn expr_tests() { assert_eq!(parse_compile_and_output("100"), "return 100\n"); assert_eq!(parse_compile_and_output("(progn 100 200 300)"), "return 300\n"); assert_eq!(parse_compile_and_output("()"), "return null\n"); } #[test] pub fn progn_tests() { assert_eq!(parse_compile_and_output("(progn (foo) (bar) (foo))"), "foo()\nbar()\nreturn foo()\n"); assert_eq!(parse_compile_and_output("(progn)"), "return null\n"); assert_eq!(parse_compile_and_output("(progn (progn))"), "return null\n"); assert_eq!(parse_compile_and_output("(progn ())"), "return null\n"); } #[test] pub fn nonexistent_assignment_test() { assert_eq!( parse_compile_and_output_err("(set nonexistent-var 0)"), Err(PError::from(GDError::new(GDErrorF::NoSuchVar(String::from("nonexistent-var")), SourceOffset(5)))), ); } #[test] pub fn assignment_test() { // No cell; only accessed directly let result0 = parse_compile_and_output("(let ((x 1)) (set x 2))"); assert_eq!(result0, "var x = 1\nx = 2\nreturn x\n"); // No cell; only accessed directly let result1 = parse_compile_and_output("(let ((x 1)) (set x 2) 3)"); assert_eq!(result1, "var x = 1\nx = 2\nreturn 3\n"); // Cell; accessed inside lambda let result2 = parse_compile_and_output("(let ((x 1)) (lambda () (set x 2)))"); assert_eq!(result2, "var x = GDLisp.Cell.new(1)\nreturn _LambdaBlock.new(x)\n"); // Cell; accessed both inside and outside lambda let result3 = parse_compile_and_output("(let ((x 1)) (lambda () (set x 2)) (set x 3))"); assert_eq!(result3, "var x = GDLisp.Cell.new(1)\nx.contents = 3\nreturn x.contents\n"); // No cell; read-only let result4 = parse_compile_and_output("(let ((x 1)) (lambda () x) x)"); assert_eq!(result4, "var x = 1\nreturn x\n"); // Cell; closure and access separately let result5 = parse_compile_and_output("(let ((x 1)) (lambda () (set x 1)) x)"); assert_eq!(result5, "var x = GDLisp.Cell.new(1)\nreturn x.contents\n"); } #[test] pub fn custom_assign_test() { let result = parse_compile_decl("((defn set-foo (c a b)) (defn example () (set (foo 1 2) 3)))"); assert_eq!(result, r#"extends Reference static func set_foo(c, a, b): return null static func example(): return set_foo(3, 1, 2) "#); } #[test] pub fn expr_at_toplevel_test() { // Note: We're using `parse_compile_decl_err` here, not // `parse_compile_and_output_err`, so the system is *expecting* // declarations. We gave it an expression and expect it to come back // with an `ExprAtTopLevel` error. let result = parse_compile_decl_err("((+ 1 1))"); assert_eq!( result, Err(PError::from(GDError::new( GDErrorF::ExprAtTopLevel( Expr::call("+", vec!(Expr::from_value(1, SourceOffset(4)), Expr::from_value(1, SourceOffset(6))), SourceOffset(1)), ), SourceOffset(1), ))), ); } #[test] pub fn slot_assign_test_1() { let result = parse_compile_and_output("(let ((a 1)) (set a:b 3))"); assert_eq!(result, "var a = 1\na.b = 3\nreturn 3\n"); } #[test] pub fn slot_assign_test_2() { let result = parse_compile_and_output("(let ((a 1)) (set (access-slot a b) 3))"); assert_eq!(result, "var a = 1\na.b = 3\nreturn 3\n"); } #[test] pub fn slot_assign_test_3() { let result = parse_compile_and_output("(progn (let ((a 1)) (set (access-slot a b) 3)) 0)"); assert_eq!(result, "var a = 1\na.b = 3\nreturn 0\n"); } #[test] pub fn slot_assign_test_4() { let result = parse_compile_and_output("(let ((a 1)) (set (access-slot a b) (a:foo)))"); assert_eq!(result, "var a = 1\nvar _assign = a.foo()\na.b = _assign\nreturn _assign\n"); } #[test] pub fn slot_assign_test_5() { let result = parse_compile_and_output("(progn (let ((a 1)) (set (access-slot a b) (a:foo))) 0)"); assert_eq!(result, "var a = 1\na.b = a.foo()\nreturn 0\n"); } #[test] pub fn assign_to_self_test() { assert_eq!( parse_compile_decl_err("((defclass Foo (Node) (defn _init () (set self 1))))"), Err(PError::from(GDError::new(GDErrorF::CannotAssignTo(String::from("self")), SourceOffset(47)))), ); } #[test] pub fn assign_to_const_test() { assert_eq!( parse_compile_decl_err("((defconst CONSTANT 1) (defn foo () (set CONSTANT 2)))"), Err(PError::from(GDError::new(GDErrorF::CannotAssignTo(String::from("CONSTANT")), SourceOffset(50)))), ); } #[test] pub fn weird_name_test() { assert_eq!(parse_compile_and_output("(let ((a-b-c 1)) (a-b-c:d-e-f))"), "var a_b_c = 1\nreturn a_b_c.d_e_f()\n"); } #[test] pub fn assign_to_slot_test() { assert_eq!(parse_compile_and_output("(let ((x 1)) (set x:foo 100) 2)"), "var x = 1\nx.foo = 100\nreturn 2\n"); assert_eq!(parse_compile_and_output("(let ((x 1)) (set x:foo 100))"), "var x = 1\nx.foo = 100\nreturn 100\n"); assert_eq!(parse_compile_and_output("(flet ((f () 1)) (set (f):foo 100) 2)"), "_flet().foo = 100\nreturn 2\n"); assert_eq!(parse_compile_and_output("(flet ((f () 1)) (set (f):foo 100))"), "_flet().foo = 100\nreturn 100\n"); } #[test] pub fn sys_this_file_test() { assert_eq!(parse_compile_and_output("(sys/special-ref this-file)"), "return load(\"res://TEST.gd\")\n"); } #[test] pub fn this_file_test() { assert_eq!(parse_compile_and_output("(this-file)"), "return load(\"res://TEST.gd\")\n"); } #[test] pub fn sys_this_file_run_test() { let output = parse_and_run(r#" ((defn foo (x) (* x 2)) (print (foo 124)) (print ((sys/special-ref this-file):foo 124)))"#); assert_eq!(output, "\n248\n248\n"); } #[test] pub fn sys_this_file_run_in_macro_test() { let output = parse_and_run(r#" ((defn foo (x) (* x 2)) (defmacro baz () '((sys/special-ref this-file):foo 124)) (print (baz)))"#); assert_eq!(output, "\n248\n"); } #[test] pub fn this_file_run_test() { let output = parse_and_run(r#" ((defn foo (x) (* x 2)) (print (foo 124)) (print ((this-file):foo 124)))"#); assert_eq!(output, "\n248\n248\n"); } #[test] pub fn this_file_run_in_macro_test() { let output = parse_and_run(r#" ((defn foo (x) (* x 2)) (defmacro baz () '((this-file):foo 124)) (print (baz)))"#); assert_eq!(output, "\n248\n"); } #[test] pub fn assign_to_vec_test_1() { let output = parse_and_run(r#" ((let ((v (vector 1 1 1))) (print v:x) (set v:x 10) (print v:x))) "#); assert_eq!(output, "\n1\n10\n"); } #[test] pub fn assign_to_vec_test_2() { let output = parse_and_run(r#" ((let ((v (vector 1 1 1))) (print v:x) (funcall (lambda () (set v:x 10))) (print v:x))) "#); assert_eq!(output, "\n1\n10\n"); } #[test] pub fn compound_assignment_test() { assert_eq!(parse_compile_and_output("(let ((a 1)) (update a (+ 2)))"), r#"var a = 1 a = a + 2 return a "#); } #[test] pub fn compound_assignment_run_test_1() { assert_eq!(parse_and_run("((let ((a 1)) (update a (+ 2)) (print a)))"), "\n3\n"); } #[test] pub fn compound_assignment_run_test_2() { assert_eq!(parse_and_run("((defn foo (x) (* x 2)) (let ((a 9)) (update a foo) (print a)))"), "\n18\n"); } ================================================ FILE: tests/test/string_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] pub fn basic_string_lit_tests() { assert_eq!(parse_compile_and_output(r#""a""#), "return \"a\"\n"); assert_eq!(parse_compile_and_output(r#""ab\"cd""#), "return \"ab\\\"cd\"\n"); assert_eq!(parse_compile_and_output(r#""α""#), "return \"α\"\n"); assert_eq!(parse_compile_and_output("\"α\nβ\""), "return \"α\\nβ\"\n"); } #[test] pub fn basic_node_path_test_1() { assert_eq!(parse_compile_decl(r#"((defclass Foo (Reference) main (defn foo () $a)))"#), r#"extends Reference func _init(): pass func foo(): return $a "#); } #[test] pub fn basic_node_path_test_2() { assert_eq!(parse_compile_decl(r#"((defclass Foo (Reference) main (defn foo () $"a")))"#), r#"extends Reference func _init(): pass func foo(): return $a "#); } #[test] pub fn basic_node_path_test_3() { assert_eq!(parse_compile_decl(r#"((defclass Foo (Reference) main (defn foo () $b-c)))"#), r#"extends Reference func _init(): pass func foo(): return $"b-c" "#); } #[test] pub fn basic_node_path_test_4() { assert_eq!(parse_compile_decl(r#"((defclass Foo (Reference) main (defn foo () $"b c")))"#), r#"extends Reference func _init(): pass func foo(): return $"b c" "#); } #[test] pub fn basic_node_path_test_5() { assert_eq!(parse_compile_decl(r#"((defclass Foo (Reference) main (defn foo () $"5\nc")))"#), r#"extends Reference func _init(): pass func foo(): return $"5\nc" "#); } #[test] pub fn basic_node_path_test_6() { // Note: \n is a *literal* newline in the code here, which will be // converted to a "backslash-n" sequence in GDScript. assert_eq!(parse_compile_decl("((defclass Foo (Reference) main (defn foo () $\"5\nc\")))"), r#"extends Reference func _init(): pass func foo(): return $"5\nc" "#); } ================================================ FILE: tests/test/type_checking_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] pub fn primitive_instance_check_test_1() { let result = parse_and_run(r#" ((print (instance? 1 Int)) (print (instance? 1.0 Float)) (print (instance? 1 Float)) (print (instance? (Reference:new) Object)) (print (instance? (Reference:new) Reference)) (print (instance? (Reference:new) Node)) (print (instance? (Reference:new) Array))) "#); assert_eq!(result, "\nTrue\nTrue\nFalse\nTrue\nTrue\nFalse\nFalse\n"); } #[test] pub fn primitive_instance_check_test_2() { let result = parse_and_run(r#" ((print (instance? V{1 2} Vector3)) (print (instance? V{1 2 3} Vector3)) (print (instance? V{1 2} Vector2)) (print (instance? V{1 2 3} Vector2)) (print (instance? 0 Vector2)) (print (instance? "A" Vector3))) "#); assert_eq!(result, "\nFalse\nTrue\nTrue\nFalse\nFalse\nFalse\n"); } #[test] pub fn primitive_instance_check_test_3() { let result = parse_and_run(r#" ((print (instance? V{1 2} Transform2D)) (print (instance? V{1 2 3} Transform2D)) (print (instance? (Transform2D 0 V{1 1}) Transform2D))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\n"); } #[test] pub fn primitive_instance_check_test_4() { let result = parse_and_run(r#" ((print (instance? (Color 0 0 0 1) Color)) (print (instance? Color:red Color)) (print (instance? 0.1 Color))) "#); assert_eq!(result, "\nTrue\nTrue\nFalse\n"); } #[test] pub fn primitive_instance_check_test_5() { let result = parse_and_run(r#" ((print (instance? Transform:IDENTITY Transform)) (print (instance? Color:red Transform)) (print (instance? Plane:PLANE_YZ Plane)) (print (instance? Color:red Plane))) "#); assert_eq!(result, "\nTrue\nFalse\nTrue\nFalse\n"); } #[test] pub fn primitive_instance_check_test_6() { let result = parse_and_run(r#" ((print (instance? Basis:IDENTITY Basis)) (print (instance? Color:red Basis)) (print (instance? Quat:IDENTITY Quat)) (print (instance? Color:red Quat))) "#); assert_eq!(result, "\nTrue\nFalse\nTrue\nFalse\n"); } #[test] pub fn object_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node Object)) (print (instance? (Reference:new) Object)) (print (instance? 3 Object)) (print (instance? [3] Object)) (print (instance? {"a" 3} Object)) (print (instance? nil Object)) (my-node:free))) "#); assert_eq!(result, "\nTrue\nTrue\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn reference_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node Reference)) (print (instance? (Reference:new) Reference)) (print (instance? 3 Reference)) (print (instance? [3] Reference)) (print (instance? {"a" 3} Reference)) (print (instance? nil Reference)) (my-node:free))) "#); assert_eq!(result, "\nFalse\nTrue\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn node_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node Node)) (print (instance? (Reference:new) Node)) (print (instance? 3 Node)) (print (instance? [3] Node)) (print (instance? {"a" 3} Node)) (print (instance? nil Node)) (my-node:free))) "#); assert_eq!(result, "\nTrue\nFalse\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn custom_reference_instance_check_test() { let result = parse_and_run(r#" ((defclass Foo (Reference)) (let ((my-node (Node:new))) (print (instance? my-node Foo)) (print (instance? (Reference:new) Foo)) (print (instance? (Foo:new) Foo)) (print (instance? 3 Foo)) (print (instance? [3] Foo)) (print (instance? {"a" 3} Foo)) (print (instance? nil Foo)) (my-node:free))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn custom_node_instance_check_test() { let result = parse_and_run(r#" ((defclass Foo (Node)) (let ((my-node (Node:new)) (my-foo (Foo:new))) (print (instance? my-node Foo)) (print (instance? (Reference:new) Foo)) (print (instance? my-foo Foo)) (print (instance? 3 Foo)) (print (instance? [3] Foo)) (print (instance? {"a" 3} Foo)) (print (instance? nil Foo)) (my-node:free) (my-foo:free))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn number_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node Number)) (print (instance? (Reference:new) Number)) (print (instance? 3 Number)) (print (instance? 3.1 Number)) (print (instance? [3] Number)) (print (instance? {"a" 3} Number)) (print (instance? nil Number)) (my-node:free))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nTrue\nFalse\nFalse\nFalse\n"); } #[test] pub fn any_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node Any)) (print (instance? (Reference:new) Any)) (print (instance? 3 Any)) (print (instance? 3.1 Any)) (print (instance? [3] Any)) (print (instance? {"a" 3} Any)) (print (instance? nil Any)) (my-node:free))) "#); assert_eq!(result, "\nTrue\nTrue\nTrue\nTrue\nTrue\nTrue\nTrue\n"); } #[test] pub fn anyref_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node AnyRef)) (print (instance? (Reference:new) AnyRef)) (print (instance? 3 AnyRef)) (print (instance? 3.1 AnyRef)) (print (instance? [3] AnyRef)) (print (instance? {"a" 3} AnyRef)) (print (instance? nil AnyRef)) (my-node:free))) "#); assert_eq!(result, "\nTrue\nTrue\nFalse\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn anyval_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node AnyVal)) (print (instance? (Reference:new) AnyVal)) (print (instance? 3 AnyVal)) (print (instance? 3.1 AnyVal)) (print (instance? [3] AnyVal)) (print (instance? {"a" 3} AnyVal)) (print (instance? nil AnyVal)) (my-node:free))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nTrue\nTrue\nTrue\nTrue\n"); } #[test] pub fn nothing_instance_check_test() { let result = parse_and_run(r#" ((let ((my-node (Node:new))) (print (instance? my-node Nothing)) (print (instance? (Reference:new) Nothing)) (print (instance? 3 Nothing)) (print (instance? 3.1 Nothing)) (print (instance? [3] Nothing)) (print (instance? {"a" 3} Nothing)) (print (instance? nil Nothing)) (my-node:free))) "#); assert_eq!(result, "\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n"); } #[test] pub fn array_instance_check_test() { let result = parse_and_run(r#" ((print (instance? (Reference:new) Array)) (print (instance? 3 Array)) (print (instance? [3] Array)) (print (instance? ((literally PoolIntArray) [3]) Array))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nFalse\n"); } #[test] pub fn specific_array_instance_check_test() { let result = parse_and_run(r#" ((print (instance? (Reference:new) PoolIntArray)) (print (instance? 3 PoolIntArray)) (print (instance? [3] PoolIntArray)) (print (instance? ((literally PoolIntArray) [3]) PoolIntArray))) "#); assert_eq!(result, "\nFalse\nFalse\nFalse\nTrue\n"); } #[test] pub fn base_array_instance_check_test() { let result = parse_and_run(r#" ((print (instance? (Reference:new) BaseArray)) (print (instance? 3 BaseArray)) (print (instance? [3] BaseArray)) (print (instance? ((literally PoolIntArray) [3]) BaseArray))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nTrue\n"); } #[test] pub fn typeof_primitive_int_test() { let result = parse_and_run(r#" ((let ((t (typeof 100))) (print (instance? 100 t)) (print (instance? -2 t)) (print (instance? "A" t)) (print (instance? (Reference:new) t)))) "#); assert_eq!(result, "\nTrue\nTrue\nFalse\nFalse\n"); } #[test] pub fn typeof_primitive_string_test() { let result = parse_and_run(r#" ((let ((t (typeof "A"))) (print (instance? 100 t)) (print (instance? -2 t)) (print (instance? "A" t)) (print (instance? (Reference:new) t)))) "#); assert_eq!(result, "\nFalse\nFalse\nTrue\nFalse\n"); } #[test] pub fn typeof_array_test() { let result = parse_and_run(r#" ((let ((t (typeof []))) (print (instance? [] t)) (print (instance? [3] t)) (print (instance? ((literally PoolIntArray) []) t)) (print (instance? (Reference:new) t)))) "#); assert_eq!(result, "\nTrue\nTrue\nFalse\nFalse\n"); } #[test] pub fn typeof_class_test() { let result = parse_and_run(r#" ((defclass Foo (Reference)) (let ((foo (Foo:new))) (print (= (typeof foo) Foo)))) "#); assert_eq!(result, "\nTrue\n"); } #[test] pub fn typeof_ref_test() { let result = parse_and_run(r#" ((let ((r (Reference:new))) (print (= (typeof r) Reference)))) "#); assert_eq!(result, "\nTrue\n"); } #[test] pub fn typeof_node_test() { let result = parse_and_run(r#" ((let ((node (Node2D:new))) (print (= (typeof node) Node2D)) (node:free))) "#); assert_eq!(result, "\nTrue\n"); } ================================================ FILE: tests/test/typecast_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; #[test] pub fn int_test() { assert_eq!(parse_compile_and_output("(int 10)"), "return int(10)\n"); } #[test] pub fn int_running_test() { assert_eq!(parse_and_run("((print (int 10.5)))"), "\n10\n"); assert_eq!(parse_and_run("((print (int \"4\")))"), "\n4\n"); } #[test] pub fn str_test() { assert_eq!(parse_compile_and_output("(str 10)"), "return str(10)\n"); } #[test] pub fn str_running_test() { assert_eq!(parse_and_run("((print (str 10.5)))"), "\n10.5\n"); assert_eq!(parse_and_run("((print (str 4)))"), "\n4\n"); assert_eq!(parse_and_run("((print (str \"foo\")))"), "\nfoo\n"); assert_eq!(parse_and_run("((print (str nil)))"), "\nNull\n"); } #[test] pub fn bool_test() { // We wrap bool(...) to support more types. assert_eq!(parse_compile_and_output("(bool 10)"), "return GDLisp._bool(10)\n"); } #[test] pub fn char_test() { assert_eq!(parse_compile_and_output("(char 10)"), "return char(10)\n"); } #[test] pub fn ord_test() { assert_eq!(parse_compile_and_output("(ord \"A\")"), "return ord(\"A\")\n"); } #[test] pub fn bool_running_test() { assert_eq!(parse_and_run("((print (bool 10)) (print (bool 0)) (print (bool (Reference:new))))"), "\nTrue\nFalse\nTrue\n"); } ================================================ FILE: tests/test/while_test.rs ================================================ // Copyright 2023 Silvio Mayolo // // This file is part of GDLisp. // // GDLisp is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // GDLisp is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with GDLisp. If not, see . use super::common::*; use gdlisp::pipeline::error::PError; use gdlisp::pipeline::source::SourceOffset; use gdlisp::ir::loops::error::LoopPrimitiveError; #[test] pub fn while_tests() { assert_eq!(parse_compile_and_output("(while 1)"), "while 1:\n pass\nreturn null\n"); assert_eq!(parse_compile_and_output("(while (foo) (foo1 0) (foo2 0 0))"), "while foo():\n foo1(0)\n foo2(0, 0)\nreturn null\n"); assert_eq!(parse_compile_and_output("(foo1 (while (bar)))"), "while bar():\n pass\nreturn foo1(null)\n"); } #[test] pub fn compound_while_tests() { // If expressions cannot be compiled into a single GDScript // expression, so this forces the while loop to use the "compound" // form. assert_eq!(parse_compile_and_output("(while (if 1 2 3) (foo))"), "while true:\n var _cond = null\n if 1:\n _cond = 2\n else:\n if true:\n _cond = 3\n else:\n _cond = null\n if !_cond:\n break\n foo()\nreturn null\n") } #[test] pub fn while_test_with_break() { assert_eq!(parse_compile_and_output("(while 1 (break))"), r#"while 1: break return null "#); } #[test] pub fn while_test_with_continue() { assert_eq!(parse_compile_and_output("(while 1 (continue))"), r#"while 1: continue return null "#); } #[test] pub fn while_test_with_break_in_if() { assert_eq!(parse_compile_and_output("(while 1 (if 1 (break) (continue)))"), r#"while 1: if 1: break else: if true: continue else: pass return null "#); } #[test] pub fn while_test_with_break_in_if_cond() { assert_eq!(parse_compile_and_output("(while (if 1 (break) (continue)) (foo))"), r#"while true: var _cond = null if 1: break _cond = null else: if true: continue _cond = null else: _cond = null if !_cond: break foo() return null "#); } #[test] pub fn bad_break_test() { assert_eq!(parse_compile_and_output_err("(break)"), Err(PError::from(LoopPrimitiveError::break_error(SourceOffset(0))))); } #[test] pub fn bad_continue_test() { assert_eq!(parse_compile_and_output_err("(continue)"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(0))))); } #[test] pub fn bad_break_in_plain_lambda_test() { assert_eq!(parse_compile_and_output_err("(lambda () (break))"), Err(PError::from(LoopPrimitiveError::break_error(SourceOffset(11))))); } #[test] pub fn bad_continue_in_plain_lambda_test() { assert_eq!(parse_compile_and_output_err("(lambda () (continue))"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(11))))); } #[test] pub fn bad_break_in_loop_lambda_test() { assert_eq!(parse_compile_and_output_err("(while 1 (lambda () (break)))"), Err(PError::from(LoopPrimitiveError::break_error(SourceOffset(20)).in_closure()))); } #[test] pub fn bad_continue_in_loop_lambda_test() { assert_eq!(parse_compile_and_output_err("(while 1 (lambda () (continue)))"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(20)).in_closure()))); } #[test] pub fn bad_continue_in_loop_lambda_class_test() { assert_eq!(parse_compile_and_output_err("(while 1 (new Reference (defn foo () (continue))))"), Err(PError::from(LoopPrimitiveError::continue_error(SourceOffset(37)).in_closure()))); }