├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── package.yml │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── archive │ ├── engine │ │ └── config │ │ │ └── base │ │ │ └── scripts.ini │ └── r6 │ │ └── config │ │ └── cybercmd │ │ └── scc.toml └── reds │ └── boot.reds ├── crates ├── cli │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── compiler │ ├── api │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── backend │ │ ├── Cargo.toml │ │ ├── src │ │ │ ├── assemble.rs │ │ │ ├── assemble │ │ │ │ └── closure.rs │ │ │ ├── inputs.rs │ │ │ ├── known_types.rs │ │ │ ├── lib.rs │ │ │ └── monomorph.rs │ │ └── tests │ │ │ ├── bytecode.rs │ │ │ ├── data │ │ │ ├── array-literal.reds │ │ │ ├── break.reds │ │ │ ├── cast.reds │ │ │ ├── continue.reds │ │ │ ├── dyn-cast.reds │ │ │ ├── enum.reds │ │ │ ├── excluded.reds │ │ │ ├── for-loop.reds │ │ │ ├── generic-struct.reds │ │ │ ├── if-else.reds │ │ │ ├── implicit-convert.reds │ │ │ ├── lambda.reds │ │ │ ├── local-init.reds │ │ │ ├── name-of.reds │ │ │ ├── named-impl.reds │ │ │ ├── nested-for-loop.reds │ │ │ ├── number-literal.reds │ │ │ ├── return-void.reds │ │ │ ├── static-arrays.reds │ │ │ ├── string-interp.reds │ │ │ ├── string-literal.reds │ │ │ ├── struct.reds │ │ │ ├── super.reds │ │ │ ├── switch.reds │ │ │ ├── ternary.reds │ │ │ └── variant.reds │ │ │ └── snapshots │ │ │ ├── bytecode__bytecode@array-literal.reds.snap │ │ │ ├── bytecode__bytecode@break.reds.snap │ │ │ ├── bytecode__bytecode@cast.reds.snap │ │ │ ├── bytecode__bytecode@continue.reds.snap │ │ │ ├── bytecode__bytecode@dyn-cast.reds.snap │ │ │ ├── bytecode__bytecode@enum.reds.snap │ │ │ ├── bytecode__bytecode@excluded.reds.snap │ │ │ ├── bytecode__bytecode@for-loop.reds.snap │ │ │ ├── bytecode__bytecode@generic-struct.reds.snap │ │ │ ├── bytecode__bytecode@if-else.reds.snap │ │ │ ├── bytecode__bytecode@implicit-convert.reds.snap │ │ │ ├── bytecode__bytecode@lambda.reds.snap │ │ │ ├── bytecode__bytecode@local-init.reds.snap │ │ │ ├── bytecode__bytecode@name-of.reds.snap │ │ │ ├── bytecode__bytecode@named-impl.reds.snap │ │ │ ├── bytecode__bytecode@nested-for-loop.reds.snap │ │ │ ├── bytecode__bytecode@number-literal.reds.snap │ │ │ ├── bytecode__bytecode@return-void.reds.snap │ │ │ ├── bytecode__bytecode@static-arrays.reds.snap │ │ │ ├── bytecode__bytecode@string-interp.reds.snap │ │ │ ├── bytecode__bytecode@string-literal.reds.snap │ │ │ ├── bytecode__bytecode@struct.reds.snap │ │ │ ├── bytecode__bytecode@super.reds.snap │ │ │ ├── bytecode__bytecode@switch.reds.snap │ │ │ ├── bytecode__bytecode@ternary.reds.snap │ │ │ └── bytecode__bytecode@variant.reds.snap │ └── frontend │ │ ├── Cargo.toml │ │ ├── src │ │ ├── cte.rs │ │ ├── diagnostic.rs │ │ ├── diagnostic │ │ │ ├── pass.rs │ │ │ └── pass │ │ │ │ └── unused_locals.rs │ │ ├── ir.rs │ │ ├── lib.rs │ │ ├── lower.rs │ │ ├── modules.rs │ │ ├── stages.rs │ │ ├── stages │ │ │ ├── infer.rs │ │ │ └── resolution.rs │ │ ├── symbols.rs │ │ ├── types.rs │ │ ├── utils.rs │ │ ├── utils │ │ │ ├── fmt.rs │ │ │ ├── lazy.rs │ │ │ └── scoped_map.rs │ │ └── visitor.rs │ │ └── tests │ │ ├── data │ │ ├── annotation-checks.reds │ │ ├── class-checks.reds │ │ ├── enum-checks.reds │ │ ├── expr-checks.reds │ │ ├── function-checks.reds │ │ ├── inference.reds │ │ ├── interpolation-error.reds │ │ ├── invariance-checks.reds │ │ ├── item-checks.reds │ │ └── type-mismatch.reds │ │ ├── errors.rs │ │ └── snapshots │ │ ├── errors__compilation_errors@annotation-checks.reds.snap │ │ ├── errors__compilation_errors@class-checks.reds.snap │ │ ├── errors__compilation_errors@enum-checks.reds.snap │ │ ├── errors__compilation_errors@expr-checks.reds.snap │ │ ├── errors__compilation_errors@function-checks.reds.snap │ │ ├── errors__compilation_errors@inference.reds.snap │ │ ├── errors__compilation_errors@interpolation-error.reds.snap │ │ ├── errors__compilation_errors@invariance-checks.reds.snap │ │ ├── errors__compilation_errors@item-checks.reds.snap │ │ └── errors__compilation_errors@type-mismatch.reds.snap ├── decompiler │ ├── Cargo.toml │ └── src │ │ ├── control_flow.rs │ │ ├── decompiler.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── location.rs ├── dotfile │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── io │ ├── Cargo.toml │ ├── examples │ │ └── cli.rs │ ├── src │ │ ├── bundle.rs │ │ ├── definition.rs │ │ ├── index.rs │ │ ├── instr.rs │ │ ├── lib.rs │ │ └── util.rs │ └── tests │ │ └── tests.rs ├── scc │ ├── capi │ │ ├── Cargo.toml │ │ ├── include │ │ │ └── scc.h │ │ └── src │ │ │ └── lib.rs │ ├── cli │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src │ │ │ ├── arguments.rs │ │ │ ├── capi.rs │ │ │ ├── main.rs │ │ │ └── raw.rs │ └── shared │ │ ├── Cargo.toml │ │ └── src │ │ ├── hints.rs │ │ ├── lib.rs │ │ ├── logger.rs │ │ ├── output.rs │ │ ├── report.rs │ │ ├── settings.rs │ │ └── timestamp.rs └── syntax │ ├── ast │ ├── Cargo.toml │ └── src │ │ ├── ast.rs │ │ ├── files.rs │ │ ├── lib.rs │ │ ├── span.rs │ │ └── visitor.rs │ ├── formatter │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── tests │ │ ├── data │ │ ├── commented.reds │ │ ├── control-flow.reds │ │ ├── module.reds │ │ └── operators.reds │ │ ├── formatted.rs │ │ └── snapshots │ │ ├── formatted__formatted_files@commented.reds.snap │ │ ├── formatted__formatted_files@control-flow.reds.snap │ │ ├── formatted__formatted_files@module.reds.snap │ │ └── formatted__formatted_files@operators.reds.snap │ └── parser │ ├── Cargo.toml │ └── src │ ├── lexer.rs │ ├── lib.rs │ ├── parser.rs │ └── parser │ ├── expr.rs │ ├── item.rs │ └── stmt.rs ├── docs └── types │ ├── Array.md │ ├── Bool.md │ ├── Cname.md │ ├── Double.md │ ├── Float.md │ ├── Int16.md │ ├── Int32.md │ ├── Int64.md │ ├── Int8.md │ ├── Nothing.md │ ├── Ref.md │ ├── ResRef.md │ ├── ScriptRef.md │ ├── String.md │ ├── TweakDbId.md │ ├── Uint16.md │ ├── Uint32.md │ ├── Uint64.md │ ├── Uint8.md │ ├── Variant.md │ ├── Void.md │ └── Wref.md └── scripts ├── build-pgo.ps1 └── package.ps1 /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | linker = "rust-lld.exe" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["*.*.x"] 6 | pull_request: 7 | branches: ["*.*.x"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check-format: 14 | name: Check formatting 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - run: rustup toolchain install nightly --profile minimal --component rustfmt --no-self-update 19 | - run: cargo +nightly fmt --all -- --check 20 | 21 | lint: 22 | name: Lint 23 | runs-on: windows-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - run: rustup toolchain install stable --profile minimal --component clippy --no-self-update 27 | - uses: Swatinem/rust-cache@v2 28 | - run: cargo clippy --all-features -- -D warnings 29 | 30 | test: 31 | name: Test 32 | runs-on: windows-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - run: rustup toolchain install stable --profile minimal --no-self-update 36 | - uses: Swatinem/rust-cache@v2 37 | - run: cargo test --all-features 38 | 39 | package: 40 | name: Package and upload artifacts 41 | uses: ./.github/workflows/package.yml 42 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | env: 5 | CARGO_TERM_COLOR: always 6 | 7 | jobs: 8 | package: 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | filter: tree:0 15 | - run: rustup toolchain install stable --profile minimal --no-self-update 16 | - uses: Swatinem/rust-cache@v2 17 | - run: cargo build --release 18 | - id: tag 19 | run: echo "tag=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $env:GITHUB_OUTPUT 20 | - run: .\scripts\package.ps1 -zipPath redscript-${{ steps.tag.outputs.tag }}.zip 21 | - uses: actions/upload-artifact@v4 22 | with: 23 | path: | 24 | redscript-${{ steps.tag.outputs.tag }}.zip 25 | target/release/redscript-cli.exe 26 | if-no-files-found: error 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | - "v[0-9]+.[0-9]+.[0-9]+-*" 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | package: 14 | name: Package and upload artifacts 15 | uses: ./.github/workflows/package.yml 16 | 17 | publish: 18 | name: Publish release 19 | runs-on: ubuntu-latest 20 | needs: package 21 | permissions: 22 | contents: write 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/download-artifact@v4 26 | with: 27 | path: artifacts 28 | merge-multiple: true 29 | - name: Publish release 30 | env: 31 | GH_TOKEN: ${{ github.token }} 32 | run: | 33 | FLAGS=() 34 | ARTIFACTS=$(find ./artifacts -type f) 35 | 36 | if echo ${{ github.ref_name }} | grep -E 'v[0-9]+\.[0-9]+\.[0-9]+-.+'; then 37 | FLAGS+=(--prerelease) 38 | fi 39 | 40 | gh release create ${{ github.ref_name }} ${FLAGS[@]} $ARTIFACTS 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | final.redscripts.modded 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | unstable_features = true 3 | use_field_init_shorthand = true 4 | imports_granularity = "Module" 5 | reorder_imports = true 6 | group_imports = "StdExternalCrate" 7 | reorder_impl_items = true 8 | reorder_modules = true 9 | edition = "2024" 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/syntax/ast", 5 | "crates/syntax/parser", 6 | "crates/syntax/formatter", 7 | "crates/io", 8 | "crates/compiler/frontend", 9 | "crates/compiler/backend", 10 | "crates/compiler/api", 11 | "crates/decompiler", 12 | "crates/cli", 13 | "crates/scc/shared", 14 | "crates/scc/capi", 15 | "crates/scc/cli", 16 | "crates/dotfile", 17 | ] 18 | 19 | [workspace.package] 20 | version = "1.0.0-preview.13" 21 | authors = ["jekky"] 22 | edition = "2024" 23 | 24 | [workspace.dependencies] 25 | redscript-ast.path = "crates/syntax/ast" 26 | redscript-parser.path = "crates/syntax/parser" 27 | redscript-formatter.path = "crates/syntax/formatter" 28 | redscript-io.path = "crates/io" 29 | redscript-compiler-frontend.path = "crates/compiler/frontend" 30 | redscript-compiler-backend.path = "crates/compiler/backend" 31 | redscript-compiler-api.path = "crates/compiler/api" 32 | redscript-decompiler.path = "crates/decompiler" 33 | scc-shared.path = "crates/scc/shared" 34 | redscript-dotfile.path = "crates/dotfile" 35 | 36 | log = "0.4" 37 | thiserror = "2" 38 | anyhow = "1" 39 | chrono = "0.4" 40 | derive-where = "1" 41 | paste = "1" 42 | bitflags = "2" 43 | bitfield-struct = "0.11" 44 | identity-hash = "0.1" 45 | foldhash = "0.1" 46 | slab = "0.4" 47 | smallvec = { version = "1", features = ["union", "const_generics"] } 48 | hashbrown = "0.15" 49 | indexmap = "2" 50 | elsa = { version = "1", features = ["indexmap"] } 51 | serde = "1" 52 | toml = { version = "0.8", default-features = false } 53 | fern = "0.7" 54 | flexi_logger = { version = "0.30", default-features = false } 55 | argh = "0.1" 56 | chumsky = { version = "1.0.0-alpha.7", features = ["label"] } 57 | crc32fast = "1" 58 | ignore = "0.4" 59 | vmap = "0.6" 60 | file-id = "0.2" 61 | fd-lock = "4" 62 | msgbox = "0.7" 63 | pretty_dtoa = "0.3" 64 | minidl = "0.1" 65 | bindgen = "0.71" 66 | mimalloc = "0.1" 67 | 68 | similar-asserts = "1" 69 | insta = { version = "1", features = ["glob"] } 70 | 71 | [workspace.dependencies.byte] 72 | git = "https://github.com/jac3km4/byte" 73 | rev = "da71833" 74 | features = ["alloc", "derive"] 75 | 76 | [workspace.dependencies.sequence_trie] 77 | git = "https://github.com/jac3km4/rust_sequence_trie" 78 | rev = "20c28c4" 79 | features = ["hashbrown"] 80 | 81 | [patch.crates-io] 82 | stable_deref_trait = { git = "https://github.com/Storyyeller/stable_deref_trait", rev = "59a35e0" } 83 | 84 | [workspace.lints.rust] 85 | warnings = "warn" 86 | future-incompatible = "warn" 87 | let-underscore = "warn" 88 | nonstandard-style = "warn" 89 | rust-2018-compatibility = "warn" 90 | rust-2018-idioms = "warn" 91 | rust-2021-compatibility = "warn" 92 | rust-2024-compatibility = "warn" 93 | 94 | [workspace.lints.clippy] 95 | all = "warn" 96 | match_same_arms = "warn" 97 | semicolon_if_nothing_returned = "warn" 98 | single_match_else = "warn" 99 | redundant_closure_for_method_calls = "warn" 100 | cloned_instead_of_copied = "warn" 101 | redundant_else = "warn" 102 | unnested_or_patterns = "warn" 103 | unreadable_literal = "warn" 104 | type_repetition_in_bounds = "warn" 105 | equatable_if_let = "warn" 106 | implicit_clone = "warn" 107 | default_trait_access = "warn" 108 | explicit_deref_methods = "warn" 109 | explicit_iter_loop = "warn" 110 | inefficient_to_string = "warn" 111 | match_bool = "warn" 112 | 113 | [profile.release] 114 | strip = true 115 | lto = true 116 | codegen-units = 1 117 | panic = "abort" 118 | 119 | [workspace.metadata.release] 120 | pre-release-commit-message = "chore: release {{version}}" 121 | tag-prefix = "" 122 | shared-version = true 123 | publish = false 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 jac3km4 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

REDscript

5 | 6 | REDscript is a statically-typed, object-oriented programming language designed for developing scripts for the REDengine, CD Projekt's game engine used in Cyberpunk 2077. If you want to learn more about the language, visit the [official wiki](https://wiki.redmodding.org/redscript). 7 | 8 | ## Installation 9 | 10 | To compile your scripts automatically on game startup, follow these steps: 11 | 12 | - Download the `redscript-mod-{version}.zip` archive from the [latest release](https://github.com/jac3km4/redscript/releases/latest). 13 | - Extract it into the main `Cyberpunk 2077` directory. 14 | - When you start the game, successful setup can be confirmed with logs appearing in `Cyberpunk 2077/r6/logs/redscript_rCURRENT.log`. 15 | 16 | ## Editor Support 17 | 18 | REDscript provides extensive editor tooling, including a language server and a debugger that integrate with many popular editors such as VSCode, neovim, IntelliJ IDEA, Zed, and Helix. For detailed setup instructions, check out the [REDscript IDE repository](https://github.com/jac3km4/redscript-ide). 19 | 20 | ## Project Structure 21 | 22 | This project includes various tools for working with REDscript, such as a compiler, a decompiler, and a formatter. These are organized as modular Rust crates located in the `crates` directory: 23 | 24 | - **`cli`** - Exposes a command-line interface serving as the entry point for all tools in this project. 25 | - **`compiler/frontend`** - Performs static analysis on REDscript syntax, lowering it to an intermediate representation. 26 | - **`compiler/backend`** - Generates bytecode from the intermediate representation and handles monomorphization for generic functions and types. 27 | - **`compiler/api`** - Provides a high-level API for interacting seamlessly with the compiler. 28 | - **`decompiler`** - Decompiles bytecode back into REDscript source code. 29 | - **`io`** - Reads and writes binary files in the native REDengine format. 30 | - **`scc/cli`** - Exposes a CLI that acts as a drop-in replacement for the original compiler made by CD Projekt. 31 | - **`scc/capi`** - Exposes a C API with an interface similar to the `scc` CLI, but with some additional capabilities like source mapping. 32 | - **`scc/shared`** - Contains common code used across the scc C API and CLI. 33 | - **`syntax/ast`** - Defines the Abstract Syntax Tree (AST) for REDscript. 34 | - **`syntax/formatter`** - Formats REDscript source code according to configuration. 35 | - **`syntax/parser`** - Parses REDscript source code into the AST. 36 | - **`dotfile`** - Parses the configuration `.redscript` file. 37 | 38 | ### Component Diagram 39 | 40 | -------------------------------------------------------------------------------- /assets/archive/engine/config/base/scripts.ini: -------------------------------------------------------------------------------- 1 | [Scripts] 2 | EnableCompilation = "true" 3 | -------------------------------------------------------------------------------- /assets/archive/r6/config/cybercmd/scc.toml: -------------------------------------------------------------------------------- 1 | [args] 2 | scriptsBlobPath = "{game_dir}\\r6\\cache\\modded\\final.redscripts" 3 | 4 | [[tasks]] 5 | command = "InvokeScc" 6 | path = "{game_dir}\\r6\\scripts" 7 | custom_cache_dir = "{game_dir}\\r6\\cache\\modded" 8 | terminate_on_errors = false 9 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-cli" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-compiler-api = { workspace = true, features = ["ignore"] } 9 | redscript-decompiler.workspace = true 10 | redscript-formatter.workspace = true 11 | redscript-dotfile.workspace = true 12 | 13 | log.workspace = true 14 | anyhow.workspace = true 15 | hashbrown.workspace = true 16 | fern = { workspace = true, features = ["colored"] } 17 | vmap.workspace = true 18 | argh.workspace = true 19 | mimalloc.workspace = true 20 | 21 | [lints] 22 | workspace = true 23 | -------------------------------------------------------------------------------- /crates/compiler/api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-compiler-api" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-ast.workspace = true 9 | redscript-parser.workspace = true 10 | redscript-io = { workspace = true, features = ["mmap"] } 11 | redscript-compiler-frontend.workspace = true 12 | redscript-compiler-backend.workspace = true 13 | 14 | log.workspace = true 15 | thiserror.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [features] 21 | ignore = ["redscript-ast/ignore"] 22 | -------------------------------------------------------------------------------- /crates/compiler/api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::path::Path; 3 | 4 | pub use redscript_ast as ast; 5 | use redscript_ast::SourceMap; 6 | pub use redscript_compiler_backend::CompilationInputs; 7 | use redscript_compiler_backend::{AssembleError, PoolError, PoolMappings}; 8 | use redscript_compiler_frontend::UnknownSource; 9 | use redscript_compiler_frontend::pass::{DiagnosticPass, UnusedLocals}; 10 | pub use redscript_compiler_frontend::{ 11 | Aggregate, CompileErrorReporter, Diagnostic, Enum, Evaluator, Field, FunctionType, 12 | LoweredCompilationUnit, LoweredFunction, PolyType, Symbols, TypeId, TypeIndex, TypeInterner, 13 | TypeSchema, TypeScope, infer_from_sources, ir, parse_file, parse_files, pass, process_sources, 14 | types, 15 | }; 16 | use redscript_io::byte; 17 | pub use redscript_io::{SaveError, ScriptBundle}; 18 | use thiserror::Error; 19 | 20 | pub struct Compilation<'ctx> { 21 | sources: &'ctx SourceMap, 22 | symbols: Symbols<'ctx>, 23 | mappings: PoolMappings<'ctx>, 24 | unit: LoweredCompilationUnit<'ctx>, 25 | bundle: ScriptBundle<'ctx>, 26 | diagnostics: Diagnostics<'ctx>, 27 | } 28 | 29 | impl<'ctx> Compilation<'ctx> { 30 | pub fn new_with( 31 | bundle: &'ctx [u8], 32 | sources: &'ctx SourceMap, 33 | interner: &'ctx TypeInterner, 34 | passes: &[Box>], 35 | ) -> Result { 36 | let mut reporter = CompileErrorReporter::default(); 37 | let bundle = ScriptBundle::from_bytes(bundle)?; 38 | let (symbols, mappings) = CompilationInputs::load(&bundle, interner)?.into_inner(); 39 | let (unit, symbols) = infer_from_sources(sources, symbols, &mut reporter, interner); 40 | unit.run_diagnostics(passes, &mut reporter); 41 | 42 | let mut diagnostics = reporter.into_reported(); 43 | diagnostics.sort_by_key(Diagnostic::is_fatal); 44 | 45 | Ok(Self { 46 | sources, 47 | symbols, 48 | mappings, 49 | unit, 50 | bundle, 51 | diagnostics: Diagnostics(diagnostics), 52 | }) 53 | } 54 | 55 | pub fn new( 56 | bundle: &'ctx [u8], 57 | sources: &'ctx SourceMap, 58 | interner: &'ctx TypeInterner, 59 | ) -> Result { 60 | Self::new_with(bundle, sources, interner, &default_diagnostics()) 61 | } 62 | 63 | pub fn flush( 64 | mut self, 65 | path: impl AsRef, 66 | ) -> Result<(Symbols<'ctx>, Diagnostics<'ctx>), FlushError<'ctx>> { 67 | if self.diagnostics.has_fatal_errors() { 68 | return Err(FlushError::CompilationErrors(self.diagnostics)); 69 | } 70 | 71 | let mut monomorph = self.mappings.into_monomorphizer(self.sources); 72 | if let Err(err) = monomorph.monomorphize(&self.unit, &self.symbols, &mut self.bundle) { 73 | if let Some(span) = err.span() { 74 | self.diagnostics 75 | .push(Diagnostic::Other(Box::new(err), span)); 76 | return Err(FlushError::CompilationErrors(self.diagnostics)); 77 | } 78 | } 79 | 80 | self.bundle.into_writeable().save(path)?; 81 | Ok((self.symbols, self.diagnostics)) 82 | } 83 | 84 | pub fn symbols(&self) -> &Symbols<'ctx> { 85 | &self.symbols 86 | } 87 | 88 | pub fn unit(&self) -> &LoweredCompilationUnit<'ctx> { 89 | &self.unit 90 | } 91 | 92 | pub fn bundle(&self) -> &ScriptBundle<'ctx> { 93 | &self.bundle 94 | } 95 | 96 | pub fn diagnostics(&self) -> &Diagnostics<'ctx> { 97 | &self.diagnostics 98 | } 99 | } 100 | 101 | #[derive(Debug)] 102 | pub struct Diagnostics<'ctx>(Vec>); 103 | 104 | impl<'ctx> Diagnostics<'ctx> { 105 | pub fn has_fatal_errors(&self) -> bool { 106 | self.0.iter().any(Diagnostic::is_fatal) 107 | } 108 | 109 | pub fn dump(&self, sources: &SourceMap) -> Result<(), UnknownSource> { 110 | let mut warnings = 0; 111 | let mut errors = 0; 112 | 113 | for diagnostic in self { 114 | if diagnostic.is_fatal() { 115 | log::error!("{}", diagnostic.display(sources)?); 116 | errors += 1; 117 | } else { 118 | log::warn!("{}", diagnostic.display(sources)?); 119 | warnings += 1; 120 | } 121 | } 122 | log::info!("Completed with {} warnings and {} errors", warnings, errors); 123 | Ok(()) 124 | } 125 | 126 | fn push(&mut self, diagnostic: Diagnostic<'ctx>) { 127 | self.0.push(diagnostic); 128 | } 129 | } 130 | 131 | impl<'ctx> IntoIterator for Diagnostics<'ctx> { 132 | type IntoIter = std::vec::IntoIter>; 133 | type Item = Diagnostic<'ctx>; 134 | 135 | fn into_iter(self) -> Self::IntoIter { 136 | self.0.into_iter() 137 | } 138 | } 139 | 140 | impl<'a, 'ctx> IntoIterator for &'a Diagnostics<'ctx> { 141 | type IntoIter = std::slice::Iter<'a, Diagnostic<'ctx>>; 142 | type Item = &'a Diagnostic<'ctx>; 143 | 144 | fn into_iter(self) -> Self::IntoIter { 145 | self.0.iter() 146 | } 147 | } 148 | 149 | impl<'ctx> From>> for Diagnostics<'ctx> { 150 | fn from(diagnostics: Vec>) -> Self { 151 | Self(diagnostics) 152 | } 153 | } 154 | 155 | impl fmt::Display for Diagnostics<'_> { 156 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 157 | self.into_iter().try_for_each(|d| writeln!(f, "{}", d)) 158 | } 159 | } 160 | 161 | #[derive(Debug, Error)] 162 | pub enum Error { 163 | #[error("pool error: {0}")] 164 | Pool(#[from] PoolError), 165 | #[error("serialization error: {0}")] 166 | Decoding(#[from] byte::Error), 167 | } 168 | 169 | #[derive(Debug, Error)] 170 | pub enum FlushError<'ctx> { 171 | #[error("fatal diagnostis found")] 172 | CompilationErrors(Diagnostics<'ctx>), 173 | #[error("code generation error: {0}")] 174 | Assemble(AssembleError<'ctx>), 175 | #[error("write error: {0}")] 176 | Write(#[from] SaveError), 177 | } 178 | 179 | impl<'ctx> From> for FlushError<'ctx> { 180 | fn from(err: AssembleError<'ctx>) -> Self { 181 | Self::Assemble(err) 182 | } 183 | } 184 | 185 | pub trait SourceMapExt { 186 | fn populate_boot_lib(&self); 187 | } 188 | 189 | impl SourceMapExt for SourceMap { 190 | fn populate_boot_lib(&self) { 191 | self.push_front( 192 | "boot.reds", 193 | include_str!("../../../../assets/reds/boot.reds"), 194 | ); 195 | } 196 | } 197 | 198 | fn default_diagnostics<'ctx>() -> Vec>> { 199 | vec![Box::new(UnusedLocals)] 200 | } 201 | -------------------------------------------------------------------------------- /crates/compiler/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-compiler-backend" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-io = { workspace = true, features = ["identity-hash"] } 9 | redscript-compiler-frontend.workspace = true 10 | 11 | thiserror.workspace = true 12 | identity-hash.workspace = true 13 | slab.workspace = true 14 | smallvec.workspace = true 15 | hashbrown.workspace = true 16 | indexmap.workspace = true 17 | 18 | [dev-dependencies] 19 | redscript-compiler-api.workspace = true 20 | 21 | similar-asserts.workspace = true 22 | insta.workspace = true 23 | 24 | [lints] 25 | workspace = true 26 | -------------------------------------------------------------------------------- /crates/compiler/backend/src/known_types.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use hashbrown::HashSet; 4 | 5 | pub struct KnownTypes { 6 | sealed: HashSet<&'static str>, 7 | never_ref: HashSet<&'static str>, 8 | } 9 | 10 | impl KnownTypes { 11 | pub fn get() -> &'static Self { 12 | static INSTANCE: LazyLock = LazyLock::new(|| KnownTypes { 13 | sealed: SEALED_TYPES.iter().copied().collect(), 14 | never_ref: NEVER_REF_TYPES.iter().copied().collect(), 15 | }); 16 | 17 | &INSTANCE 18 | } 19 | 20 | pub fn is_sealed(&self, name: &str) -> bool { 21 | self.sealed.contains(name) 22 | } 23 | 24 | pub fn is_never_ref(&self, name: &str) -> bool { 25 | self.never_ref.contains(name) 26 | } 27 | } 28 | 29 | const NEVER_REF_TYPES: &[&str] = &["ReactionData"]; 30 | 31 | // generated with https://github.com/jac3km4/redscript-sealed-struct-dumper 32 | // this is a list of native structs that are fully exposed to scripts 33 | const SEALED_TYPES: &[&str] = &[ 34 | "AIActiveCommandList", 35 | "AICommandNodeFunction", 36 | "AIDelegateAttrRef", 37 | "AIDelegateTaskRef", 38 | "AIScriptUtils", 39 | "AVSpawnPointsRequestResult", 40 | "ActionDisplayData", 41 | "ActionPrereqs", 42 | "AmmoData", 43 | "AttackDebugData", 44 | "AttackInitContext", 45 | "BinkVideoSummary", 46 | "Box", 47 | "BuffInfo", 48 | "CachedBoolValue", 49 | "CharacterCustomizationAttribute", 50 | "CharactersChain", 51 | "ChatBoxText", 52 | "Color", 53 | "CombatSpaceHelper", 54 | "ComputerUIData", 55 | "ContextDisplayData", 56 | "ControllerHit", 57 | "CustomQuestNotificationData", 58 | "DSSSpawnRequestResult", 59 | "DamageInfo", 60 | "DebugDrawer", 61 | "DelayID", 62 | "DialogChoiceHubs", 63 | "DismemberedLimbCount", 64 | "DropInstruction", 65 | "EffectData", 66 | "EffectDurationModifierScriptContext", 67 | "EffectExecutionScriptContext", 68 | "EffectInfo", 69 | "EffectPreloadScriptContext", 70 | "EffectProviderScriptContext", 71 | "EffectScriptContext", 72 | "EffectSingleFilterScriptContext", 73 | "EngineTime", 74 | "EntityGameInterface", 75 | "EntityRequestComponentsInterface", 76 | "EntityResolveComponentsInterface", 77 | "EnumNameToIndexCache", 78 | "FragmentBuilder", 79 | "Frustum", 80 | "GOGRewardPack", 81 | "GameInstance", 82 | "GenericDataContent", 83 | "GetActionsContext", 84 | "HDRColor", 85 | "HandIKDescriptionResult", 86 | "HandleWithValue", 87 | "HitRepresentationQueryResult", 88 | "HitShapeResult", 89 | "IKTargetRef", 90 | "IMappinData", 91 | "ImpactPointData", 92 | "InnerItemData", 93 | "InputHintGroupData", 94 | "InputTriggerState", 95 | "InteractionChoiceCaption", 96 | "InteractionChoiceData", 97 | "InteractionChoiceHubData", 98 | "InteractionChoiceMetaData", 99 | "InteractionLayerData", 100 | "InventoryItemAbility", 101 | "InventoryItemSortData", 102 | "JournalFactNameValue", 103 | "JournalRequestStateFilter", 104 | "KillInfo", 105 | "LevelUpData", 106 | "LightPreset", 107 | "ListChoiceData", 108 | "ListenerAction", 109 | "ListenerActionConsumer", 110 | "LocationInformation", 111 | "LookAtLimits", 112 | "LookAtPartRequest", 113 | "LookAtRef", 114 | "LootVisualiserControlWrapper", 115 | "MappinUIProfile", 116 | "MappinUIUtils", 117 | "MappinUtils", 118 | "Matrix", 119 | "MeasurementUtils", 120 | "MinigameProgramData", 121 | "MotionConstrainedTierDataParams", 122 | "MountingInfo", 123 | "MountingSlotId", 124 | "MovementParameters", 125 | "NPCstubData", 126 | "NarrationEvent", 127 | "NarrativePlateData", 128 | "NavigationFindPointResult", 129 | "NearestRoadFromPlayerInfo", 130 | "NewMappinID", 131 | "PhotoModeOptionGridButtonData", 132 | "PinInfo", 133 | "PlayerBioMonitor", 134 | "PrereqCheckData", 135 | "PrereqData", 136 | "PrereqParams", 137 | "PreventionSystemDebugData", 138 | "Quaternion", 139 | "RWLock", 140 | "Range", 141 | "RectF", 142 | "RegisterCooldownFromRecordRequest", 143 | "RegisterNewCooldownRequest", 144 | "RemoteControlDrivingUIData", 145 | "RestrictMovementArea", 146 | "SDOSink", 147 | "SEquipArea", 148 | "SEquipSlot", 149 | "SEquipmentSet", 150 | "SItemInfo", 151 | "SItemStack", 152 | "SItemStackRequirementData", 153 | "SLastUsedWeapon", 154 | "SLoadout", 155 | "SPartSlots", 156 | "SSlotActiveItems", 157 | "SSlotInfo", 158 | "SSlotVisualInfo", 159 | "SVisualTagProcessing", 160 | "ScanningTooltipElementData", 161 | "ScanningTooltipElementDef", 162 | "ScriptExecutionContext", 163 | "SecureFootingResult", 164 | "SlotWeaponData", 165 | "SnapshotResult", 166 | "Sphere", 167 | "SquadOrder", 168 | "StatViewData", 169 | "StateMachineIdentifier", 170 | "StateMachineInstanceData", 171 | "StatusEffectTDBPicker", 172 | "StimuliMergeInfo", 173 | "TDBID", 174 | "TS_TargetPartInfo", 175 | "TacticRatio", 176 | "TargetFilterTicket", 177 | "TargetSearchFilter", 178 | "TelemetryDamageDealt", 179 | "TelemetryEnemy", 180 | "TelemetryEnemyDown", 181 | "TelemetryInventoryItem", 182 | "TelemetryLevelGained", 183 | "TelemetryQuickHack", 184 | "TelemetrySourceEntity", 185 | "TraceResult", 186 | "Transform", 187 | "TrialHelper", 188 | "TutorialBracketData", 189 | "UIScreenDefinition", 190 | "UnlockableProgram", 191 | "Vector2", 192 | "Vector3", 193 | "Vector4", 194 | "VendorData", 195 | "VideoWidgetSummary", 196 | "VisionBlockerTypeFlags", 197 | "VisualizersInfo", 198 | "WeakspotPhysicalDestructionComponent", 199 | "WeakspotPhysicalDestructionProperties", 200 | "WeaponRosterInfo", 201 | "WidgetUtils", 202 | "WorkEntryId", 203 | "WrappedEntIDArray", 204 | "bbUIInteractionData", 205 | "gameGrenadeThrowQueryParams", 206 | "gamePendingSubtitles", 207 | "gameSaveLock", 208 | "gameStatDetailedData", 209 | "gameSuggestedDefenseValues", 210 | "gameVisionModeSystemRevealIdentifier", 211 | "gameinteractionsActiveLayerData", 212 | "gamemappinsSenseCone", 213 | "gameprojectileLaunchParams", 214 | "gameuiDetectionParams", 215 | "gameuiDriverCombatCrosshairReticleData", 216 | "gameuiGenericNotificationData", 217 | "gameuiMountedWeaponTarget", 218 | "gameuiPatchIntroPackage", 219 | "gameuiSwitchPair", 220 | "gameuiWeaponShootParams", 221 | "inkInputKeyData", 222 | "inkMargin", 223 | "inkScreenProjectionData", 224 | "inkWidgetLibraryReference", 225 | "questPaymentConditionData", 226 | "scnDialogDisplayString", 227 | "scnDialogLineData", 228 | "smartGunUISightParameters", 229 | "smartGunUITargetParameters", 230 | "vehicleUnmountPosition", 231 | "worldTrafficLaneRef", 232 | ]; 233 | -------------------------------------------------------------------------------- /crates/compiler/backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod assemble; 2 | mod inputs; 3 | mod known_types; 4 | mod monomorph; 5 | 6 | pub use assemble::AssembleError; 7 | pub use inputs::{CompilationInputs, Error as PoolError, PoolMappings}; 8 | pub use monomorph::Monomorphizer; 9 | 10 | type IndexMap = indexmap::IndexMap; 11 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/array-literal.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let simple = [1, 2, 3]; 4 | let nested = [[1, 2], [3, 4]]; 5 | } 6 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/break.reds: -------------------------------------------------------------------------------- 1 | 2 | native func Log(str: String) 3 | native func OperatorAssignAdd(out l: Int32, r: Int32) -> Int32 4 | native func OperatorLess(l: Int32, r: Int32) -> Bool 5 | native func OperatorEqual(l: Int32, r: Int32) -> Bool 6 | 7 | func Test() { 8 | for i in [0, 1] { 9 | if i == 0 { 10 | break; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/cast.reds: -------------------------------------------------------------------------------- 1 | 2 | native func Cast(i: Int32) -> Float 3 | native func Cast(i: Int32) -> String 4 | 5 | func Test() { 6 | let _: Float = Cast(1); 7 | let _ = Cast(2); 8 | } 9 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/continue.reds: -------------------------------------------------------------------------------- 1 | 2 | native func Log(str: String) 3 | native func OperatorAssignAdd(out l: Int32, r: Int32) -> Int32 4 | native func OperatorLess(l: Int32, r: Int32) -> Bool 5 | native func OperatorEqual(l: Int32, r: Int32) -> Bool 6 | 7 | func Test() { 8 | for i in [0, 1] { 9 | if i == 0 { 10 | continue; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/dyn-cast.reds: -------------------------------------------------------------------------------- 1 | 2 | class Base {} 3 | 4 | class Class extends Base {} 5 | 6 | func Test() { 7 | let x: Base = new Class(); 8 | x as Class; 9 | let y: wref = x; 10 | y as Class; 11 | } 12 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/enum.reds: -------------------------------------------------------------------------------- 1 | 2 | enum Direction { 3 | Left = 0, 4 | Right = 1, 5 | } 6 | 7 | func Testing(dir: Direction) -> Int32 { 8 | switch dir { 9 | case Direction.Left: 10 | return EnumInt(dir); 11 | case Direction.Right: 12 | return EnumInt(dir); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/excluded.reds: -------------------------------------------------------------------------------- 1 | 2 | @if(ModuleExists("NonExisting")) 3 | func ExcludedFunc1() {} 4 | 5 | @if(false && true) 6 | func ExcludedFunc2() {} 7 | 8 | @if(true) 9 | func IncludedFunc() {} 10 | 11 | @if(false) 12 | @if(true) 13 | class ExcludedClass { 14 | func ExcludedMethod() {} 15 | } 16 | 17 | @if(true) 18 | class IncludedClass { 19 | func IncludedMethod() {} 20 | 21 | @if(false) 22 | func ExcludedMethod() {} 23 | } 24 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/for-loop.reds: -------------------------------------------------------------------------------- 1 | 2 | native func Log(str: String) 3 | native func OperatorAssignAdd(out l: Int32, r: Int32) -> Int32 4 | native func OperatorLess(l: Int32, r: Int32) -> Bool 5 | 6 | func Test() { 7 | for i in [0, 1] { 8 | Log(ToString(i)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/generic-struct.reds: -------------------------------------------------------------------------------- 1 | 2 | struct Tuple { 3 | let a: A; 4 | let b: B; 5 | 6 | static func Swap(self: Tuple) -> Tuple = 7 | new Tuple(self.b, self.a) 8 | } 9 | 10 | func Test() { 11 | let a = new Tuple(1, "a"); 12 | let b = new Tuple("b", 2); 13 | a = Tuple.Swap(b); 14 | } 15 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/if-else.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test(bool: Bool) -> Int32 { 3 | if bool { 4 | return 1; 5 | } else { 6 | return 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/implicit-convert.reds: -------------------------------------------------------------------------------- 1 | 2 | native func TakeScriptRef(str: script_ref) 3 | native func TakeRef(instance: Class) 4 | 5 | class Class { 6 | let field: String; 7 | func Method() {} 8 | } 9 | 10 | func Test() { 11 | TakeScriptRef("test"); 12 | 13 | let a: wref = new Class(); 14 | TakeRef(a); 15 | 16 | a.Method(); 17 | a.field; 18 | } 19 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/lambda.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let a = 1; 4 | let f = (b: Int32) -> (c: Int32) -> a + b + c; 5 | } 6 | 7 | native func OperatorAdd(a: Int32, b: Int32) -> Int32 8 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/local-init.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let _: Struct; 4 | let _: Class; 5 | let _: wref; 6 | let _: TestEnum; 7 | let _: String; 8 | let _: CName; 9 | let _: TweakDBID; 10 | let _: array; 11 | let _: Int8; 12 | let _: Int16; 13 | let _: Int32; 14 | let _: Int64; 15 | let _: Uint8; 16 | let _: Uint16; 17 | let _: Uint32; 18 | let _: Uint64; 19 | let _: Float; 20 | let _: Double; 21 | } 22 | 23 | struct Struct { 24 | let a: Int32; 25 | let b: Int32; 26 | } 27 | 28 | class Class {} 29 | 30 | enum TestEnum { 31 | Member = 0 32 | } 33 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/name-of.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let _ = NameOf(); 4 | // legacy syntax 5 | let _ = NameOf(Int32); 6 | Generic(); 7 | } 8 | 9 | func Generic() { 10 | let _ = NameOf(); 11 | } 12 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/named-impl.reds: -------------------------------------------------------------------------------- 1 | module CurveData 2 | 3 | @nameImplementation(CurveDataFloat as CurveData) 4 | native struct CurveData { 5 | native static func Create() -> CurveData 6 | } 7 | 8 | func Test() { 9 | let _: CurveData = CurveData.Create(); 10 | } 11 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/nested-for-loop.reds: -------------------------------------------------------------------------------- 1 | 2 | native func Log(str: String) 3 | native func OperatorAssignAdd(out l: Int32, r: Int32) -> Int32 4 | native func OperatorAdd(l: Int32, r: Int32) -> Int32 5 | native func OperatorLess(l: Int32, r: Int32) -> Bool 6 | 7 | func Test() { 8 | for i in [0, 1] { 9 | for j in [0, 1] { 10 | Log(ToString(i + j)); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/number-literal.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let a: Float = 1; 4 | a = 2.0; 5 | 6 | let b: Double = 3; 7 | b = 4.0; 8 | b = 5.0d; 9 | 10 | let _: Int8 = 6; 11 | let _: Int16 = 7; 12 | let _: Int32 = 8; 13 | 14 | let c: Int64 = 7; 15 | c = 8l; 16 | 17 | let _: Uint8 = 9; 18 | let _: Uint16 = 10; 19 | 20 | let d: Uint32 = 11; 21 | d = 12u; 22 | 23 | let e: Uint64 = 13; 24 | e = 14ul; 25 | } 26 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/return-void.reds: -------------------------------------------------------------------------------- 1 | 2 | func ReturnVoid() { 3 | return; 4 | } 5 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/static-arrays.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let static_arr: [Int32; 10]; 4 | static_arr[0] = 1; 5 | } 6 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/string-interp.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test(year: Int32) -> String { 3 | let birthYear = 1990; 4 | let name = "John"; 5 | return s"My name is \(name) and I am \(year - birthYear) years old"; 6 | } 7 | 8 | native func OperatorAdd(a: script_ref, b: script_ref) -> String 9 | native func OperatorSubtract(a: Int32, b: Int32) -> Int32 10 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/string-literal.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | let _: String = "\u{1F4A9}"; 4 | let _: CName = n"back"; 5 | let _: ResRef = r"base\\gameplay\\gui\\common\\buttonhints.inkwidget"; 6 | let _: TweakDBID = t"MappinIcons.QuestMappin"; 7 | } 8 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/struct.reds: -------------------------------------------------------------------------------- 1 | 2 | struct Struct { 3 | let x: Int32; 4 | let y: String; 5 | } 6 | 7 | func Test() { 8 | let a = new Struct(10, ""); 9 | } 10 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/super.reds: -------------------------------------------------------------------------------- 1 | 2 | class Base { 3 | func Testing() -> Int32 = 0 4 | } 5 | 6 | class Class extends Base { 7 | func Testing() -> Int32 = super.Testing() 8 | } 9 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/switch.reds: -------------------------------------------------------------------------------- 1 | 2 | native func OperatorModulo(l: Int32, r: Int32) -> Int32 3 | 4 | func Test(val: Int32) -> Bool { 5 | switch val % 4 { 6 | case -1: 7 | case 1: 8 | case 2: 9 | case 3: 10 | break; 11 | default: 12 | return true; 13 | } 14 | return false; 15 | } 16 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/ternary.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test1(val: Int32) -> Bool = val % 2 == 0 ? true : false 3 | func Test2(val: wref) -> wref = IsDefined(val) ? val : null 4 | 5 | class Dummy {} 6 | 7 | native func OperatorModulo(l: Int32, r: Int32) -> Int32 8 | native func OperatorEqual(l: Int32, r: Int32) -> Bool 9 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/data/variant.reds: -------------------------------------------------------------------------------- 1 | 2 | native func TakesVariant(var: Variant) 3 | 4 | func Test() { 5 | let x = ToVariant(1); 6 | let _ = IsDefined(x); 7 | let y = FromVariant(x); 8 | let _ = VariantTypeName(x); 9 | 10 | TakesVariant(2); 11 | } 12 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@array-literal.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/array-literal.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | array.resize 0 9 | local 0 10 | u64.const 3 11 | assign 12 | array.element 0 13 | local 0 14 | u64.const 0 15 | i32.one 16 | assign 17 | array.element 0 18 | local 0 19 | u64.const 1 20 | i32.const 2 21 | assign 22 | array.element 0 23 | local 0 24 | u64.const 2 25 | i32.const 3 26 | assign 27 | local 1 28 | local 0 29 | array.resize 0 30 | local 2 31 | u64.const 2 32 | assign 33 | array.element 0 34 | local 2 35 | u64.const 0 36 | i32.one 37 | assign 38 | array.element 0 39 | local 2 40 | u64.const 1 41 | i32.const 2 42 | array.resize 0 43 | local 3 44 | u64.const 2 45 | assign 46 | array.element 0 47 | local 3 48 | u64.const 0 49 | i32.const 3 50 | assign 51 | array.element 0 52 | local 3 53 | u64.const 1 54 | i32.const 4 55 | array.resize 1 56 | local 4 57 | u64.const 2 58 | assign 59 | array.element 1 60 | local 4 61 | u64.const 0 62 | local 2 63 | assign 64 | array.element 1 65 | local 4 66 | u64.const 1 67 | local 3 68 | assign 69 | local 5 70 | local 4 71 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@break.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/break.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | array.resize 0 9 | local 0 10 | u64.const 2 11 | assign 12 | array.element 0 13 | local 0 14 | u64.const 0 15 | i32.zero 16 | assign 17 | array.element 0 18 | local 0 19 | u64.const 1 20 | i32.one 21 | assign 22 | local 1 23 | local 0 24 | assign 25 | local 2 26 | i32.zero 27 | jump.if_not 144 28 | invoke.static j43 l8 f1 0000000000000000 29 | local 2 30 | array.size 0 31 | local 1 32 | param.end 33 | assign 34 | local 3 35 | array.element 0 36 | local 1 37 | local 2 38 | invoke.static j26 l8 f2 0000000000000000 39 | local 2 40 | i32.one 41 | param.end 42 | jump.if_not 32 43 | invoke.static j26 l9 f3 0000000000000000 44 | local 3 45 | i32.zero 46 | param.end 47 | jump 6 48 | jump 0 49 | jump -147 50 | 51 | // OperatorLess (f1) 52 | 53 | // OperatorAssignAdd (f2) 54 | 55 | // OperatorEqual (f3) 56 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@cast.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/cast.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | invoke.static j17 l6 f1 0000000000000000 11 | i32.one 12 | param.end 13 | assign 14 | local 1 15 | invoke.static j21 l7 f2 0000000000000000 16 | i32.const 2 17 | param.end 18 | 19 | // Cast (f1) 20 | 21 | // Cast (f2) 22 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@continue.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/continue.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | array.resize 0 9 | local 0 10 | u64.const 2 11 | assign 12 | array.element 0 13 | local 0 14 | u64.const 0 15 | i32.zero 16 | assign 17 | array.element 0 18 | local 0 19 | u64.const 1 20 | i32.one 21 | assign 22 | local 1 23 | local 0 24 | assign 25 | local 2 26 | i32.zero 27 | jump.if_not 144 28 | invoke.static j43 l8 f1 0000000000000000 29 | local 2 30 | array.size 0 31 | local 1 32 | param.end 33 | assign 34 | local 3 35 | array.element 0 36 | local 1 37 | local 2 38 | invoke.static j26 l8 f2 0000000000000000 39 | local 2 40 | i32.one 41 | param.end 42 | jump.if_not 32 43 | invoke.static j26 l9 f3 0000000000000000 44 | local 3 45 | i32.zero 46 | param.end 47 | jump -141 48 | jump 0 49 | jump -147 50 | 51 | // OperatorLess (f1) 52 | 53 | // OperatorAssignAdd (f2) 54 | 55 | // OperatorEqual (f3) 56 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@dyn-cast.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/dyn-cast.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | object.new 0 11 | ref.dyncast 0 12 | local 0 13 | assign 14 | local 1 15 | ref.to_wref 16 | local 0 17 | wref.dyncast 0 18 | local 1 19 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@enum.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/enum.reds 5 | snapshot_kind: text 6 | --- 7 | // Testing;Direction (f0) 8 | switch 10 9 | param 0 10 | switch.label 39 17 11 | enum.const 0 0 12 | return 13 | enum.to_int 0 4 14 | param 0 15 | switch.label 39 17 16 | enum.const 0 1 17 | return 18 | enum.to_int 0 4 19 | param 0 20 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@excluded.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/excluded.reds 5 | snapshot_kind: text 6 | --- 7 | // IncludedClass::IncludedMethod; (f0) 8 | 9 | // IncludedFunc; (f1) 10 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@for-loop.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/for-loop.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | array.resize 0 9 | local 0 10 | u64.const 2 11 | assign 12 | array.element 0 13 | local 0 14 | u64.const 0 15 | i32.zero 16 | assign 17 | array.element 0 18 | local 0 19 | u64.const 1 20 | i32.one 21 | assign 22 | local 1 23 | local 0 24 | assign 25 | local 2 26 | i32.zero 27 | jump.if_not 143 28 | invoke.static j43 l7 f1 0000000000000000 29 | local 2 30 | array.size 0 31 | local 1 32 | param.end 33 | assign 34 | local 3 35 | array.element 0 36 | local 1 37 | local 2 38 | invoke.static j26 l7 f2 0000000000000000 39 | local 2 40 | i32.one 41 | param.end 42 | invoke.static j34 l8 f3 0000000000000000 43 | to_string 1 44 | local 3 45 | param.end 46 | jump -146 47 | 48 | // OperatorLess (f1) 49 | 50 | // OperatorAssignAdd (f2) 51 | 52 | // Log (f3) 53 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@generic-struct.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/generic-struct.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | struct.new 2 0 11 | i32.one 12 | string.const 0 13 | assign 14 | local 1 15 | struct.new 2 1 16 | string.const 1 17 | i32.const 2 18 | assign 19 | local 0 20 | invoke.static j25 l13 f1 0000000000000000 21 | local 1 22 | param.end 23 | 24 | // Tuple::Swap;Tuple (f1) 25 | return 26 | struct.new 2 0 27 | struct.field 0 28 | param 0 29 | struct.field 1 30 | param 0 31 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@if-else.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/if-else.reds 5 | snapshot_kind: text 6 | --- 7 | // Test;Bool (f0) 8 | jump.if_not 14 9 | param 0 10 | return 11 | i32.one 12 | jump 2 13 | return 14 | i32.zero 15 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@implicit-convert.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/implicit-convert.reds 5 | snapshot_kind: text 6 | --- 7 | // Class::Method; (f0) 8 | 9 | // Test; (f1) 10 | invoke.static j30 l11 f2 0000000000000001 11 | as_ref 0 12 | string.const 0 13 | param.end 14 | assign 15 | local 0 16 | ref.to_wref 17 | object.new 0 18 | invoke.static j26 l14 f3 0000000000000000 19 | wref.to_ref 20 | local 0 21 | param.end 22 | ctx 26 23 | wref.to_ref 24 | local 0 25 | invoke.virtual j16 l16 f0 0000000000000000 26 | param.end 27 | ctx 19 28 | wref.to_ref 29 | local 0 30 | object.field 0 31 | 32 | // TakeScriptRef (f2) 33 | 34 | // TakeRef (f3) 35 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@lambda.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/lambda.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | i32.one 11 | assign 12 | local 1 13 | invoke.static j25 l0 f1 0000000000000000 14 | local 0 15 | param.end 16 | 17 | // Function1::Call;Int32 (f2) 18 | 19 | // Function1>::Call;Int32 (f3) 20 | 21 | // lambda$13::Instantiate (f1) 22 | assign 23 | local 2 24 | object.new 0 25 | assign 26 | ctx 15 27 | local 2 28 | object.field 0 29 | param 0 30 | return 31 | local 2 32 | 33 | // lambda$13::Call;Int32 (f4) 34 | return 35 | invoke.static j34 l0 f5 0000000000000000 36 | object.field 0 37 | param 1 38 | param.end 39 | 40 | // lambda$18::Instantiate (f5) 41 | assign 42 | local 3 43 | object.new 1 44 | assign 45 | ctx 15 46 | local 3 47 | object.field 1 48 | param 2 49 | assign 50 | ctx 15 51 | local 3 52 | object.field 2 53 | param 3 54 | return 55 | local 3 56 | 57 | // lambda$18::Call;Int32 (f6) 58 | return 59 | invoke.static j59 l4 f7 0000000000000000 60 | invoke.static j34 l4 f7 0000000000000000 61 | object.field 1 62 | object.field 2 63 | param.end 64 | param 4 65 | param.end 66 | 67 | // OperatorAdd (f7) 68 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@local-init.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/local-init.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | struct.new 0 0 11 | assign 12 | local 1 13 | ref.null 14 | assign 15 | local 2 16 | wref.null 17 | assign 18 | local 3 19 | enum.from_int 0 1 20 | i8.const 0 21 | assign 22 | local 4 23 | string.const 0 24 | assign 25 | local 5 26 | cname.const 0 27 | assign 28 | local 6 29 | tweakdb.const 0 30 | array.clear 1 31 | local 7 32 | assign 33 | local 8 34 | i8.const 0 35 | assign 36 | local 9 37 | i16.const 0 38 | assign 39 | local 10 40 | i32.zero 41 | assign 42 | local 11 43 | i64.const 0 44 | assign 45 | local 12 46 | u8.const 0 47 | assign 48 | local 13 49 | u16.const 0 50 | assign 51 | local 14 52 | u32.const 0 53 | assign 54 | local 15 55 | u64.const 0 56 | assign 57 | local 16 58 | f32.const 0 59 | assign 60 | local 17 61 | f64.const 0 62 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@name-of.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/name-of.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | cname.const 0 11 | assign 12 | local 1 13 | cname.const 0 14 | invoke.static j16 l6 f1 0000000000000000 15 | param.end 16 | 17 | // Generic; (f1) 18 | assign 19 | local 2 20 | cname.const 0 21 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@named-impl.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/named-impl.reds 5 | snapshot_kind: text 6 | --- 7 | // CurveData.Test; (f0) 8 | assign 9 | local 0 10 | invoke.static j16 l9 f1 0000000000000000 11 | param.end 12 | 13 | // CurveData.CurveDataFloat::Create (f1) 14 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@nested-for-loop.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/nested-for-loop.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | array.resize 0 9 | local 0 10 | u64.const 2 11 | assign 12 | array.element 0 13 | local 0 14 | u64.const 0 15 | i32.zero 16 | assign 17 | array.element 0 18 | local 0 19 | u64.const 1 20 | i32.one 21 | assign 22 | local 1 23 | local 0 24 | assign 25 | local 2 26 | i32.zero 27 | jump.if_not 395 28 | invoke.static j43 l8 f1 0000000000000000 29 | local 2 30 | array.size 0 31 | local 1 32 | param.end 33 | assign 34 | local 3 35 | array.element 0 36 | local 1 37 | local 2 38 | invoke.static j26 l8 f2 0000000000000000 39 | local 2 40 | i32.one 41 | param.end 42 | array.resize 0 43 | local 4 44 | u64.const 2 45 | assign 46 | array.element 0 47 | local 4 48 | u64.const 0 49 | i32.zero 50 | assign 51 | array.element 0 52 | local 4 53 | u64.const 1 54 | i32.one 55 | assign 56 | local 5 57 | local 4 58 | assign 59 | local 6 60 | i32.zero 61 | jump.if_not 168 62 | invoke.static j43 l9 f1 0000000000000000 63 | local 6 64 | array.size 0 65 | local 5 66 | param.end 67 | assign 68 | local 7 69 | array.element 0 70 | local 5 71 | local 6 72 | invoke.static j26 l9 f2 0000000000000000 73 | local 6 74 | i32.one 75 | param.end 76 | invoke.static j59 l10 f3 0000000000000000 77 | to_string 1 78 | invoke.static j34 l10 f4 0000000000000000 79 | local 3 80 | local 7 81 | param.end 82 | param.end 83 | jump -171 84 | jump -398 85 | 86 | // OperatorLess (f1) 87 | 88 | // OperatorAssignAdd (f2) 89 | 90 | // Log (f3) 91 | 92 | // OperatorAdd (f4) 93 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@number-literal.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/number-literal.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | f32.const 1 11 | assign 12 | local 0 13 | f32.const 2 14 | assign 15 | local 1 16 | f64.const 3 17 | assign 18 | local 1 19 | f64.const 4 20 | assign 21 | local 1 22 | f64.const 5 23 | assign 24 | local 2 25 | i8.const 6 26 | assign 27 | local 3 28 | i16.const 7 29 | assign 30 | local 4 31 | i32.const 8 32 | assign 33 | local 5 34 | i64.const 7 35 | assign 36 | local 5 37 | i64.const 8 38 | assign 39 | local 6 40 | u8.const 9 41 | assign 42 | local 7 43 | u16.const 10 44 | assign 45 | local 8 46 | u32.const 11 47 | assign 48 | local 8 49 | u32.const 12 50 | assign 51 | local 9 52 | u64.const 13 53 | assign 54 | local 9 55 | u64.const 14 56 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@return-void.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/return-void.reds 5 | snapshot_kind: text 6 | --- 7 | // ReturnVoid; (f0) 8 | return 9 | nop 10 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@static-arrays.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/static-arrays.reds 5 | --- 6 | // Test; (f0) 7 | assign 8 | local 0 9 | assign 10 | static_array.element 0 11 | local 0 12 | i32.zero 13 | i32.one 14 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@string-interp.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/string-interp.reds 5 | snapshot_kind: text 6 | --- 7 | // Test;Int32 (f0) 8 | assign 9 | local 0 10 | i32.const 1990 11 | assign 12 | local 1 13 | string.const 0 14 | return 15 | invoke.static j212 l5 f1 0000000000000011 16 | as_ref 0 17 | invoke.static j173 l5 f1 0000000000000011 18 | as_ref 0 19 | invoke.static j96 l5 f1 0000000000000011 20 | as_ref 0 21 | invoke.static j57 l5 f1 0000000000000011 22 | as_ref 0 23 | string.const 1 24 | as_ref 0 25 | to_string 0 26 | local 1 27 | param.end 28 | as_ref 0 29 | string.const 2 30 | param.end 31 | as_ref 0 32 | to_string 1 33 | invoke.static j34 l5 f2 0000000000000000 34 | param 0 35 | local 0 36 | param.end 37 | param.end 38 | as_ref 0 39 | string.const 3 40 | param.end 41 | 42 | // OperatorAdd (f1) 43 | 44 | // OperatorSubtract (f2) 45 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@string-literal.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/string-literal.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | string.const 0 11 | assign 12 | local 1 13 | cname.const 0 14 | assign 15 | local 2 16 | resource.const 0 17 | assign 18 | local 3 19 | tweakdb.const 0 20 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@struct.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/struct.reds 5 | snapshot_kind: text 6 | --- 7 | // Test; (f0) 8 | assign 9 | local 0 10 | struct.new 2 0 11 | i32.const 10 12 | string.const 0 13 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@super.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/super.reds 5 | snapshot_kind: text 6 | --- 7 | // Base::Testing; (f0) 8 | return 9 | i32.zero 10 | 11 | // Class::Testing; (f1) 12 | return 13 | ctx 17 14 | this 15 | invoke.static j16 l7 f0 0000000000000000 16 | param.end 17 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@switch.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/switch.reds 5 | --- 6 | // Test;Int32 (f0) 7 | switch 7 8 | invoke.static j30 l5 f1 0000000000000000 9 | param 0 10 | i32.const 4 11 | param.end 12 | switch.label 7 31 13 | i32.const -1 14 | switch.label 3 21 15 | i32.one 16 | switch.label 7 15 17 | i32.const 2 18 | switch.label 10 5 19 | i32.const 3 20 | jump 3 21 | switch.default 22 | return 23 | true.const 24 | return 25 | false.const 26 | 27 | // OperatorModulo (f1) 28 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@ternary.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/ternary.reds 5 | --- 6 | // Test1;Int32 (f0) 7 | return 8 | cond 50 49 9 | invoke.static j47 l2 f1 0000000000000000 10 | invoke.static j30 l2 f2 0000000000000000 11 | param 0 12 | i32.const 2 13 | param.end 14 | i32.zero 15 | param.end 16 | true.const 17 | false.const 18 | 19 | // Test2;Dummy (f3) 20 | return 21 | cond 22 21 22 | ref.to_bool 23 | wref.to_ref 24 | param 1 25 | param 1 26 | wref.null 27 | 28 | // OperatorEqual (f1) 29 | 30 | // OperatorModulo (f2) 31 | -------------------------------------------------------------------------------- /crates/compiler/backend/tests/snapshots/bytecode__bytecode@variant.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/backend/tests/bytecode.rs 3 | expression: "BytecodePrinter::new(&bundle, bundle_len)" 4 | input_file: crates/compiler/backend/tests/data/variant.reds 5 | --- 6 | // Test; (f0) 7 | assign 8 | local 0 9 | variant.new 0 10 | i32.one 11 | assign 12 | local 1 13 | variant.is_defined 14 | local 0 15 | assign 16 | local 2 17 | variant.extract 0 18 | local 0 19 | assign 20 | local 3 21 | variant.type_name 22 | local 0 23 | invoke.static j30 l10 f1 0000000000000000 24 | variant.new 0 25 | i32.const 2 26 | param.end 27 | 28 | // TakesVariant (f1) 29 | -------------------------------------------------------------------------------- /crates/compiler/frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-compiler-frontend" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-ast.workspace = true 9 | redscript-parser.workspace = true 10 | 11 | thiserror.workspace = true 12 | derive-where.workspace = true 13 | paste.workspace = true 14 | bitfield-struct.workspace = true 15 | identity-hash.workspace = true 16 | slab.workspace = true 17 | smallvec.workspace = true 18 | hashbrown.workspace = true 19 | indexmap.workspace = true 20 | elsa.workspace = true 21 | sequence_trie.workspace = true 22 | 23 | [dev-dependencies] 24 | redscript-compiler-api.workspace = true 25 | 26 | insta.workspace = true 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/cte.rs: -------------------------------------------------------------------------------- 1 | use hashbrown::HashSet; 2 | use redscript_ast::{self as ast, Span, Spanned}; 3 | use thiserror::Error; 4 | 5 | #[derive(Debug)] 6 | pub struct Evaluator<'a> { 7 | modules: HashSet>, 8 | } 9 | 10 | impl<'a> Evaluator<'a> { 11 | pub fn new(modules: HashSet>) -> Self { 12 | Self { modules } 13 | } 14 | 15 | pub fn from_modules<'b>(modules: impl IntoIterator>) -> Self 16 | where 17 | 'a: 'b, 18 | { 19 | let modules = modules 20 | .into_iter() 21 | .filter_map(|m| m.path.as_ref()) 22 | .map(|path| path.segments.clone()) 23 | .collect::>(); 24 | Self { modules } 25 | } 26 | 27 | pub fn eval(&self, (expr, span): &Spanned>) -> Result { 28 | let res = match expr { 29 | ast::Expr::Constant(ast::Constant::Bool(b)) => Value::Bool(*b), 30 | ast::Expr::BinOp { lhs, op, rhs } => match (self.eval(lhs)?, self.eval(rhs)?) { 31 | (Value::Bool(lhs), Value::Bool(rhs)) => match op { 32 | ast::BinOp::And => Value::Bool(lhs && rhs), 33 | ast::BinOp::Or => Value::Bool(lhs || rhs), 34 | _ => return Err(Error::UnsupportedOperation(*span)), 35 | }, 36 | }, 37 | ast::Expr::UnOp { op, expr } => match (op, self.eval(expr)?) { 38 | (ast::UnOp::Not, Value::Bool(b)) => Value::Bool(!b), 39 | _ => return Err(Error::UnsupportedOperation(*span)), 40 | }, 41 | ast::Expr::Call { 42 | expr, 43 | type_args, 44 | args, 45 | } => match (&**expr, &**type_args, &**args) { 46 | ( 47 | (ast::Expr::Ident("ModuleExists"), _), 48 | [], 49 | [(ast::Expr::Constant(ast::Constant::String(str)), _)], 50 | ) => { 51 | let path = str.split('.').collect::>(); 52 | Value::Bool(self.modules.contains(&path[..])) 53 | } 54 | _ => return Err(Error::UnsupportedOperation(*span)), 55 | }, 56 | _ => return Err(Error::UnsupportedOperation(*span)), 57 | }; 58 | Ok(res) 59 | } 60 | } 61 | 62 | pub enum Value { 63 | Bool(bool), 64 | } 65 | 66 | #[derive(Debug, Error)] 67 | pub enum Error { 68 | #[error("unsupported operation")] 69 | UnsupportedOperation(Span), 70 | } 71 | 72 | impl Error { 73 | pub fn span(&self) -> Span { 74 | match self { 75 | Self::UnsupportedOperation(span) => *span, 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/diagnostic/pass.rs: -------------------------------------------------------------------------------- 1 | use crate::{CompileErrorReporter, LoweredFunction}; 2 | 3 | mod unused_locals; 4 | 5 | pub use unused_locals::UnusedLocals; 6 | 7 | pub trait DiagnosticPass<'ctx> { 8 | fn run(&self, func: &LoweredFunction<'ctx>, reporter: &mut CompileErrorReporter<'ctx>); 9 | } 10 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/diagnostic/pass/unused_locals.rs: -------------------------------------------------------------------------------- 1 | use hashbrown::{HashMap, HashSet}; 2 | use redscript_ast::Span; 3 | 4 | use super::DiagnosticPass; 5 | use crate::visitor::Visitor; 6 | use crate::{CompileErrorReporter, Diagnostic, LoweredFunction, ir}; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct UnusedLocals; 10 | 11 | impl<'ctx> DiagnosticPass<'ctx> for UnusedLocals { 12 | fn run(&self, func: &LoweredFunction<'ctx>, reporter: &mut CompileErrorReporter<'ctx>) { 13 | let mut visitor = UnusedLocalVisitor::new(func); 14 | visitor.visit_block(&func.block); 15 | 16 | for (_, span) in visitor.unused_locals() { 17 | reporter.report(Diagnostic::UnusedLocal(span)); 18 | } 19 | } 20 | } 21 | 22 | #[derive(Debug, Default)] 23 | struct UnusedLocalVisitor { 24 | spans: HashMap, 25 | used: HashSet, 26 | } 27 | 28 | impl UnusedLocalVisitor { 29 | fn new(func: &LoweredFunction<'_>) -> Self { 30 | let mut this = Self::default(); 31 | for local in &func.locals { 32 | this.register_local(local); 33 | } 34 | this 35 | } 36 | 37 | fn register_local(&mut self, local: &ir::LocalInfo<'_>) { 38 | if let Some(span) = local.span { 39 | self.spans.insert(local.id, span); 40 | } 41 | } 42 | 43 | fn unused_locals(&self) -> impl Iterator { 44 | self.spans 45 | .iter() 46 | .filter(|&(local, _)| !self.used.contains(local)) 47 | .map(|(&local, &span)| (local, span)) 48 | } 49 | } 50 | 51 | impl<'ctx> Visitor<'ctx> for UnusedLocalVisitor { 52 | fn visit_init_default(&mut self, _local: ir::Local, _typ: &ir::Type<'ctx>, _span: Span) {} 53 | 54 | fn visit_assign(&mut self, place: &ir::Expr<'ctx>, value: &ir::Expr<'ctx>, _span: Span) { 55 | match place { 56 | ir::Expr::Local(_, _) => {} 57 | other => self.visit_expr(other), 58 | } 59 | self.visit_expr(value); 60 | } 61 | 62 | fn visit_new_closure(&mut self, closure: &ir::Closure<'ctx>, _span: Span) { 63 | for local in &closure.locals { 64 | self.register_local(local); 65 | } 66 | } 67 | 68 | fn visit_local(&mut self, local: ir::Local, _span: Span) { 69 | self.used.insert(local); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod cte; 2 | mod diagnostic; 3 | pub mod ir; 4 | mod lower; 5 | mod modules; 6 | mod stages; 7 | mod symbols; 8 | pub mod types; 9 | pub mod utils; 10 | mod visitor; 11 | 12 | pub use cte::Evaluator; 13 | pub use diagnostic::{Diagnostic, Reporter, UnknownSource, pass}; 14 | pub use lower::{CoalesceError, Error as LowerError, PolyType, TypeRef}; 15 | pub use redscript_ast as ast; 16 | use redscript_parser as parser; 17 | pub use stages::{LoweredClass, LoweredCompilationUnit, LoweredFunction}; 18 | use stages::{NameResolution, Scope}; 19 | pub use symbols::{ 20 | Aggregate, AggregateFlags, Enum, Field, FieldFlags, FieldId, FieldIndex, FieldMap, 21 | FreeFunction, FreeFunctionFlags, FreeFunctionIndex, FunctionIndex, FunctionKind, FunctionMap, 22 | FunctionType, Method, MethodFlags, MethodId, MethodMap, Param, ParamFlags, QualifiedName, 23 | Symbols, TypeDef, TypeSchema, 24 | }; 25 | pub use types::{ 26 | CtxVar, Immutable, Mono, MonoType, RefType, Type, TypeApp, TypeId, TypeIndex, TypeInterner, 27 | TypeKind, Variance, predef, 28 | }; 29 | 30 | type IndexMap = indexmap::IndexMap; 31 | type IndexSet = indexmap::IndexSet; 32 | type FrozenIndexSet = elsa::FrozenIndexSet; 33 | 34 | pub type TypeScope<'scope, 'ctx> = IndexMap<&'ctx str, TypeRef<'scope, 'ctx>>; 35 | pub type CompileErrorReporter<'ctx> = Reporter>; 36 | type LowerReporter<'id> = Reporter>; 37 | 38 | pub fn infer_from_sources<'ctx>( 39 | sources: &'ctx ast::SourceMap, 40 | symbols: Symbols<'ctx>, 41 | reporter: &mut CompileErrorReporter<'ctx>, 42 | interner: &'ctx TypeInterner, 43 | ) -> (LoweredCompilationUnit<'ctx>, Symbols<'ctx>) { 44 | let mods = parse_files(sources, reporter); 45 | let evaluator = Evaluator::from_modules(&mods); 46 | let (unit, symbols) = process_sources(mods, symbols, evaluator, reporter, interner); 47 | (unit, symbols) 48 | } 49 | 50 | pub fn parse_files<'ctx>( 51 | sources: &'ctx ast::SourceMap, 52 | reporter: &mut CompileErrorReporter<'ctx>, 53 | ) -> Vec> { 54 | let (module, errs) = parser::parse_modules(sources.files().map(|(id, f)| (id, f.source()))); 55 | reporter.report_many(errs); 56 | module 57 | } 58 | 59 | pub fn parse_file<'ctx>( 60 | id: ast::FileId, 61 | file: &'ctx ast::File, 62 | reporter: &mut CompileErrorReporter<'ctx>, 63 | ) -> Option> { 64 | let (module, errs) = parser::parse_module(file.source(), id); 65 | reporter.report_many(errs); 66 | module 67 | } 68 | 69 | pub fn process_sources<'ctx>( 70 | modules: impl IntoIterator>, 71 | symbols: Symbols<'ctx>, 72 | evaluator: Evaluator<'ctx>, 73 | reporter: &mut CompileErrorReporter<'ctx>, 74 | interner: &'ctx TypeInterner, 75 | ) -> (LoweredCompilationUnit<'ctx>, Symbols<'ctx>) { 76 | let mut scope = Scope::new(&symbols); 77 | let mut resolution = NameResolution::new(modules, evaluator, symbols, reporter, interner); 78 | 79 | resolution.populate_globals(&mut scope); 80 | resolution.progress(&scope).finish(&scope, reporter) 81 | } 82 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/modules.rs: -------------------------------------------------------------------------------- 1 | use redscript_ast as ast; 2 | use sequence_trie::SequenceTrie; 3 | use smallvec::smallvec; 4 | 5 | use crate::lower::FreeFunctionIndexes; 6 | use crate::{FreeFunctionIndex, QualifiedName, TypeId}; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct ModuleMap<'ctx> { 10 | map: SequenceTrie<&'ctx str, Export<'ctx>, hashbrown::DefaultHashBuilder>, 11 | } 12 | 13 | impl<'ctx> ModuleMap<'ctx> { 14 | #[inline] 15 | pub fn add_type( 16 | &mut self, 17 | path: QualifiedName<'ctx>, 18 | id: TypeId<'ctx>, 19 | ) -> Result<(), NameRedefinition> { 20 | if self.map.insert_owned(&path, Export::Type(id)).is_some() { 21 | return Err(NameRedefinition); 22 | }; 23 | Ok(()) 24 | } 25 | 26 | pub fn add_function( 27 | &mut self, 28 | path: QualifiedName<'ctx>, 29 | index: FreeFunctionIndex, 30 | ) -> Result<(), NameRedefinition> { 31 | match self.map.get_mut(&path) { 32 | None => { 33 | self.map 34 | .insert_owned(&path, Export::FreeFunction(smallvec![index])); 35 | } 36 | Some(Export::FreeFunction(vec)) => vec.push(index), 37 | Some(_) => return Err(NameRedefinition), 38 | }; 39 | Ok(()) 40 | } 41 | 42 | pub fn exports( 43 | &self, 44 | path: impl AsRef<[&'ctx str]>, 45 | ) -> Option)>> { 46 | let node = self.map.get_node(path.as_ref())?; 47 | let it = node 48 | .children_with_keys() 49 | .filter_map(|(&k, n)| Some((k, n.value()?))); 50 | Some(it) 51 | } 52 | 53 | pub fn exists(&self, path: impl IntoIterator) -> bool { 54 | self.map.get_node(path).is_some() 55 | } 56 | 57 | pub fn visit_import( 58 | &self, 59 | import: &ast::Import<'ctx>, 60 | mut on_type: impl FnMut(&'ctx str, TypeId<'ctx>), 61 | mut on_function: impl FnMut(&'ctx str, FreeFunctionIndex), 62 | mut on_not_found: impl FnMut(&'ctx str), 63 | ) { 64 | let mut visit_export = |export: &Export<'ctx>, name: &'ctx str| match export { 65 | Export::Type(id) => on_type(name, *id), 66 | Export::FreeFunction(vec) => vec.iter().for_each(|&idx| on_function(name, idx)), 67 | }; 68 | 69 | match import { 70 | ast::Import::Exact(path) => { 71 | let last = path.as_ref().last().unwrap(); 72 | match self.map.get(path.as_ref()) { 73 | Some(export) => visit_export(export, last), 74 | None => on_not_found(last), 75 | } 76 | } 77 | ast::Import::Select(path, names) => { 78 | match self.map.get_node(path.as_ref()) { 79 | Some(node) => names.iter().for_each(|&name| match node.get([name]) { 80 | Some(export) => visit_export(export, name), 81 | None => on_not_found(name), 82 | }), 83 | None => on_not_found(path.as_ref().last().unwrap()), 84 | }; 85 | } 86 | ast::Import::All(path) => match self.exports(path.as_ref()) { 87 | Some(exports) => { 88 | let mut peekable = exports.peekable(); 89 | if peekable.peek().is_none() { 90 | on_not_found(path.as_ref().last().unwrap()); 91 | } else { 92 | peekable.for_each(|(name, export)| visit_export(export, name)); 93 | } 94 | } 95 | None => on_not_found(path.as_ref().last().unwrap()), 96 | }, 97 | }; 98 | } 99 | } 100 | 101 | #[derive(Debug)] 102 | pub enum Export<'ctx> { 103 | FreeFunction(FreeFunctionIndexes), 104 | Type(TypeId<'ctx>), 105 | } 106 | 107 | #[derive(Debug)] 108 | pub struct NameRedefinition; 109 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/stages.rs: -------------------------------------------------------------------------------- 1 | mod infer; 2 | mod resolution; 3 | 4 | pub use infer::{LoweredClass, LoweredCompilationUnit, LoweredFunction, TypeInference}; 5 | pub use resolution::{FunctionAnnotation, NameResolution, Scope}; 6 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod fmt; 2 | mod lazy; 3 | mod scoped_map; 4 | 5 | pub(crate) use lazy::Lazy; 6 | pub use scoped_map::ScopedMap; 7 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/utils/fmt.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::cell::RefCell; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct DisplayFn(F); 6 | 7 | impl DisplayFn { 8 | pub fn new(f: F) -> Self { 9 | Self(f) 10 | } 11 | } 12 | 13 | impl fmt::Display for DisplayFn 14 | where 15 | F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result, 16 | { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | self.0(f) 19 | } 20 | } 21 | 22 | pub fn lowercase(thing: &str) -> impl fmt::Display + Clone + use<'_> { 23 | DisplayFn::new(move |f: &mut fmt::Formatter<'_>| { 24 | thing 25 | .chars() 26 | .try_for_each(|c| write!(f, "{}", c.to_lowercase())) 27 | }) 28 | } 29 | 30 | pub fn sep_by(iter: I, sep: &str) -> impl fmt::Display + use<'_, T, I> 31 | where 32 | I: IntoIterator, 33 | I::Item: fmt::Display, 34 | { 35 | let iter = RefCell::new(iter.into_iter()); 36 | DisplayFn::new(move |f: &mut fmt::Formatter<'_>| { 37 | if let Some(first) = iter.borrow_mut().next() { 38 | write!(f, "{first}")?; 39 | } 40 | iter.borrow_mut() 41 | .try_for_each(|item| write!(f, "{sep}{item}"))?; 42 | Ok(()) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/utils/lazy.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::fmt; 3 | 4 | pub struct Lazy { 5 | get: F, 6 | value: RefCell>, 7 | } 8 | 9 | impl Lazy { 10 | #[inline] 11 | pub fn new(get: F) -> Self { 12 | Self { 13 | get, 14 | value: RefCell::new(None), 15 | } 16 | } 17 | 18 | #[inline] 19 | pub fn try_get(&self) -> Option 20 | where 21 | A: Clone, 22 | { 23 | self.value.borrow().clone() 24 | } 25 | 26 | pub fn get(&self, env: &E) -> Result 27 | where 28 | A: Clone, 29 | F: Fn(&E) -> A, 30 | { 31 | let mut borrow = match self.value.try_borrow_mut() { 32 | Ok(mut slot) => match &mut *slot { 33 | Some(var) => return Ok(var.clone()), 34 | None => slot, 35 | }, 36 | Err(_) => return Err(CycleError), 37 | }; 38 | 39 | let var = (self.get)(env); 40 | *borrow = Some(var.clone()); 41 | Ok(var) 42 | } 43 | } 44 | 45 | impl Lazy A>> { 46 | pub fn resolved(value: A) -> Self { 47 | Self { 48 | get: Box::new(move |_| unreachable!()), 49 | value: RefCell::new(Some(value)), 50 | } 51 | } 52 | } 53 | 54 | impl fmt::Debug for Lazy { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | f.debug_struct("Lazy") 57 | .field("value", &self.value.try_borrow()) 58 | .finish() 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | pub struct CycleError; 64 | -------------------------------------------------------------------------------- /crates/compiler/frontend/src/utils/scoped_map.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::hash::Hash; 3 | 4 | use crate::IndexMap; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum ScopedMap<'a, K, V> { 8 | Cons(IndexMap, &'a Self), 9 | Tail(IndexMap), 10 | } 11 | 12 | impl<'a, K, V> ScopedMap<'a, K, V> 13 | where 14 | K: Eq + Hash, 15 | { 16 | #[inline] 17 | pub fn get(&self, key: &Q) -> Option<&V> 18 | where 19 | K: Borrow, 20 | Q: ?Sized + Eq + Hash, 21 | { 22 | Some(self.get_with_depth(key)?.0) 23 | } 24 | 25 | #[inline] 26 | pub fn get_with_depth(&self, key: &Q) -> Option<(&V, u16)> 27 | where 28 | K: Borrow, 29 | Q: ?Sized + Eq + Hash, 30 | { 31 | self.scope_iter() 32 | .zip(0u16..) 33 | .find_map(|(map, depth)| Some((map.get(key)?, depth))) 34 | } 35 | 36 | #[inline] 37 | pub fn insert(&mut self, k: K, v: V) -> Option { 38 | self.top_mut().insert(k, v) 39 | } 40 | 41 | #[inline] 42 | pub fn introduce_scope(&'a self) -> Self { 43 | self.push_scope(IndexMap::default()) 44 | } 45 | 46 | #[inline] 47 | pub fn push_scope(&'a self, map: IndexMap) -> Self { 48 | Self::Cons(map, self) 49 | } 50 | 51 | #[inline] 52 | pub fn pop_scope(self) -> IndexMap { 53 | match self { 54 | Self::Cons(map, _) | Self::Tail(map) => map, 55 | } 56 | } 57 | 58 | #[inline] 59 | pub fn top(&self) -> &IndexMap { 60 | match self { 61 | Self::Cons(map, _) | Self::Tail(map) => map, 62 | } 63 | } 64 | 65 | #[inline] 66 | pub fn top_mut(&mut self) -> &mut IndexMap { 67 | match self { 68 | Self::Cons(map, _) | Self::Tail(map) => map, 69 | } 70 | } 71 | 72 | pub fn len(&self) -> usize { 73 | self.scope_iter().map(IndexMap::len).sum() 74 | } 75 | 76 | pub fn is_empty(&self) -> bool { 77 | self.len() == 0 78 | } 79 | 80 | #[inline] 81 | pub fn iter(&self) -> impl Iterator { 82 | self.scope_iter().flat_map(IndexMap::iter) 83 | } 84 | 85 | #[inline] 86 | pub fn scope_iter(&self) -> ScopeIter<'_, 'a, K, V> { 87 | ScopeIter { map: Some(self) } 88 | } 89 | 90 | pub fn is_top_level(&self) -> bool { 91 | matches!(self, ScopedMap::Tail(_)) 92 | } 93 | } 94 | 95 | impl Default for ScopedMap<'_, K, V> { 96 | #[inline] 97 | fn default() -> Self { 98 | Self::Tail(IndexMap::default()) 99 | } 100 | } 101 | 102 | impl Extend<(K, V)> for ScopedMap<'_, K, V> { 103 | fn extend>(&mut self, iter: T) { 104 | self.top_mut().extend(iter); 105 | } 106 | } 107 | 108 | impl FromIterator<(K, V)> for ScopedMap<'_, K, V> { 109 | #[inline] 110 | fn from_iter>(iter: T) -> Self { 111 | Self::Tail(iter.into_iter().collect()) 112 | } 113 | } 114 | 115 | impl From> for ScopedMap<'_, K, V> { 116 | fn from(map: IndexMap) -> Self { 117 | Self::Tail(map) 118 | } 119 | } 120 | 121 | #[derive(Debug)] 122 | pub struct ScopeIter<'a, 'b, K, V> { 123 | map: Option<&'a ScopedMap<'b, K, V>>, 124 | } 125 | 126 | impl<'a, K, V> Iterator for ScopeIter<'a, '_, K, V> { 127 | type Item = &'a IndexMap; 128 | 129 | #[inline] 130 | fn next(&mut self) -> Option { 131 | match self.map? { 132 | ScopedMap::Cons(head, tail) => { 133 | self.map = Some(tail); 134 | Some(head) 135 | } 136 | ScopedMap::Tail(map) => { 137 | self.map = None; 138 | Some(map) 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/annotation-checks.reds: -------------------------------------------------------------------------------- 1 | 2 | class UserClass { 3 | func HelloWorld(x: String) {} 4 | } 5 | 6 | @wrapMethod(UserClass) 7 | func HelloWorld(x: String) { 8 | } 9 | 10 | @wrapMethod(NonExistingClass) 11 | func HelloWorld(x: String) { 12 | } 13 | 14 | @addMethod(IScriptable) 15 | func ForbiddenGenericAnnotatedMethod(a: A) { 16 | } 17 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/class-checks.reds: -------------------------------------------------------------------------------- 1 | 2 | struct Struct {} 3 | 4 | class ClassExtendingStruct extends Struct {} 5 | 6 | abstract class AbstractClass { 7 | func UnimplementedMethod(a: A) 8 | final func FinalMethod(a: A) {} 9 | } 10 | 11 | class IncompleteClass extends AbstractClass {} 12 | 13 | class CompleteClass extends AbstractClass { 14 | func UnimplementedMethod(a: Int32) {} 15 | } 16 | 17 | class ClassExtendingPrimitive extends Int32 {} 18 | 19 | @badAnnotation() 20 | class ClassWithUnknownAnnotation {} 21 | 22 | class ClassDefinedTwice {} 23 | class ClassDefinedTwice {} 24 | 25 | class ClassWithDuplicateMethod { 26 | func Method(x: String) {} 27 | func Method(x: String) {} 28 | } 29 | 30 | class ClassOverridingFinalMethod extends AbstractClass { 31 | func UnimplementedMethod(a: Int32) {} 32 | final func FinalMethod(a: Int32) {} 33 | } 34 | 35 | class ClassWithInvalidTypeArgCounts extends AbstractClass { 36 | let field: AbstractClass; 37 | 38 | func Method(a: AbstractClass) -> AbstractClass {} 39 | } 40 | 41 | class CircularClassA extends CircularClassB {} 42 | class CircularClassB extends CircularClassA {} 43 | 44 | struct StructWithNonStaticMethod { 45 | func Method() {} 46 | } 47 | 48 | class ScriptedClassWithNativeMembers { 49 | native let field: Int32; 50 | 51 | native func NativeMethod() 52 | } 53 | 54 | class ClassWithInvalidPersistentFields { 55 | persistent let string: String; 56 | persistent let variant: Variant; 57 | persistent let res: ResRef; 58 | 59 | persistent let int: Int32; 60 | persistent let float: Float; 61 | persistent let array: [Int32]; 62 | } 63 | 64 | class ClassWithBadVariance<+A, -B> { 65 | func MethodA(a: A) 66 | func MethodB() -> B 67 | } 68 | 69 | class Animal {} 70 | 71 | class Chair {} 72 | 73 | class Dog extends Animal {} 74 | 75 | class ClassWithBound { 76 | } 77 | 78 | class ClassViolatingBound extends ClassWithBound {} 79 | 80 | class ClassNotViolatingBound1 extends ClassWithBound {} 81 | class ClassNotViolatingBound2 extends ClassWithBound {} 82 | 83 | struct ClashingIntStruct {} 84 | 85 | @nameImplementation(ClashingIntStruct as GenericStruct) 86 | @nameImplementation(NonClashingStringStruct as GenericStruct) 87 | @nameImplementation(DuplicateStringStruct as GenericStruct) 88 | struct GenericStruct {} 89 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/enum-checks.reds: -------------------------------------------------------------------------------- 1 | 2 | enum Enum { 3 | One = 1, 4 | Two = 2, 5 | 6 | DuplicateOne = 1, 7 | 8 | DuplicatedName = 3, 9 | DuplicatedName = 4, 10 | } 11 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/expr-checks.reds: -------------------------------------------------------------------------------- 1 | 2 | native func ReturnsInt() -> Int32 3 | native func ReturnsArray() -> [Int32] 4 | native func HasOutParam(out x: Int32) 5 | 6 | struct Struct { 7 | let x: Int32; 8 | } 9 | 10 | func Test() { 11 | ReturnsInt() = 2; 12 | 13 | (new Struct(2)).x; 14 | 15 | ArraySize(ReturnsArray()); 16 | ReturnsArray()[0] = 2; 17 | 18 | HasOutParam(2); 19 | } 20 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/function-checks.reds: -------------------------------------------------------------------------------- 1 | 2 | native func NativeFunc(x: Int32) -> Int32 // OK 3 | 4 | native func NativeFuncWithUnexpectedBody(x: Int32) -> Int32 { 5 | return x; 6 | } 7 | 8 | func FreeFunctionWithMissingBody(x: Int32) -> Int32 9 | 10 | final static func UnusedFinalStaticQualifiers(s: String) -> String { 11 | return s; 12 | } 13 | 14 | func FunctionWithVariance<+A>() -> A {} 15 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/inference.reds: -------------------------------------------------------------------------------- 1 | 2 | func ValidInference() -> Int32 { 3 | let f = (a) -> (b, c) -> (d) -> d(a(b), c); 4 | let i = f((x) -> x)(1, "")((x, _) -> x); 5 | return i; 6 | } 7 | 8 | func ValidInference() -> Int32 { 9 | let f = (a) -> (b, c) -> (d) -> d(a(b), c); 10 | let i = f((x) -> x)("", 1)((x, _) -> x); 11 | return i; 12 | } 13 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/interpolation-error.reds: -------------------------------------------------------------------------------- 1 | 2 | func InterpolationError() -> String { 3 | return s"asd \(unresolved)"; 4 | } 5 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/invariance-checks.reds: -------------------------------------------------------------------------------- 1 | 2 | class Class {} 3 | 4 | class Subclass extends Class { 5 | let children: [Class]; 6 | } 7 | 8 | func AcceptsClassArray(arr: [Class]) {} 9 | 10 | func PassesSubclassArray() { 11 | let arr: [Subclass] = []; 12 | AcceptsClassArray(arr); 13 | 14 | let scriptables: [IScriptable]; 15 | let sub: Subclass; 16 | ArrayPush(scriptables, sub); 17 | ArrayPush(scriptables, sub.children[0]); 18 | } 19 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/item-checks.reds: -------------------------------------------------------------------------------- 1 | import NonExisting 2 | 3 | let badTopLevelField: CName; 4 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/data/type-mismatch.reds: -------------------------------------------------------------------------------- 1 | 2 | class Animal {} 3 | 4 | class Pet extends Animal {} 5 | 6 | class List<+A> {} 7 | 8 | class Predicate<-A> {} 9 | 10 | func Testing() { 11 | let _: Int32 = ""; 12 | let _: CName = ""; 13 | let _: TweakDBID = ""; 14 | let _: ResRef = ""; 15 | let _: Animal = new Pet(); // OK 16 | let _: Pet = new Animal(); 17 | 18 | // covariance 19 | let a: List = new List(); // OK 20 | let b: List = new List(); // OK 21 | let c: List = new List(); 22 | 23 | // contravariance 24 | let _: Predicate = new Predicate(); // OK 25 | let _: Predicate = new Predicate(); 26 | let _: Predicate = new Predicate(); // OK 27 | } 28 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/errors.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use redscript_ast::SourceMap; 4 | use redscript_compiler_api::{ 5 | CompileErrorReporter, Diagnostic, SourceMapExt, Symbols, TypeInterner, 6 | }; 7 | use redscript_compiler_frontend::infer_from_sources; 8 | 9 | #[test] 10 | fn compilation_errors() { 11 | insta::glob!("data/*.reds", |path| { 12 | let current = env::current_dir().unwrap().canonicalize().unwrap(); 13 | let relative = path 14 | .strip_prefix(¤t) 15 | .unwrap() 16 | .to_string_lossy() 17 | .replace("\\", "/"); 18 | let sources = SourceMap::from_files(&[relative]).unwrap(); 19 | sources.populate_boot_lib(); 20 | 21 | let interner = TypeInterner::default(); 22 | let symbols = Symbols::with_default_types(); 23 | let mut reporter = CompileErrorReporter::default(); 24 | 25 | let (_, _) = infer_from_sources(&sources, symbols, &mut reporter, &interner); 26 | insta::assert_snapshot!(DisplayDiagnostics(reporter.into_reported(), &sources)); 27 | }); 28 | } 29 | 30 | struct DisplayDiagnostics<'ctx>(Vec>, &'ctx SourceMap); 31 | 32 | impl std::fmt::Display for DisplayDiagnostics<'_> { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | self.0 35 | .iter() 36 | .try_for_each(|diagnostic| writeln!(f, "{}", diagnostic.display(self.1).unwrap())) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@annotation-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/annotation-checks.reds 5 | --- 6 | [INVALID_ANN_USE] At tests/data/annotation-checks.reds:6:13 7 | @wrapMethod(UserClass) 8 | ^^^^^^^^^ 9 | this annotation attempts to modify a user-defined symbol, which is not allowed 10 | 11 | [UNRESOLVED_TYPE] At tests/data/annotation-checks.reds:10:13 12 | @wrapMethod(NonExistingClass) 13 | ^^^^^^^^^^^^^^^^ 14 | 'NonExistingClass' is not a known type 15 | 16 | [INVALID_ANN_USE] At tests/data/annotation-checks.reds:11:6 17 | func HelloWorld(x: String) { 18 | ^^^^^^^^^^ 19 | could not find a method with a matching signature for the @wrapMethod(NonExistingClass) annotation 20 | 21 | [INVALID_ANN_USE] At tests/data/annotation-checks.reds:15:6 22 | func ForbiddenGenericAnnotatedMethod(a: A) { 23 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 24 | annotated methods cannot be generic 25 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@class-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/class-checks.reds 5 | --- 6 | [SYM_REDEFINITION] At tests/data/class-checks.reds:23:7 7 | class ClassDefinedTwice {} 8 | ^^^^^^^^^^^^^^^^^^ 9 | this name is already defined in the scope 10 | 11 | [INVALID_ANN_USE] At tests/data/class-checks.reds:19:1 12 | @badAnnotation() 13 | ^^^ 14 | 'badAnnotation' is not a valid annotation in this context 15 | 16 | [UNEXPECTED_NATIVE] At tests/data/class-checks.reds:49:14 17 | native let field: Int32; 18 | ^^^^^ 19 | scripted types are not allowed to have native members 20 | 21 | [UNEXPECTED_NATIVE] At tests/data/class-checks.reds:51:15 22 | native func NativeMethod() 23 | ^^^^^^^^^^^^ 24 | scripted types are not allowed to have native members 25 | 26 | [INVALID_PERSISTENT] At tests/data/class-checks.reds:55:18 27 | persistent let string: String; 28 | ^^^^^^ 29 | Strings, Variants and Resources cannot be persisted 30 | 31 | [INVALID_PERSISTENT] At tests/data/class-checks.reds:56:18 32 | persistent let variant: Variant; 33 | ^^^^^^^ 34 | Strings, Variants and Resources cannot be persisted 35 | 36 | [INVALID_PERSISTENT] At tests/data/class-checks.reds:57:18 37 | persistent let res: ResRef; 38 | ^^^ 39 | Strings, Variants and Resources cannot be persisted 40 | 41 | [MISSING_BODY] At tests/data/class-checks.reds:65:8 42 | func MethodA(a: A) 43 | ^^^^^^^ 44 | this function must have a body 45 | 46 | [MISSING_BODY] At tests/data/class-checks.reds:66:8 47 | func MethodB() -> B 48 | ^^^^^^^ 49 | this function must have a body 50 | 51 | [NON_STATIC_STRUCT_FN] At tests/data/class-checks.reds:45:8 52 | func Method() {} 53 | ^^^^^^ 54 | struct methods must be static 55 | 56 | [SYM_REDEFINITION] At tests/data/class-checks.reds:85:21 57 | @nameImplementation(ClashingIntStruct as GenericStruct) 58 | ^^^^^^^^^^^^^^^^^^ 59 | this name is already defined in the scope 60 | 61 | [DUP_IMPL] At tests/data/class-checks.reds:87:46 62 | @nameImplementation(DuplicateStringStruct as GenericStruct) 63 | ^^^^^^^^^^^^^^^^^^^^^ 64 | this implementation is a duplicate of a previous one 65 | 66 | [INVALID_BASE] At tests/data/class-checks.reds:4:7 67 | class ClassExtendingStruct extends Struct {} 68 | ^^^^^^^^^^^^^^^^^^^^^ 69 | this type cannot inherit from a struct 70 | 71 | [INVALID_BASE] At tests/data/class-checks.reds:17:7 72 | class ClassExtendingPrimitive extends Int32 {} 73 | ^^^^^^^^^^^^^^^^^^^^^^^^ 74 | this type cannot inherit from a primitive 75 | 76 | [INVALID_TYPE_ARG_COUNT] At tests/data/class-checks.reds:35:7 77 | class ClassWithInvalidTypeArgCounts extends AbstractClass { 78 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 79 | type AbstractClass expects 1 type arguments 80 | 81 | [INVALID_TYPE_ARG_COUNT] At tests/data/class-checks.reds:36:7 82 | let field: AbstractClass; 83 | ^^^^^ 84 | type AbstractClass expects 1 type arguments 85 | 86 | [INVALID_TYPE_ARG_COUNT] At tests/data/class-checks.reds:38:15 87 | func Method(a: AbstractClass) -> AbstractClass {} 88 | ^^^^^^^^^^^^^^^^ 89 | type AbstractClass expects 1 type arguments 90 | 91 | [INVALID_BASE] At tests/data/class-checks.reds:41:7 92 | class CircularClassA extends CircularClassB {} 93 | ^^^^^^^^^^^^^^^ 94 | this class circularly extends itself 95 | 96 | [INVALID_VARIANCE] At tests/data/class-checks.reds:65:16 97 | func MethodA(a: A) 98 | ^^^^ 99 | the type 'A' appears in contravariant position, which is incompatible with its declaration 100 | 101 | [INVALID_VARIANCE] At tests/data/class-checks.reds:66:8 102 | func MethodB() -> B 103 | ^^^^^^^ 104 | the type 'B' appears in covariant position, which is incompatible with its declaration 105 | 106 | [UNSASTISFIED_BOUND] At tests/data/class-checks.reds:78:7 107 | class ClassViolatingBound extends ClassWithBound {} 108 | ^^^^^^^^^^^^^^^^^^^^ 109 | type Chair does not satisfy expected bound Animal 110 | 111 | [DUP_METHOD] At tests/data/class-checks.reds:25:7 112 | class ClassWithDuplicateMethod { 113 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 114 | this class contains a duplicated implementation of the 'Method' method 115 | 116 | [MISSING_IMPL] At tests/data/class-checks.reds:11:7 117 | class IncompleteClass extends AbstractClass {} 118 | ^^^^^^^^^^^^^^^^ 119 | this class is missing some required method implementation(s): 120 | func UnimplementedMethod(a: Int32) {} 121 | 122 | [FINAL_FN_OVERRIDE] At tests/data/class-checks.reds:30:7 123 | class ClassOverridingFinalMethod extends AbstractClass { 124 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 125 | this class overrides a final method 'FinalMethod' 126 | 127 | [MISSING_IMPL] At tests/data/class-checks.reds:35:7 128 | class ClassWithInvalidTypeArgCounts extends AbstractClass { 129 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 130 | this class is missing some required method implementation(s): 131 | func UnimplementedMethod(a: Int32) {} 132 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@enum-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/enum-checks.reds 5 | --- 6 | [DUP_VARIANT_VAL] At tests/data/enum-checks.reds:6:3 7 | DuplicateOne = 1, 8 | ^^^^^^^^^^^^^^^^ 9 | duplicate variant value 10 | 11 | [DUP_VARIANT_NAME] At tests/data/enum-checks.reds:9:3 12 | DuplicatedName = 4, 13 | ^^^^^^^^^^^^^^^^^^ 14 | duplicate variant name 15 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@expr-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/expr-checks.reds 5 | --- 6 | [INVALID_PLACE] At tests/data/expr-checks.reds:11:3 7 | ReturnsInt() = 2; 8 | ^^^^^^^^^^^^^^^^ 9 | this expression is not a place that can be written to 10 | 11 | [INVALID_TEMP] At tests/data/expr-checks.reds:13:3 12 | (new Struct(2)).x; 13 | ^^^^^^^^^^^^^^^^^ 14 | a temporary cannot be used here, consider storing this value in a variable 15 | 16 | [INVALID_TEMP] At tests/data/expr-checks.reds:15:13 17 | ArraySize(ReturnsArray()); 18 | ^^^^^^^^^^^^^^ 19 | a temporary cannot be used here, consider storing this value in a variable 20 | 21 | [INVALID_TEMP] At tests/data/expr-checks.reds:16:3 22 | ReturnsArray()[0] = 2; 23 | ^^^^^^^^^^^^^^ 24 | a temporary cannot be used here, consider storing this value in a variable 25 | 26 | [INVALID_TEMP] At tests/data/expr-checks.reds:18:15 27 | HasOutParam(2); 28 | ^ 29 | a temporary cannot be used here, consider storing this value in a variable 30 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@function-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/function-checks.reds 5 | --- 6 | [UNEXPECTED_BODY] At tests/data/function-checks.reds:4:13 7 | native func NativeFuncWithUnexpectedBody(x: Int32) -> Int32 { 8 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 9 | this function cannot have a body 10 | 11 | [MISSING_BODY] At tests/data/function-checks.reds:8:6 12 | func FreeFunctionWithMissingBody(x: Int32) -> Int32 13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 | this function must have a body 15 | 16 | [UNUSED_ITEM_QUALIFIERS] At tests/data/function-checks.reds:10:19 17 | final static func UnusedFinalStaticQualifiers(s: String) -> String { 18 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 19 | final, static qualifiers have no effect on this item 20 | 21 | [NON_DATA_VARIANCE] At tests/data/function-checks.reds:14:28 22 | func FunctionWithVariance<+A>() -> A {} 23 | ^ 24 | non-data types cannot have variance 25 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@inference.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(reporter.into_reported(), &sources)" 4 | input_file: crates/compiler/frontend/tests/data/inference.reds 5 | snapshot_kind: text 6 | --- 7 | [TYPE_ERR] At tests/data/inference.reds:11:10 8 | return i; 9 | ^ 10 | type mismatch: found String when expected Int32 11 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@interpolation-error.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(reporter.into_reported(), &sources)" 4 | input_file: crates/compiler/frontend/tests/data/interpolation-error.reds 5 | --- 6 | [UNRESOLVED_REF] At tests/data/interpolation-error.reds:3:18 7 | return s"asd \(unresolved)"; 8 | ^^^^^^^^^^ 9 | 'unresolved' is not defined 10 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@invariance-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/invariance-checks.reds 5 | --- 6 | [TYPE_ERR] At tests/data/invariance-checks.reds:12:21 7 | AcceptsClassArray(arr); 8 | ^^^ 9 | type mismatch: found Class when expected Subclass 10 | when comparing array and array 11 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@item-checks.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/item-checks.reds 5 | --- 6 | [UNRESOLVED_IMPORT] At tests/data/item-checks.reds:1:1 7 | import NonExisting 8 | ^^^ 9 | 'NonExisting' could not be found 10 | 11 | [UNEXPECTED_ITEM] At tests/data/item-checks.reds:3:5 12 | let badTopLevelField: CName; 13 | ^^^^^^^^^^^^^^^^ 14 | items of this type are not allowed here 15 | -------------------------------------------------------------------------------- /crates/compiler/frontend/tests/snapshots/errors__compilation_errors@type-mismatch.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/compiler/frontend/tests/errors.rs 3 | expression: "DisplayDiagnostics(diagnostics, &sources)" 4 | input_file: crates/compiler/frontend/tests/data/type-mismatch.reds 5 | --- 6 | [TYPE_ERR] At tests/data/type-mismatch.reds:11:18 7 | let _: Int32 = ""; 8 | ^^ 9 | type mismatch: found String when expected Int32 10 | 11 | [WRONG_STRING_LIT] At tests/data/type-mismatch.reds:12:18 12 | let _: CName = ""; 13 | ^^ 14 | expected a CName here, you should prefix your literal with 'n', e.g. n"lorem ipsum" 15 | 16 | [TYPE_ERR] At tests/data/type-mismatch.reds:12:18 17 | let _: CName = ""; 18 | ^^ 19 | type mismatch: found String when expected CName 20 | 21 | [WRONG_STRING_LIT] At tests/data/type-mismatch.reds:13:22 22 | let _: TweakDBID = ""; 23 | ^^ 24 | expected a TweakDBID here, you should prefix your literal with 't', e.g. t"lorem ipsum" 25 | 26 | [TYPE_ERR] At tests/data/type-mismatch.reds:13:22 27 | let _: TweakDBID = ""; 28 | ^^ 29 | type mismatch: found String when expected TweakDBID 30 | 31 | [WRONG_STRING_LIT] At tests/data/type-mismatch.reds:14:19 32 | let _: ResRef = ""; 33 | ^^ 34 | expected a ResRef here, you should prefix your literal with 'r', e.g. r"lorem ipsum" 35 | 36 | [TYPE_ERR] At tests/data/type-mismatch.reds:14:19 37 | let _: ResRef = ""; 38 | ^^ 39 | type mismatch: found String when expected ResRef 40 | 41 | [TYPE_ERR] At tests/data/type-mismatch.reds:16:16 42 | let _: Pet = new Animal(); 43 | ^^^^^^^^^^^^ 44 | type mismatch: found Animal when expected Pet 45 | 46 | [TYPE_ERR] At tests/data/type-mismatch.reds:21:22 47 | let c: List = new List(); 48 | ^^^^^^^^^^^^^^^^^^ 49 | type mismatch: found Animal when expected Pet 50 | when comparing List and List 51 | 52 | [TYPE_ERR] At tests/data/type-mismatch.reds:25:30 53 | let _: Predicate = new Predicate(); 54 | ^^^^^^^^^^^^^^^^^^^^ 55 | type mismatch: found Animal when expected Pet 56 | when comparing Predicate and Predicate 57 | -------------------------------------------------------------------------------- /crates/decompiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-decompiler" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-io.workspace = true 9 | redscript-ast.workspace = true 10 | redscript-formatter.workspace = true 11 | 12 | thiserror.workspace = true 13 | hashbrown.workspace = true 14 | 15 | [lints] 16 | workspace = true 17 | -------------------------------------------------------------------------------- /crates/decompiler/src/control_flow.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use redscript_io::{CodeIter, Instr, Offset}; 4 | 5 | use crate::error::Error; 6 | use crate::location::{Bounds, Location}; 7 | 8 | pub fn build_control_flow(mut it: CodeIter<'_, Offset>) -> Result { 9 | ControlFlowBlock::default().build(&mut it, Location::MAX) 10 | } 11 | 12 | #[derive(Debug, Default)] 13 | pub struct ControlFlowBlock { 14 | type_: BlockType, 15 | children: BTreeMap, 16 | entry: Option, 17 | exit: Option, 18 | } 19 | 20 | impl ControlFlowBlock { 21 | fn new(type_: BlockType) -> Self { 22 | Self { 23 | type_, 24 | children: BTreeMap::new(), 25 | entry: None, 26 | exit: None, 27 | } 28 | } 29 | 30 | fn case() -> Self { 31 | Self::new(BlockType::Case) 32 | } 33 | 34 | fn conditional() -> Self { 35 | Self::new(BlockType::Conditional) 36 | } 37 | 38 | pub fn entry(&self) -> Option { 39 | self.entry 40 | } 41 | 42 | pub fn exit(&self) -> Option { 43 | self.exit 44 | } 45 | 46 | pub fn type_(&self) -> BlockType { 47 | self.type_ 48 | } 49 | 50 | fn with_entry(mut self, entry: Location) -> Self { 51 | self.entry = Some(entry); 52 | self 53 | } 54 | 55 | fn with_exit(mut self, exit: Location) -> Self { 56 | self.exit = Some(exit); 57 | self 58 | } 59 | 60 | pub fn get_block(&self, bounds: Bounds) -> &ControlFlowBlock { 61 | if let Some((_, parent)) = self 62 | .children 63 | .range( 64 | Bounds::new(Location::ZERO, bounds.end()) 65 | ..=Bounds::new(bounds.start(), Location::MAX), 66 | ) 67 | .next_back() 68 | .filter(|(b, _)| b.contains(bounds)) 69 | { 70 | parent.get_block(bounds) 71 | } else { 72 | self 73 | } 74 | } 75 | 76 | pub fn get_containing(&self, loc: Location) -> (Bounds, &ControlFlowBlock) { 77 | if let Some((&bounds, parent)) = self 78 | .children 79 | .range(Bounds::new(Location::ZERO, loc)..=Bounds::new(loc, Location::MAX)) 80 | .next_back() 81 | .filter(|(bounds, _)| bounds.contains_location(loc)) 82 | { 83 | (bounds, parent.get_containing(loc).1) 84 | } else { 85 | (Bounds::UNBOUNDED, self) 86 | } 87 | } 88 | 89 | fn insert(&mut self, bounds: Bounds, block: ControlFlowBlock) { 90 | if let Some((_, parent)) = self 91 | .children 92 | .range_mut( 93 | Bounds::new(Location::ZERO, bounds.end()) 94 | ..Bounds::new(bounds.start(), Location::MAX), 95 | ) 96 | .next_back() 97 | .filter(|(b, _)| b.contains(bounds)) 98 | { 99 | parent.children.insert(bounds, block); 100 | } else { 101 | self.children.insert(bounds, block); 102 | } 103 | } 104 | 105 | fn build( 106 | self, 107 | it: &mut CodeIter<'_, Offset>, 108 | end: Location, 109 | ) -> Result { 110 | ControlFlowBuilder::new(self, it).consume(end) 111 | } 112 | } 113 | 114 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 115 | pub enum BlockType { 116 | #[default] 117 | Block, 118 | While, 119 | Case, 120 | Conditional, 121 | } 122 | 123 | struct ControlFlowBuilder<'ctx, 'i> { 124 | block: ControlFlowBlock, 125 | it: &'ctx mut CodeIter<'i, Offset>, 126 | } 127 | 128 | impl<'ctx, 'i> ControlFlowBuilder<'ctx, 'i> { 129 | fn new(block: ControlFlowBlock, it: &'ctx mut CodeIter<'i, Offset>) -> Self { 130 | Self { block, it } 131 | } 132 | 133 | fn position(&self) -> Location { 134 | self.it.virtual_offset().into() 135 | } 136 | 137 | fn consume(mut self, end: Location) -> Result { 138 | let start = self.position(); 139 | 140 | loop { 141 | let pos = self.position(); 142 | let Some(instr) = self.it.next() else { 143 | break; 144 | }; 145 | match instr { 146 | Instr::SwitchLabel(label) => { 147 | let body = pos + label.body(); 148 | let next = pos + label.next_case(); 149 | if body > next { 150 | continue; 151 | } 152 | let nested = ControlFlowBlock::case() 153 | .with_exit(next) 154 | .build(self.it, next)?; 155 | self.block.insert(Bounds::new(body, next), nested); 156 | } 157 | Instr::Jump(jump) => { 158 | if i32::from(jump.target()) == instr.virtual_size() as i32 { 159 | // no-op jump 160 | continue; 161 | } 162 | let target = pos + jump.target(); 163 | if target + instr.virtual_size() == start { 164 | self.block.type_ = BlockType::While; 165 | self.block.exit = Some(pos); 166 | } else { 167 | self.block.exit = Some(target); 168 | } 169 | return Ok(self.block); 170 | } 171 | Instr::JumpIfFalse(jump) => { 172 | let target = pos + jump.target(); 173 | let nested = ControlFlowBlock::conditional() 174 | .with_entry(pos) 175 | .build(self.it, target)?; 176 | let kind = nested.type_; 177 | let exit = nested.exit; 178 | self.block.insert(Bounds::new(pos, target), nested); 179 | 180 | if let (BlockType::Conditional, Some(target)) = (kind, exit) { 181 | while self.position() < target { 182 | let offset = self.position(); 183 | let branches = 184 | ControlFlowBlock::conditional().build(self.it, target)?; 185 | self.block.insert(Bounds::new(offset, target), branches); 186 | } 187 | } 188 | } 189 | _ => {} 190 | } 191 | if self.position() == end { 192 | break; 193 | } 194 | } 195 | 196 | Ok(self.block) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /crates/decompiler/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error("Unexpected end of code")] 8 | UnexpectedEndOfCode, 9 | #[error("Missing {1} in the pool at index {0}")] 10 | MissingPoolItem(u32, &'static str), 11 | #[error("Missing parameter end instruction")] 12 | MissingParamEnd, 13 | #[error("Invalid cast with void return type")] 14 | VoidCast, 15 | #[error("Invalid number of arguments for an operator")] 16 | InvalidOperatorArgs, 17 | #[error("Read error: {0}")] 18 | ReadFailure(redscript_io::byte::Error), 19 | } 20 | 21 | impl From for Error { 22 | fn from(err: redscript_io::byte::Error) -> Self { 23 | Error::ReadFailure(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/decompiler/src/location.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | 3 | use redscript_io::Offset; 4 | 5 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 6 | pub struct Location(u32); 7 | 8 | impl Location { 9 | pub const MAX: Location = Location(u32::MAX); 10 | pub const ZERO: Location = Location(0); 11 | } 12 | 13 | impl From for Location { 14 | fn from(x: u32) -> Self { 15 | Self(x) 16 | } 17 | } 18 | 19 | impl From for u32 { 20 | fn from(x: Location) -> u32 { 21 | x.0 22 | } 23 | } 24 | 25 | impl Add for Location { 26 | type Output = Location; 27 | 28 | fn add(self, rhs: u16) -> Self::Output { 29 | Location(self.0 + u32::from(rhs)) 30 | } 31 | } 32 | 33 | impl Add for Location { 34 | type Output = Location; 35 | 36 | fn add(self, rhs: Offset) -> Self::Output { 37 | Location( 38 | self.0 39 | .checked_add_signed(i32::from(rhs)) 40 | .expect("bytecode offset should not overflow"), 41 | ) 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 46 | pub struct Bounds { 47 | start: Location, 48 | end: Location, 49 | } 50 | 51 | impl Bounds { 52 | pub const UNBOUNDED: Bounds = Bounds { 53 | start: Location::ZERO, 54 | end: Location::MAX, 55 | }; 56 | 57 | pub fn new(start: Location, end: Location) -> Self { 58 | Self { start, end } 59 | } 60 | 61 | pub fn start(&self) -> Location { 62 | self.start 63 | } 64 | 65 | pub fn end(&self) -> Location { 66 | self.end 67 | } 68 | 69 | pub fn contains(&self, bounds: Bounds) -> bool { 70 | self.start <= bounds.start && bounds.end <= self.end 71 | } 72 | 73 | pub fn contains_location(&self, loc: Location) -> bool { 74 | self.start <= loc && loc <= self.end 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/dotfile/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-dotfile" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | anyhow.workspace = true 9 | serde = { workspace = true, features = ["derive"] } 10 | toml.workspace = true 11 | 12 | [lints] 13 | workspace = true 14 | -------------------------------------------------------------------------------- /crates/dotfile/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use serde::Deserialize; 5 | 6 | const DOTFILE_NAME: &str = ".redscript"; 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub struct Dotfile { 10 | #[serde(default = "default_roots")] 11 | pub source_roots: Vec, 12 | #[serde(default)] 13 | pub format: FormatConfig, 14 | } 15 | 16 | impl Dotfile { 17 | pub fn load_or_default(path: impl AsRef) -> anyhow::Result { 18 | match fs::read_to_string(path.as_ref().join(DOTFILE_NAME)) { 19 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), 20 | Err(err) => Err(err.into()), 21 | Ok(content) => Ok(toml::from_str(&content)?), 22 | } 23 | } 24 | } 25 | 26 | impl Default for Dotfile { 27 | fn default() -> Self { 28 | Self { 29 | source_roots: default_roots(), 30 | format: FormatConfig::default(), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug, Default, Deserialize)] 36 | pub struct FormatConfig { 37 | pub indent: Option, 38 | pub max_width: Option, 39 | pub max_chain_fields: Option, 40 | pub max_chain_calls: Option, 41 | pub max_chain_operators: Option, 42 | pub max_chain_total: Option, 43 | } 44 | 45 | fn default_roots() -> Vec { 46 | vec![PathBuf::from(".")] 47 | } 48 | -------------------------------------------------------------------------------- /crates/io/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-io" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bitfield-struct.workspace = true 9 | identity-hash = { workspace = true, optional = true } 10 | foldhash.workspace = true 11 | indexmap.workspace = true 12 | byte.workspace = true 13 | crc32fast.workspace = true 14 | vmap = { workspace = true, optional = true } 15 | 16 | [dev-dependencies] 17 | vmap.workspace = true 18 | argh.workspace = true 19 | 20 | [[example]] 21 | name = "cli" 22 | required-features = ["mmap"] 23 | 24 | [features] 25 | mmap = ["dep:vmap"] 26 | identity-hash = ["dep:identity-hash"] 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /crates/io/examples/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use argh::FromArgs; 4 | use redscript_io::ScriptBundle; 5 | use vmap::Map; 6 | 7 | #[derive(FromArgs)] 8 | /// Example CLI tool options 9 | struct Opts { 10 | /// input file path 11 | #[argh(option)] 12 | input: PathBuf, 13 | /// output file path 14 | #[argh(option)] 15 | output: PathBuf, 16 | } 17 | 18 | fn main() { 19 | let opts = argh::from_env::(); 20 | 21 | let (map, _file) = Map::with_options().open(opts.input).unwrap(); 22 | ScriptBundle::from_bytes(&map) 23 | .unwrap() 24 | .into_writeable() 25 | .save(opts.output) 26 | .unwrap(); 27 | } 28 | -------------------------------------------------------------------------------- /crates/io/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use byte::ctx::LittleEndian; 4 | 5 | mod bundle; 6 | mod definition; 7 | mod index; 8 | mod instr; 9 | mod util; 10 | 11 | const ENDIANESS: LittleEndian = byte::LE; 12 | 13 | #[cfg(feature = "mmap")] 14 | pub use bundle::SaveError; 15 | pub use bundle::{BundleReader, PoolItemIndex, PoolItemIndexMut, ScriptBundle, WriteableBundle}; 16 | pub use byte; 17 | pub use definition::{ 18 | Class, ClassFlags, CodeIter, Definition, Enum, EnumMember, Field, FieldFlags, Function, 19 | FunctionBody, FunctionFlags, IndexedDefinition, Local, LocalFlags, Parameter, ParameterFlags, 20 | Property, SourceFile, SourceReference, Type, TypeKind, Visibility, 21 | }; 22 | pub use index::{ 23 | CNameIndex, ClassIndex, EnumIndex, EnumValueIndex, FieldIndex, FunctionIndex, LocalIndex, 24 | ParameterIndex, ResourceIndex, SourceFileIndex, StringIndex, TweakDbIndex, TypeIndex, 25 | }; 26 | pub use instr::{ 27 | Breakpoint, Conditional, Instr, InvokeFlags, Jump, Offset, Profile, Switch, SwitchLabel, 28 | }; 29 | 30 | pub type Str<'a> = Cow<'a, str>; 31 | -------------------------------------------------------------------------------- /crates/io/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use byte::ctx::{Endianess, Len}; 4 | use byte::{BytesExt, Measure, TryRead, TryWrite}; 5 | 6 | use crate::Str; 7 | use crate::index::NzPoolIndex; 8 | 9 | pub struct Prefixed(pub Ctx); 10 | 11 | impl<'a, A, Ctx> TryRead<'a, Prefixed> for Vec 12 | where 13 | A: TryRead<'a, Ctx>, 14 | Ctx: Endianess, 15 | { 16 | fn try_read(bytes: &'a [u8], Prefixed(ctx): Prefixed) -> byte::Result<(Self, usize)> { 17 | let offset = &mut 0; 18 | let len: u32 = bytes.read(offset, ctx)?; 19 | let result = bytes 20 | .read_iter(offset, ctx) 21 | .take(len as _) 22 | .collect::, _>>()?; 23 | Ok((result, *offset)) 24 | } 25 | } 26 | 27 | impl TryWrite> for Vec 28 | where 29 | A: TryWrite, 30 | Ctx: Endianess, 31 | { 32 | fn try_write(&self, bytes: &mut [u8], Prefixed(ctx): Prefixed) -> byte::Result { 33 | let offset = &mut 0; 34 | bytes.write(offset, &(self.len() as u32), ctx)?; 35 | for item in self { 36 | bytes.write(offset, item, ctx)?; 37 | } 38 | Ok(*offset) 39 | } 40 | } 41 | 42 | impl Measure> for Vec 43 | where 44 | A: Measure, 45 | Ctx: Copy, 46 | { 47 | fn measure(&self, Prefixed(ctx): Prefixed) -> usize { 48 | let len = mem::size_of::(); 49 | let items: usize = self.iter().map(|item| item.measure(ctx)).sum(); 50 | len + items 51 | } 52 | } 53 | 54 | impl<'a, Ctx> TryRead<'a, Prefixed> for Str<'a> 55 | where 56 | Ctx: Endianess, 57 | { 58 | fn try_read(bytes: &'a [u8], Prefixed(ctx): Prefixed) -> byte::Result<(Self, usize)> { 59 | let offset = &mut 0; 60 | let len: u16 = bytes.read(offset, ctx)?; 61 | let result: &'a str = bytes.read(offset, Len(len.into()))?; 62 | Ok((Str::Borrowed(result), *offset)) 63 | } 64 | } 65 | 66 | impl TryWrite> for Str<'_> 67 | where 68 | Ctx: Endianess, 69 | { 70 | fn try_write(&self, bytes: &mut [u8], Prefixed(ctx): Prefixed) -> byte::Result { 71 | let offset = &mut 0; 72 | bytes.write(offset, &(self.len() as u16), ctx)?; 73 | bytes.write(offset, self.as_ref(), Len(self.len()))?; 74 | Ok(*offset) 75 | } 76 | } 77 | 78 | impl Measure> for Str<'_> { 79 | #[inline] 80 | fn measure(&self, _: Prefixed) -> usize { 81 | mem::size_of::() + self.len() 82 | } 83 | } 84 | 85 | pub struct FlagDependent(pub Ctx); 86 | 87 | impl<'a, A, Ctx> TryRead<'a, FlagDependent> for Option 88 | where 89 | A: TryRead<'a, Ctx>, 90 | { 91 | #[inline] 92 | fn try_read( 93 | bytes: &'a [u8], 94 | FlagDependent(ctx): FlagDependent, 95 | ) -> byte::Result<(Self, usize)> { 96 | let (value, size) = A::try_read(bytes, ctx)?; 97 | Ok((Some(value), size)) 98 | } 99 | } 100 | 101 | impl TryWrite> for Option 102 | where 103 | A: TryWrite, 104 | { 105 | #[inline] 106 | fn try_write( 107 | &self, 108 | bytes: &mut [u8], 109 | FlagDependent(ctx): FlagDependent, 110 | ) -> byte::Result { 111 | match self { 112 | Some(value) => value.try_write(bytes, ctx), 113 | None => Ok(0), 114 | } 115 | } 116 | } 117 | 118 | impl Measure> for Option 119 | where 120 | A: Measure, 121 | { 122 | #[inline] 123 | fn measure(&self, FlagDependent(ctx): FlagDependent) -> usize { 124 | match self { 125 | Some(value) => value.measure(ctx), 126 | None => 0, 127 | } 128 | } 129 | } 130 | 131 | pub struct OptionalIndex(pub Ctx); 132 | 133 | impl TryRead<'_, OptionalIndex> for Option> { 134 | #[inline] 135 | fn try_read( 136 | bytes: &[u8], 137 | OptionalIndex(ctx): OptionalIndex, 138 | ) -> byte::Result<(Self, usize)> { 139 | let (index, size) = TryRead::try_read(bytes, ctx)?; 140 | Ok((NzPoolIndex::new(index), size)) 141 | } 142 | } 143 | 144 | impl TryWrite> for Option> { 145 | #[inline] 146 | fn try_write( 147 | &self, 148 | bytes: &mut [u8], 149 | OptionalIndex(ctx): OptionalIndex, 150 | ) -> byte::Result { 151 | match self { 152 | &Some(i) => u32::from(i).try_write(bytes, ctx), 153 | None => 0u32.try_write(bytes, ctx), 154 | } 155 | } 156 | } 157 | 158 | impl Measure> for Option> { 159 | #[inline] 160 | fn measure(&self, _: OptionalIndex) -> usize { 161 | mem::size_of::() 162 | } 163 | } 164 | 165 | macro_rules! impl_bitfield_read_write { 166 | ($ty:ty) => { 167 | impl TryRead<'_, Ctx> for $ty { 168 | #[inline] 169 | fn try_read(bytes: &[u8], ctx: Ctx) -> byte::Result<($ty, usize)> { 170 | let (value, size) = TryRead::try_read(bytes, ctx)?; 171 | Ok((<$ty>::from_bits(value), size)) 172 | } 173 | } 174 | 175 | impl TryWrite for $ty { 176 | #[inline] 177 | fn try_write(&self, bytes: &mut [u8], ctx: Ctx) -> byte::Result { 178 | self.into_bits().try_write(bytes, ctx) 179 | } 180 | } 181 | 182 | impl Measure for $ty { 183 | #[inline] 184 | fn measure(&self, _ctx: Ctx) -> usize { 185 | ::core::mem::size_of::<$ty>() 186 | } 187 | } 188 | }; 189 | } 190 | 191 | pub(crate) use impl_bitfield_read_write; 192 | -------------------------------------------------------------------------------- /crates/io/tests/tests.rs: -------------------------------------------------------------------------------- 1 | use redscript_io::{Definition, ScriptBundle, Type, TypeKind}; 2 | 3 | #[test] 4 | fn roundtrip_def() { 5 | let mut bundle = ScriptBundle::default(); 6 | let name = bundle.cnames_mut().add("whatever"); 7 | let typ = Type::new(name, TypeKind::Class); 8 | let idx = bundle.define(typ.clone()); 9 | 10 | assert_eq!(bundle[idx], typ); 11 | } 12 | 13 | #[test] 14 | fn roundtrip_encode() { 15 | let mut bundle = ScriptBundle::default(); 16 | let name = bundle.cnames_mut().add("whatever"); 17 | let typ = Type::new(name, TypeKind::Class); 18 | bundle.define(typ.clone()); 19 | 20 | let bytes = bundle.into_writeable().to_bytes().unwrap(); 21 | let bundle = ScriptBundle::from_bytes(&bytes).unwrap(); 22 | let defs = bundle.definitions().cloned().collect::>(); 23 | assert_eq!(&defs, &[Definition::Type(typ)]); 24 | } 25 | -------------------------------------------------------------------------------- /crates/scc/capi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scc-lib" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | scc-shared.workspace = true 12 | anyhow.workspace = true 13 | 14 | mimalloc.workspace = true 15 | 16 | [lints] 17 | workspace = true 18 | -------------------------------------------------------------------------------- /crates/scc/capi/include/scc.h: -------------------------------------------------------------------------------- 1 | #ifndef SCC_H 2 | #define SCC_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #if defined(_WIN32) && !defined(BINDING_TEST) 9 | #include 10 | #endif 11 | 12 | typedef struct SccSettings SccSettings; 13 | typedef struct SccOutput SccOutput; 14 | typedef struct SccResult SccResult; 15 | typedef struct SccSourceRef SccSourceRef; 16 | 17 | typedef struct StrWithLen { 18 | const char *str; 19 | size_t len; 20 | } StrWithLen; 21 | 22 | enum SccSourceRefType { 23 | SCC_SOURCE_REF_TYPE_UNDEFINED = 0, 24 | SCC_SOURCE_REF_TYPE_CLASS = 1, 25 | SCC_SOURCE_REF_TYPE_FIELD = 2, 26 | SCC_SOURCE_REF_TYPE_FUNCTION = 3, 27 | SCC_SOURCE_REF_TYPE_ENUM = 4, 28 | }; 29 | 30 | typedef SccSettings *scc_settings_new(const char *r6_dir); 31 | 32 | typedef void scc_settings_set_custom_cache_file(SccSettings *settings, 33 | const char *cache_file); 34 | 35 | typedef void scc_settings_set_output_cache_file(SccSettings *settings, 36 | const char *cache_file); 37 | 38 | typedef void scc_settings_add_script_path(SccSettings *settings, 39 | const char *path); 40 | 41 | typedef void scc_settings_disable_error_popup(SccSettings *settings); 42 | 43 | typedef SccResult *scc_compile(SccSettings *settings); 44 | 45 | typedef void scc_free_result(SccResult *result); 46 | 47 | typedef SccOutput *scc_get_success(SccResult *result); 48 | 49 | typedef size_t scc_copy_error(SccResult *result, char *buffer, 50 | size_t buffer_size); 51 | 52 | typedef SccSourceRef *scc_output_get_source_ref(SccOutput *output, 53 | size_t index); 54 | 55 | typedef size_t scc_output_source_ref_count(SccOutput *output); 56 | 57 | typedef uint8_t scc_source_ref_type(SccOutput *output, SccSourceRef *ref); 58 | 59 | typedef bool scc_source_ref_is_native(SccOutput *output, SccSourceRef *ref); 60 | 61 | typedef StrWithLen scc_source_ref_name(SccOutput *output, SccSourceRef *ref); 62 | 63 | typedef StrWithLen scc_source_ref_parent_name(SccOutput *output, 64 | SccSourceRef *ref); 65 | 66 | typedef StrWithLen scc_source_ref_path(SccOutput *output, SccSourceRef *ref); 67 | 68 | typedef size_t scc_source_ref_line(SccOutput *output, SccSourceRef *ref); 69 | 70 | typedef struct SccApi { 71 | /** 72 | * Creates new compilation settings. 73 | */ 74 | scc_settings_new *settings_new; 75 | /** 76 | * Overrides the default cache file location. 77 | */ 78 | scc_settings_set_custom_cache_file *settings_set_custom_cache_file; 79 | /** 80 | * Sets a custom output cache file location. 81 | * 82 | * Added in redscript 0.5.18. It will be null if the loaded library version is 83 | * older. The caller should do a null check if they want to maintain backward 84 | * compatibility. 85 | */ 86 | scc_settings_set_output_cache_file *settings_set_output_cache_file; 87 | /** 88 | * Adds a path to be searched for scripts during compilation. 89 | */ 90 | scc_settings_add_script_path *settings_add_script_path; 91 | /** 92 | * Disables the error popup that is shown when the compiler encounters errors. 93 | * 94 | * Added in redscript 0.5.28. It will be null if the loaded library version is 95 | * older. The caller should do a null check if they want to maintain backward 96 | * compatibility. 97 | */ 98 | scc_settings_disable_error_popup *settings_disable_error_popup; 99 | /** 100 | * Runs the compiler with the given settings. 101 | */ 102 | scc_compile *compile; 103 | /** 104 | * Frees the memory allocated for the result. 105 | */ 106 | scc_free_result *free_result; 107 | /** 108 | * Returns the output for a successful compilation and NULL otherwise. 109 | */ 110 | scc_get_success *get_success; 111 | /** 112 | * Copies the error message to the given buffer. The message is truncated if 113 | * the buffer is too small. 114 | */ 115 | scc_copy_error *copy_error; 116 | /** 117 | * Returns the source reference at the given index. 118 | */ 119 | scc_output_get_source_ref *output_get_source_ref; 120 | /** 121 | * Returns the total number of source references in the output. 122 | */ 123 | scc_output_source_ref_count *output_source_ref_count; 124 | /** 125 | * Returns the type of entity behind the reference. 126 | */ 127 | scc_source_ref_type *source_ref_type; 128 | /** 129 | * Returns whether the entity behind the reference is native. 130 | */ 131 | scc_source_ref_is_native *source_ref_is_native; 132 | /** 133 | * Returns the name of the entity behind the reference. 134 | */ 135 | scc_source_ref_name *source_ref_name; 136 | /** 137 | * Returns the name of the parent of the entity behind the reference. 138 | */ 139 | scc_source_ref_parent_name *source_ref_parent_name; 140 | /** 141 | * Returns the path to a file where the entity behind the reference is 142 | * defined. 143 | */ 144 | scc_source_ref_path *source_ref_path; 145 | /** 146 | * Returns the line in the source code where the entity behind the reference 147 | * is defined. 148 | */ 149 | scc_source_ref_line *source_ref_line; 150 | } SccApi; 151 | 152 | #if defined(_WIN32) && !defined(BINDING_TEST) 153 | 154 | inline SccApi scc_load_api(HMODULE module) { 155 | SccApi api = { 156 | (scc_settings_new *)GetProcAddress(module, "scc_settings_new"), 157 | (scc_settings_set_custom_cache_file *)GetProcAddress( 158 | module, "scc_settings_set_custom_cache_file"), 159 | (scc_settings_set_output_cache_file *)GetProcAddress( 160 | module, "scc_settings_set_output_cache_file"), 161 | (scc_settings_add_script_path *)GetProcAddress( 162 | module, "scc_settings_add_script_path"), 163 | (scc_settings_disable_error_popup *)GetProcAddress( 164 | module, "scc_settings_disable_error_popup"), 165 | (scc_compile *)GetProcAddress(module, "scc_compile"), 166 | (scc_free_result *)GetProcAddress(module, "scc_free_result"), 167 | (scc_get_success *)GetProcAddress(module, "scc_get_success"), 168 | (scc_copy_error *)GetProcAddress(module, "scc_copy_error"), 169 | (scc_output_get_source_ref *)GetProcAddress(module, 170 | "scc_output_get_source_ref"), 171 | (scc_output_source_ref_count *)GetProcAddress( 172 | module, "scc_output_source_ref_count"), 173 | (scc_source_ref_type *)GetProcAddress(module, "scc_source_ref_type"), 174 | (scc_source_ref_is_native *)GetProcAddress(module, 175 | "scc_source_ref_is_native"), 176 | (scc_source_ref_name *)GetProcAddress(module, "scc_source_ref_name"), 177 | (scc_source_ref_parent_name *)GetProcAddress( 178 | module, "scc_source_ref_parent_name"), 179 | (scc_source_ref_path *)GetProcAddress(module, "scc_source_ref_path"), 180 | (scc_source_ref_line *)GetProcAddress(module, "scc_source_ref_line"), 181 | }; 182 | return api; 183 | } 184 | 185 | #endif 186 | 187 | #endif /* SCC_H */ 188 | -------------------------------------------------------------------------------- /crates/scc/capi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::marker::PhantomData; 3 | use std::path::{Path, PathBuf}; 4 | use std::ptr; 5 | 6 | use mimalloc::MiMalloc; 7 | use scc_shared::{SccOutput, SccSettings, SourceRef, SourceRefType}; 8 | 9 | #[global_allocator] 10 | static GLOBAL: MiMalloc = MiMalloc; 11 | 12 | /// # Safety 13 | /// The caller must ensure that `r6_dir` is a valid null-terminated UTF-8 string. 14 | #[unsafe(no_mangle)] 15 | pub unsafe extern "C" fn scc_settings_new(r6_dir: *const i8) -> Box { 16 | let r6_dir = unsafe { c_path(r6_dir) }; 17 | let root_dir = r6_dir.parent().map(Path::to_owned).unwrap_or(r6_dir); 18 | Box::new(SccSettings::new(root_dir, None, None, vec![], true)) 19 | } 20 | 21 | /// # Safety 22 | /// The caller must ensure that `settings` is a valid pointer to a `SccSettings` struct and 23 | /// `path` is a valid null-terminated UTF-8 string. 24 | #[unsafe(no_mangle)] 25 | pub unsafe extern "C" fn scc_settings_set_custom_cache_file( 26 | settings: &mut SccSettings, 27 | path: *const i8, 28 | ) { 29 | settings.set_custom_cache_file(unsafe { c_path(path) }); 30 | } 31 | 32 | /// # Safety 33 | /// The caller must ensure that `settings` is a valid pointer to a `SccSettings` struct and 34 | /// `path` is a valid null-terminated UTF-8 string. 35 | #[unsafe(no_mangle)] 36 | pub unsafe extern "C" fn scc_settings_set_output_cache_file( 37 | settings: &mut SccSettings, 38 | path: *const i8, 39 | ) { 40 | settings.set_output_cache_file(unsafe { c_path(path) }); 41 | } 42 | 43 | /// # Safety 44 | /// The caller must ensure that `settings` is a valid pointer to a `SccSettings` struct and 45 | /// `path` is a valid null-terminated UTF-8 string. 46 | #[unsafe(no_mangle)] 47 | pub unsafe extern "C" fn scc_settings_add_script_path(settings: &mut SccSettings, path: *const i8) { 48 | settings.add_script_path(unsafe { c_path(path) }); 49 | } 50 | 51 | #[unsafe(no_mangle)] 52 | pub extern "C" fn scc_settings_disable_error_popup(settings: &mut SccSettings) { 53 | settings.set_show_error_report(false); 54 | } 55 | 56 | #[unsafe(no_mangle)] 57 | pub extern "C" fn scc_compile(settings: Box) -> Box { 58 | let res = match scc_shared::compile(&settings) { 59 | Ok(output) => SccResult::Success(Box::new(output)), 60 | Err(err) => SccResult::Error(err), 61 | }; 62 | Box::new(res) 63 | } 64 | 65 | #[unsafe(no_mangle)] 66 | pub extern "C" fn scc_free_result(_: Box) {} 67 | 68 | #[unsafe(no_mangle)] 69 | pub extern "C" fn scc_get_success(output: &SccResult) -> Option<&SccOutput> { 70 | match output { 71 | SccResult::Success(success) => Some(success), 72 | SccResult::Error(_) => None, 73 | } 74 | } 75 | 76 | /// # Safety 77 | /// The caller must ensure that `buffer` is a valid pointer to a buffer of at least `buffer_size` 78 | /// bytes. 79 | #[unsafe(no_mangle)] 80 | pub unsafe extern "C" fn scc_copy_error( 81 | output: &SccResult, 82 | buffer: *mut u8, 83 | buffer_size: usize, 84 | ) -> usize { 85 | match output { 86 | SccResult::Success(_) => 0, 87 | SccResult::Error(error) => { 88 | let error = error.to_string(); 89 | let max_len = error.len().min(buffer_size - 1); 90 | unsafe { 91 | ptr::copy_nonoverlapping(error.as_ptr(), buffer, max_len); 92 | buffer.add(max_len).write(0); 93 | } 94 | max_len 95 | } 96 | } 97 | } 98 | 99 | #[unsafe(no_mangle)] 100 | pub extern "C" fn scc_output_get_source_ref(output: &SccOutput, i: usize) -> *const SourceRef { 101 | &output.refs()[i] 102 | } 103 | 104 | #[unsafe(no_mangle)] 105 | pub extern "C" fn scc_output_source_ref_count(output: &SccOutput) -> usize { 106 | output.refs().len() 107 | } 108 | 109 | #[unsafe(no_mangle)] 110 | pub extern "C" fn scc_source_ref_type(_output: &SccOutput, link: &SourceRef) -> SourceRefType { 111 | link.type_() 112 | } 113 | 114 | #[unsafe(no_mangle)] 115 | pub extern "C" fn scc_source_ref_is_native(_output: &SccOutput, _link: &SourceRef) -> bool { 116 | true 117 | } 118 | 119 | #[unsafe(no_mangle)] 120 | pub extern "C" fn scc_source_ref_name<'a>( 121 | output: &'a SccOutput, 122 | link: &'a SourceRef, 123 | ) -> StrWithLen<'a> { 124 | output.name(link).unwrap_or_default().into() 125 | } 126 | 127 | #[unsafe(no_mangle)] 128 | pub extern "C" fn scc_source_ref_parent_name<'a>( 129 | output: &'a SccOutput, 130 | link: &SourceRef, 131 | ) -> StrWithLen<'a> { 132 | output.parent_name(link).unwrap_or_default().into() 133 | } 134 | 135 | #[unsafe(no_mangle)] 136 | pub extern "C" fn scc_source_ref_path<'a>( 137 | output: &'a SccOutput, 138 | link: &SourceRef, 139 | ) -> StrWithLen<'a> { 140 | output 141 | .path(link) 142 | .and_then(|path| path.to_str()) 143 | .unwrap_or_default() 144 | .into() 145 | } 146 | 147 | #[unsafe(no_mangle)] 148 | pub extern "C" fn scc_source_ref_line(output: &SccOutput, link: &SourceRef) -> usize { 149 | output.line(link).unwrap_or(0) 150 | } 151 | 152 | unsafe fn c_path(r6_dir: *const i8) -> PathBuf { 153 | let cstr = unsafe { CStr::from_ptr(r6_dir) }; 154 | PathBuf::from(cstr.to_string_lossy().as_ref()) 155 | } 156 | 157 | pub enum SccResult { 158 | Success(Box), 159 | Error(anyhow::Error), 160 | } 161 | 162 | #[derive(Debug)] 163 | #[repr(C)] 164 | pub struct StrWithLen<'a> { 165 | ptr: *const u8, 166 | len: usize, 167 | _marker: PhantomData<&'a str>, 168 | } 169 | 170 | impl<'a> From<&'a str> for StrWithLen<'a> { 171 | fn from(value: &str) -> Self { 172 | Self { 173 | ptr: value.as_ptr(), 174 | len: value.len(), 175 | _marker: PhantomData, 176 | } 177 | } 178 | } 179 | 180 | impl Default for StrWithLen<'_> { 181 | fn default() -> Self { 182 | Self { 183 | ptr: std::ptr::null(), 184 | len: 0, 185 | _marker: PhantomData, 186 | } 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | 194 | #[test] 195 | fn str_with_len_has_correct_layout() { 196 | let mut data = [0u8; 16]; 197 | unsafe { 198 | data.as_mut_ptr() 199 | .cast::<*const u8>() 200 | .write(b"test".as_ptr()); 201 | data.as_mut_ptr().add(8).cast::().write(4); 202 | let str = data.as_ptr().cast::>().read(); 203 | assert_eq!(std::slice::from_raw_parts(str.ptr, str.len), b"test"); 204 | } 205 | } 206 | 207 | #[test] 208 | fn source_ref_type_is_stable() { 209 | assert_eq!(SourceRefType::Undefined as u8, 0); 210 | assert_eq!(SourceRefType::Class as u8, 1); 211 | assert_eq!(SourceRefType::Field as u8, 2); 212 | assert_eq!(SourceRefType::Function as u8, 3); 213 | assert_eq!(SourceRefType::Enum as u8, 4); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /crates/scc/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scc" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | anyhow.workspace = true 9 | chumsky.workspace = true 10 | minidl.workspace = true 11 | 12 | [dev-dependencies] 13 | similar-asserts.workspace = true 14 | 15 | [build-dependencies] 16 | bindgen.workspace = true 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /crates/scc/cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | println!("cargo:rerun-if-changed=../capi/include/scc.h"); 6 | 7 | let bindings = bindgen::Builder::default() 8 | .header("../capi/include/scc.h") 9 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 10 | .clang_arg("-D BINDING_TEST") 11 | .generate() 12 | .expect("Unable to generate bindings"); 13 | 14 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 15 | bindings 16 | .write_to_file(out_path.join("bindings.rs")) 17 | .expect("Couldn't write bindings!"); 18 | } 19 | -------------------------------------------------------------------------------- /crates/scc/cli/src/capi.rs: -------------------------------------------------------------------------------- 1 | use minidl::Library; 2 | 3 | use crate::raw; 4 | 5 | pub(super) fn load() -> anyhow::Result { 6 | let lib = Library::load("scc_lib.dll")?; 7 | Ok(unsafe { 8 | raw::SccApi { 9 | settings_new: lib.sym("scc_settings_new\0")?, 10 | settings_set_custom_cache_file: lib.sym("scc_settings_set_custom_cache_file\0")?, 11 | settings_add_script_path: lib.sym("scc_settings_add_script_path\0")?, 12 | settings_set_output_cache_file: lib.sym("scc_settings_set_output_cache_file\0")?, 13 | settings_disable_error_popup: lib.sym("scc_settings_disable_error_popup\0")?, 14 | compile: lib.sym("scc_compile\0")?, 15 | free_result: lib.sym("scc_free_result\0")?, 16 | get_success: lib.sym("scc_get_success\0")?, 17 | copy_error: lib.sym("scc_copy_error\0")?, 18 | output_get_source_ref: lib.sym("scc_output_get_source_ref\0")?, 19 | output_source_ref_count: lib.sym("scc_output_source_ref_count\0")?, 20 | source_ref_type: lib.sym("scc_source_ref_type\0")?, 21 | source_ref_is_native: lib.sym("scc_source_ref_is_native\0")?, 22 | source_ref_name: lib.sym("scc_source_ref_name\0")?, 23 | source_ref_parent_name: lib.sym("scc_source_ref_parent_name\0")?, 24 | source_ref_path: lib.sym("scc_source_ref_path\0")?, 25 | source_ref_line: lib.sym("scc_source_ref_line\0")?, 26 | } 27 | }) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use std::mem; 33 | 34 | use raw::*; 35 | 36 | use super::*; 37 | 38 | // this test just ensures that the API types haven't changed 39 | // existing function signatures are not allowed to change due to backwards compatibility 40 | #[test] 41 | fn api_functions_are_stable() { 42 | let api = SccApi { 43 | settings_new: None, 44 | settings_set_custom_cache_file: None, 45 | settings_set_output_cache_file: None, 46 | settings_add_script_path: None, 47 | settings_disable_error_popup: None, 48 | compile: None, 49 | free_result: None, 50 | get_success: None, 51 | copy_error: None, 52 | output_get_source_ref: None, 53 | output_source_ref_count: None, 54 | source_ref_type: None, 55 | source_ref_is_native: None, 56 | source_ref_name: None, 57 | source_ref_parent_name: None, 58 | source_ref_path: None, 59 | source_ref_line: None, 60 | }; 61 | 62 | let _settings_new: Option *mut SccSettings> = 63 | api.settings_new; 64 | let _settings_set_custom_cache_file: Option< 65 | unsafe extern "C" fn(*mut SccSettings, *const i8), 66 | > = api.settings_set_custom_cache_file; 67 | let _settings_set_output_cache_file: Option< 68 | unsafe extern "C" fn(*mut SccSettings, *const i8), 69 | > = api.settings_set_output_cache_file; 70 | let _settings_add_script_path: Option = 71 | api.settings_add_script_path; 72 | let _settings_disable_error_popup: Option = 73 | api.settings_disable_error_popup; 74 | let _compile: Option *mut SccResult> = 75 | api.compile; 76 | let _free_result: Option = api.free_result; 77 | let _get_success: Option *mut SccOutput> = 78 | api.get_success; 79 | let _copy_error: Option usize> = 80 | api.copy_error; 81 | let _output_get_source_ref: Option< 82 | unsafe extern "C" fn(*mut SccOutput, usize) -> *mut SccSourceRef, 83 | > = api.output_get_source_ref; 84 | let _output_source_ref_count: Option usize> = 85 | api.output_source_ref_count; 86 | let _source_ref_type: Option< 87 | unsafe extern "C" fn(*mut SccOutput, *mut SccSourceRef) -> u8, 88 | > = api.source_ref_type; 89 | let _source_ref_is_native: Option< 90 | unsafe extern "C" fn(*mut SccOutput, *mut SccSourceRef) -> bool, 91 | > = api.source_ref_is_native; 92 | let _source_ref_name: Option< 93 | unsafe extern "C" fn(*mut SccOutput, *mut SccSourceRef) -> StrWithLen, 94 | > = api.source_ref_name; 95 | let _source_ref_parent_name: Option< 96 | unsafe extern "C" fn(*mut SccOutput, *mut SccSourceRef) -> StrWithLen, 97 | > = api.source_ref_parent_name; 98 | let _source_ref_path: Option< 99 | unsafe extern "C" fn(*mut SccOutput, *mut SccSourceRef) -> StrWithLen, 100 | > = api.source_ref_path; 101 | let _source_ref_line: Option< 102 | unsafe extern "C" fn(*mut SccOutput, *mut SccSourceRef) -> usize, 103 | > = api.source_ref_line; 104 | } 105 | 106 | #[test] 107 | fn api_types_are_stable() { 108 | assert_eq!(mem::size_of::(), 16); 109 | assert_eq!(mem::offset_of!(StrWithLen, str_), 0); 110 | assert_eq!(mem::offset_of!(StrWithLen, len), 8); 111 | 112 | assert_eq!(SccSourceRefType_SCC_SOURCE_REF_TYPE_UNDEFINED, 0); 113 | assert_eq!(SccSourceRefType_SCC_SOURCE_REF_TYPE_CLASS, 1); 114 | assert_eq!(SccSourceRefType_SCC_SOURCE_REF_TYPE_FIELD, 2); 115 | assert_eq!(SccSourceRefType_SCC_SOURCE_REF_TYPE_FUNCTION, 3); 116 | assert_eq!(SccSourceRefType_SCC_SOURCE_REF_TYPE_ENUM, 4); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /crates/scc/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::CString; 3 | use std::fs::File; 4 | use std::io::{self, BufRead}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use anyhow::{Context, anyhow}; 8 | use arguments::Arguments; 9 | 10 | mod arguments; 11 | mod capi; 12 | mod raw; 13 | 14 | fn main() -> anyhow::Result<()> { 15 | let (r6_dir, args) = match Arguments::from_env_args(std::env::args()) { 16 | Ok(args) => { 17 | let r6_dir = args 18 | .scripts_dir 19 | .parent() 20 | .context("provided scripts directory has no parent directory")? 21 | .to_owned(); 22 | (r6_dir, Some(args)) 23 | } 24 | Err(err) => { 25 | let current_dir = env::current_dir()?; 26 | let r6_dir = current_dir 27 | .parent() 28 | .and_then(Path::parent) 29 | .map(|path| path.join("r6")) 30 | .filter(|path| path.exists()) 31 | .ok_or_else(|| anyhow!("could not run scc: {err}"))?; 32 | (r6_dir, None) 33 | } 34 | }; 35 | run(&r6_dir, args) 36 | } 37 | 38 | fn run(r6_dir: &Path, args: Option) -> anyhow::Result<()> { 39 | let api = capi::load()?; 40 | 41 | let settings_new = api.settings_new.context("missing 'settings_new'")?; 42 | let set_custom_cache_file = api 43 | .settings_set_custom_cache_file 44 | .context("missing 'settings_set_custom_cache_file'")?; 45 | let add_script_path = api 46 | .settings_add_script_path 47 | .context("missing 'settings_add_script_path'")?; 48 | let compile = api.compile.context("missing 'compile'")?; 49 | let free_result = api.free_result.context("missing 'free_result'")?; 50 | 51 | let root = c_path(r6_dir)?; 52 | 53 | unsafe { 54 | let settings = settings_new(root.as_ptr()); 55 | 56 | if let Some(args) = args { 57 | if let Some(cache_file) = args.cache_file { 58 | let cache_file = c_path(&cache_file)?; 59 | set_custom_cache_file(settings, cache_file.as_ptr()); 60 | } 61 | 62 | if let Some(script_paths_file) = args.script_paths_file { 63 | for script_path in io::BufReader::new(File::open(script_paths_file)?) 64 | .lines() 65 | .map(|line| Ok(PathBuf::from(line?))) 66 | .collect::>>()? 67 | { 68 | let script_path = c_path(&script_path)?; 69 | add_script_path(settings, script_path.as_ptr()); 70 | } 71 | } 72 | } 73 | 74 | let res = compile(settings); 75 | free_result(res); 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | fn c_path(path: &Path) -> anyhow::Result { 82 | Ok(CString::new(path.to_string_lossy().as_ref())?) 83 | } 84 | -------------------------------------------------------------------------------- /crates/scc/cli/src/raw.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(dead_code)] 5 | #![allow(clippy::unreadable_literal)] 6 | 7 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 8 | -------------------------------------------------------------------------------- /crates/scc/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scc-shared" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-compiler-api = { workspace = true, features = ["ignore"] } 9 | 10 | log.workspace = true 11 | thiserror.workspace = true 12 | anyhow.workspace = true 13 | chrono.workspace = true 14 | hashbrown = { workspace = true, features = ["serde"] } 15 | serde = { workspace = true, features = ["derive"] } 16 | toml = { workspace = true, features = ["parse"] } 17 | flexi_logger.workspace = true 18 | vmap.workspace = true 19 | file-id.workspace = true 20 | fd-lock.workspace = true 21 | msgbox.workspace = true 22 | 23 | [dev-dependencies] 24 | similar-asserts.workspace = true 25 | 26 | [lints] 27 | workspace = true 28 | -------------------------------------------------------------------------------- /crates/scc/shared/src/hints.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::fs; 3 | use std::path::Path; 4 | 5 | use anyhow::Context; 6 | use hashbrown::HashMap; 7 | use serde::Deserialize; 8 | 9 | #[derive(Debug, Deserialize)] 10 | pub struct UserHint { 11 | id: String, 12 | message: String, 13 | file_name: Option, 14 | span_starts_with: Option, 15 | line_contains: Option, 16 | } 17 | 18 | impl UserHint { 19 | pub fn id(&self) -> &str { 20 | &self.id 21 | } 22 | 23 | pub fn message(&self) -> &str { 24 | &self.message 25 | } 26 | 27 | pub fn does_match(&self, file_name: Option<&OsStr>, source: &str, source_line: &str) -> bool { 28 | self.file_name 29 | .as_ref() 30 | .is_none_or(|p| Some(p.as_os_str()) == file_name) 31 | && (self 32 | .span_starts_with 33 | .as_deref() 34 | .is_some_and(|str| source.starts_with(str)) 35 | || self 36 | .line_contains 37 | .as_deref() 38 | .is_some_and(|str| source_line.contains(str))) 39 | } 40 | } 41 | 42 | #[derive(Debug, Default)] 43 | pub struct UserHints { 44 | hints: HashMap>, 45 | } 46 | 47 | impl UserHints { 48 | pub fn load(path: impl AsRef) -> anyhow::Result { 49 | let mut hints = HashMap::new(); 50 | if !path.as_ref().exists() { 51 | return Ok(Self { hints }); 52 | } 53 | 54 | let dir = fs::read_dir(path).context("Failed to read the hints directory")?; 55 | for entry in dir { 56 | let entry = entry.context("Failed to read a hint directory entry")?; 57 | if entry.path().extension() == Some(OsStr::new("toml")) { 58 | let contents = 59 | fs::read_to_string(entry.path()).context("Failed to read a hint file")?; 60 | let contents: HashMap> = 61 | toml::from_str(&contents).context("Failed to parse a hint file")?; 62 | for (key, val) in contents { 63 | hints.entry(key).or_default().extend(val); 64 | } 65 | } 66 | } 67 | Ok(Self { hints }) 68 | } 69 | 70 | #[inline] 71 | pub fn get_by_code(&self, error_code: &str) -> Option<&[UserHint]> { 72 | self.hints.get(error_code).map(Vec::as_slice) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/scc/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::{env, fs, io}; 3 | 4 | use anyhow::Context; 5 | use fd_lock::RwLock; 6 | use output::extract_refs; 7 | pub use output::{SccOutput, SourceRef, SourceRefType}; 8 | use redscript_compiler_api::ast::SourceMap; 9 | use redscript_compiler_api::{Compilation, FlushError, SaveError, SourceMapExt, TypeInterner}; 10 | use report::{CompilationFailure, ErrorReport}; 11 | pub use settings::SccSettings; 12 | use settings::{BACKUP_FILE_EXT, TIMESTAMP_FILE_EXT}; 13 | use timestamp::CompileTimestamp; 14 | use vmap::Map; 15 | 16 | mod hints; 17 | mod logger; 18 | mod output; 19 | mod report; 20 | mod settings; 21 | mod timestamp; 22 | 23 | pub fn compile(settings: &SccSettings) -> anyhow::Result { 24 | logger::setup(settings.root_dir()); 25 | 26 | match compile_inner(settings) { 27 | Ok(output) => { 28 | log::info!("Compilation successful"); 29 | Ok(output) 30 | } 31 | Err(err) => { 32 | log::error!("Compilation failed: {err}"); 33 | if settings.should_show_error_report() { 34 | let report = ErrorReport::new(&err).to_string(); 35 | msgbox::create("REDscript error", &report, msgbox::IconType::Error).ok(); 36 | } 37 | Err(err) 38 | } 39 | } 40 | } 41 | 42 | fn compile_inner(settings: &SccSettings) -> anyhow::Result { 43 | log::info!("Running REDscript {}", env!("CARGO_PKG_VERSION")); 44 | 45 | let cache_file = settings.cache_file_path(); 46 | 47 | let ts_path = cache_file.with_extension(TIMESTAMP_FILE_EXT); 48 | let mut ts_lock = RwLock::new( 49 | fs::OpenOptions::new() 50 | .read(true) 51 | .write(true) 52 | .create(true) 53 | .truncate(false) 54 | .open(&ts_path) 55 | .context("failed to open the timestamp file")?, 56 | ); 57 | let mut ts_file = ts_lock 58 | .write() 59 | .context("failed to acquire a write lock on the timestamp file")?; 60 | 61 | let input_file = prepare_input_cache(settings, &mut ts_file)?; 62 | let output_file = settings.output_cache_file_path(); 63 | 64 | let sources = SourceMap::from_paths_recursively(settings.script_paths())?; 65 | log::info!( 66 | "Compiling {} files:\n{}", 67 | sources.len(), 68 | sources.display_at(settings.root_dir()) 69 | ); 70 | sources.populate_boot_lib(); 71 | 72 | let interner = TypeInterner::default(); 73 | let (mmap, _f) = Map::with_options().open(&input_file)?; 74 | let refs = { 75 | match Compilation::new_with(&mmap, &sources, &interner, &[])?.flush(&output_file) { 76 | Err(FlushError::Write(SaveError::Mmap(err))) 77 | if err.kind() == io::ErrorKind::PermissionDenied => 78 | { 79 | anyhow::bail!( 80 | "could not write to '{}', make sure the file is not read-only", 81 | output_file.display() 82 | ); 83 | } 84 | Err(FlushError::CompilationErrors(diagnostics)) => { 85 | diagnostics.dump(&sources)?; 86 | 87 | return Err(CompilationFailure::new(diagnostics, &sources, settings)?.into()); 88 | } 89 | Err(err) => anyhow::bail!("{err}"), 90 | Ok((syms, diagnostics)) => { 91 | diagnostics.dump(&sources)?; 92 | 93 | log::info!("Succesfully written '{}'", output_file.display()); 94 | extract_refs(&syms, &interner) 95 | } 96 | } 97 | }; 98 | 99 | CompileTimestamp::try_from(&output_file.metadata()?)?.write(&mut *ts_file)?; 100 | 101 | Ok(SccOutput::new(sources, interner, refs)) 102 | } 103 | 104 | fn prepare_input_cache(settings: &SccSettings, ts_file: &mut fs::File) -> anyhow::Result { 105 | let cache_file = settings.cache_file_path(); 106 | if !cache_file.exists() { 107 | let cache_dir = cache_file 108 | .parent() 109 | .context("provided cache file path has no parent directory")?; 110 | fs::create_dir_all(cache_dir).context("failed to create the cache directory")?; 111 | 112 | let base = settings.unmodified_cache_file_path(); 113 | fs::copy(&base, &cache_file).context("could not copy the base cache file")?; 114 | } 115 | 116 | let backup_file = cache_file.with_extension(BACKUP_FILE_EXT); 117 | if !backup_file.exists() { 118 | let base = settings.default_backup_cache_file_path(); 119 | if base.exists() { 120 | log::info!("Re-initializing the backup file from {}", base.display()); 121 | 122 | fs::copy(&base, &backup_file) 123 | .context("could not copy the base redscript backup file")?; 124 | } 125 | } 126 | 127 | let write_ts = cache_file 128 | .metadata() 129 | .and_then(|meta| CompileTimestamp::try_from(&meta)) 130 | .context("failed to obtain a timestamp of the cache file")?; 131 | let saved_ts = CompileTimestamp::read(&mut *ts_file) 132 | .context("failed to read the existing timestamp file")?; 133 | 134 | if settings 135 | .does_have_separate_output_cache() 136 | .context("failed to check for output cache")? 137 | { 138 | match saved_ts { 139 | saved_ts if saved_ts != Some(write_ts) && backup_file.exists() => { 140 | log::info!("Removing a stale backup file at {}", backup_file.display()); 141 | 142 | fs::remove_file(&backup_file).context("failed to remove a stale backup file")?; 143 | } 144 | _ if backup_file.exists() => { 145 | log::info!("Restoring the backup file to {}", cache_file.display()); 146 | 147 | fs::rename(&backup_file, &cache_file) 148 | .context("failed to restore the backup file")?; 149 | } 150 | _ => {} 151 | } 152 | Ok(cache_file.into_owned()) 153 | } else { 154 | match saved_ts { 155 | None if backup_file.exists() => { 156 | log::info!("Previous cache backup file found"); 157 | } 158 | saved_ts if saved_ts != Some(write_ts) => { 159 | log::info!( 160 | "The final.redscripts file is not ours, copying it to {}", 161 | backup_file.display() 162 | ); 163 | 164 | fs::copy(&cache_file, &backup_file).context("failed to copy the cache file")?; 165 | } 166 | Some(_) if !backup_file.exists() => { 167 | anyhow::bail!( 168 | "a compilation timestamp was found, but backup files are missing, your \ 169 | installation might be corrupted, you should remove the contents of 'r6/cache/' 170 | directory and verify game files with Steam/GOG" 171 | ); 172 | } 173 | _ => {} 174 | } 175 | Ok(backup_file) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /crates/scc/shared/src/logger.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use chrono::SecondsFormat; 4 | use flexi_logger::{Age, Cleanup, Criterion, Duplicate, FileSpec, LogSpecBuilder, Logger, Naming}; 5 | use log::LevelFilter; 6 | 7 | pub fn setup(root_dir: &Path) { 8 | let file = FileSpec::default() 9 | .directory(root_dir.join("r6").join("logs")) 10 | .basename("redscript"); 11 | 12 | Logger::with(LogSpecBuilder::new().default(LevelFilter::Info).build()) 13 | .log_to_file(file) 14 | .duplicate_to_stdout(Duplicate::All) 15 | .rotate( 16 | Criterion::Age(Age::Day), 17 | Naming::Timestamps, 18 | Cleanup::KeepLogFiles(4), 19 | ) 20 | .format(|out, time, msg| { 21 | let time = time 22 | .now() 23 | .with_timezone(&chrono::Utc) 24 | .to_rfc3339_opts(SecondsFormat::Secs, true); 25 | write!(out, "[{} - {time}] {}", msg.level(), msg.args()) 26 | }) 27 | .start() 28 | .ok(); 29 | } 30 | -------------------------------------------------------------------------------- /crates/scc/shared/src/output.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use redscript_compiler_api::ast::{self, SourceMap, Span}; 4 | use redscript_compiler_api::{Symbols, TypeIndex, TypeInterner}; 5 | 6 | pub struct SccOutput { 7 | sources: SourceMap, 8 | interner: TypeInterner, 9 | refs: Vec, 10 | } 11 | 12 | impl SccOutput { 13 | pub fn new(sources: SourceMap, interner: TypeInterner, refs: Vec) -> Self { 14 | Self { 15 | sources, 16 | interner, 17 | refs, 18 | } 19 | } 20 | 21 | #[inline] 22 | pub fn refs(&self) -> &[SourceRef] { 23 | &self.refs 24 | } 25 | 26 | pub fn name<'a>(&'a self, ref_: &'a SourceRef) -> Option<&'a str> { 27 | match ref_ { 28 | SourceRef::Type(id, _) => Some(self.interner.get_index(*id)?.as_str()), 29 | SourceRef::Function(name, _) 30 | | SourceRef::Method(_, name, _) 31 | | SourceRef::Field(_, name, _) => Some(name), 32 | } 33 | } 34 | 35 | pub fn parent_name<'a>(&'a self, ref_: &SourceRef) -> Option<&'a str> { 36 | match ref_ { 37 | SourceRef::Method(id, _, _) | SourceRef::Field(id, _, _) => { 38 | Some(self.interner.get_index(*id)?.as_str()) 39 | } 40 | _ => None, 41 | } 42 | } 43 | 44 | pub fn path<'a>(&'a self, ref_: &SourceRef) -> Option<&'a Path> { 45 | let span = ref_.span(); 46 | self.sources.get(span.file).map(ast::File::path) 47 | } 48 | 49 | pub fn line(&self, ref_: &SourceRef) -> Option { 50 | let span = ref_.span(); 51 | self.sources 52 | .get(span.file) 53 | .map(|f| f.line_and_offset(span.start).0) 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | pub enum SourceRef { 59 | Type(TypeIndex, Span), 60 | Function(String, Span), 61 | Method(TypeIndex, String, Span), 62 | Field(TypeIndex, String, Span), 63 | } 64 | 65 | impl SourceRef { 66 | pub fn type_(&self) -> SourceRefType { 67 | match self { 68 | SourceRef::Type(_, _) => SourceRefType::Class, 69 | SourceRef::Function(_, _) | SourceRef::Method(_, _, _) => SourceRefType::Function, 70 | SourceRef::Field(_, _, _) => SourceRefType::Field, 71 | } 72 | } 73 | 74 | pub fn span(&self) -> Span { 75 | match self { 76 | SourceRef::Type(_, span) 77 | | SourceRef::Function(_, span) 78 | | SourceRef::Method(_, _, span) 79 | | SourceRef::Field(_, _, span) => *span, 80 | } 81 | } 82 | } 83 | 84 | #[repr(u8)] 85 | pub enum SourceRefType { 86 | Undefined = 0, 87 | Class = 1, 88 | Field = 2, 89 | Function = 3, 90 | Enum = 4, 91 | } 92 | 93 | pub(super) fn extract_refs(syms: &Symbols<'_>, interner: &TypeInterner) -> Vec { 94 | let mut refs = Vec::new(); 95 | 96 | for (id, def) in syms.types() { 97 | let Some(id) = interner.get_index_of(id) else { 98 | continue; 99 | }; 100 | let Some(agg) = def.schema().as_aggregate() else { 101 | continue; 102 | }; 103 | if !agg.flags().is_native() { 104 | continue; 105 | } 106 | let Some(span) = agg.span() else { continue }; 107 | refs.push(SourceRef::Type(id, span)); 108 | 109 | for entry in agg.methods().iter() { 110 | let Some(span) = entry.func().span() else { 111 | continue; 112 | }; 113 | refs.push(SourceRef::Method(id, (**entry.name()).into(), span)); 114 | } 115 | for field in agg.fields().iter() { 116 | let Some(span) = field.field().span() else { 117 | continue; 118 | }; 119 | refs.push(SourceRef::Field(id, field.name().into(), span)); 120 | } 121 | } 122 | 123 | for entry in syms.free_functions() { 124 | let Some(span) = entry.func().span() else { 125 | continue; 126 | }; 127 | if !entry.func().flags().is_native() { 128 | continue; 129 | } 130 | refs.push(SourceRef::Function(entry.name().to_string(), span)); 131 | } 132 | 133 | refs 134 | } 135 | -------------------------------------------------------------------------------- /crates/scc/shared/src/report.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::path::PathBuf; 3 | 4 | use hashbrown::{HashMap, HashSet}; 5 | use redscript_compiler_api::ast::SourceMap; 6 | use redscript_compiler_api::{Diagnostic, Diagnostics}; 7 | use thiserror::Error; 8 | 9 | use crate::SccSettings; 10 | use crate::hints::UserHints; 11 | 12 | #[derive(Debug)] 13 | pub struct ErrorReport<'ctx> { 14 | cause: &'ctx anyhow::Error, 15 | } 16 | 17 | impl<'ctx> ErrorReport<'ctx> { 18 | pub fn new(cause: &'ctx anyhow::Error) -> Self { 19 | Self { cause } 20 | } 21 | } 22 | 23 | impl fmt::Display for ErrorReport<'_> { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | writeln!(f, "REDscript compilation has failed.")?; 26 | writeln!(f)?; 27 | 28 | if let Some(source) = self.cause.downcast_ref::() { 29 | writeln!(f, "Fatal errors were found in the files listed below:")?; 30 | for file in &source.failing_files { 31 | writeln!(f, "{}", file.display())?; 32 | } 33 | writeln!(f)?; 34 | 35 | if !source.hints.is_empty() { 36 | writeln!( 37 | f, 38 | "One or more of the errors found has a known solution. Here are the \ 39 | recommended steps:" 40 | )?; 41 | for hint in &source.hints { 42 | writeln!(f, "- {}", hint)?; 43 | } 44 | } else { 45 | writeln!( 46 | f, 47 | "You should check if these mods are outdated and update them if possible. They may \ 48 | also be incompatible with the current version of the game, in which case you \ 49 | should remove them and try again." 50 | )?; 51 | } 52 | } else { 53 | writeln!(f, "Reason: {}", self.cause)?; 54 | } 55 | 56 | Ok(()) 57 | } 58 | } 59 | 60 | #[derive(Debug, Error)] 61 | #[error("fatal errors found")] 62 | pub struct CompilationFailure { 63 | failing_files: Vec, 64 | hints: Vec, 65 | } 66 | 67 | impl CompilationFailure { 68 | pub fn new( 69 | diagnostics: Diagnostics<'_>, 70 | sources: &SourceMap, 71 | settings: &SccSettings, 72 | ) -> anyhow::Result { 73 | let fatal = diagnostics 74 | .into_iter() 75 | .filter(Diagnostic::is_fatal) 76 | .collect::>(); 77 | 78 | let failing_files = fatal 79 | .iter() 80 | .map(|d| d.span().file) 81 | .collect::>() 82 | .into_iter() 83 | .filter_map(|f| { 84 | let path = sources.get(f)?.path(); 85 | let path = path.strip_prefix(settings.root_dir()).unwrap_or(path); 86 | Some(path.to_owned()) 87 | }) 88 | .collect(); 89 | 90 | let hints_config = UserHints::load(settings.user_hints_dir()).unwrap_or_else(|err| { 91 | log::warn!("Failed to parse one of the user hints TOML files: {}", err); 92 | UserHints::default() 93 | }); 94 | 95 | let mut hints = HashMap::new(); 96 | for hint in fatal.iter().filter_map(|d| { 97 | let candidates = hints_config.get_by_code(d.code())?; 98 | let span = d.span(); 99 | let file = sources.get(span.file)?; 100 | let source = file.span_contents(span); 101 | let (line, _) = file.line_and_offset(span.start); 102 | let line_source = file.line_contents(line)?; 103 | candidates 104 | .iter() 105 | .find(|hint| hint.does_match(file.path().file_name(), source, line_source)) 106 | }) { 107 | hints 108 | .entry(hint.id()) 109 | .or_insert_with(|| hint.message().to_owned()); 110 | } 111 | 112 | Ok(Self { 113 | failing_files, 114 | hints: hints.into_values().collect(), 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/scc/shared/src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::path::{Path, PathBuf}; 3 | use std::{io, iter}; 4 | 5 | use file_id::get_file_id; 6 | 7 | const BUNDLE_FILE: &str = "final.redscripts"; 8 | const BACKUP_BUNDLE_FILE: &str = "final.redscripts.bk"; 9 | 10 | const R6_DIR: &str = "r6"; 11 | const CACHE_DIR: &str = "cache"; 12 | const SCRIPTS_DIR: &str = "scripts"; 13 | const CONFIG_DIR: &str = "config"; 14 | const USER_HINTS_DIR: &str = "redsUserHints"; 15 | 16 | pub(crate) const CACHE_FILE_EXT: &str = "redscripts"; 17 | pub(crate) const BACKUP_FILE_EXT: &str = "redscripts.bk"; 18 | pub(crate) const TIMESTAMP_FILE_EXT: &str = "redscripts.ts"; 19 | 20 | #[derive(Debug)] 21 | #[repr(C)] 22 | pub struct SccSettings { 23 | root_dir: PathBuf, 24 | custom_cache_file: Option, 25 | output_cache_file: Option, 26 | additional_script_paths: Vec, 27 | show_error_report: bool, 28 | } 29 | 30 | impl SccSettings { 31 | pub fn new( 32 | root_dir: PathBuf, 33 | custom_cache_file: Option, 34 | output_cache_file: Option, 35 | additional_script_paths: Vec, 36 | show_error_report: bool, 37 | ) -> Self { 38 | Self { 39 | root_dir, 40 | custom_cache_file, 41 | output_cache_file, 42 | additional_script_paths, 43 | show_error_report, 44 | } 45 | } 46 | 47 | pub fn root_dir(&self) -> &Path { 48 | &self.root_dir 49 | } 50 | 51 | pub fn user_hints_dir(&self) -> PathBuf { 52 | self.root_dir 53 | .join(R6_DIR) 54 | .join(CONFIG_DIR) 55 | .join(USER_HINTS_DIR) 56 | } 57 | 58 | fn default_cache_file_path(&self) -> PathBuf { 59 | self.root_dir.join(R6_DIR).join(CACHE_DIR).join(BUNDLE_FILE) 60 | } 61 | 62 | fn default_scripts_dir_path(&self) -> PathBuf { 63 | self.root_dir.join(R6_DIR).join(SCRIPTS_DIR) 64 | } 65 | 66 | pub fn script_paths(&self) -> impl Iterator> { 67 | iter::once(self.default_scripts_dir_path().into()) 68 | .chain(self.additional_script_paths.iter().map(Into::into)) 69 | } 70 | 71 | pub fn cache_file_path(&self) -> Cow<'_, Path> { 72 | self.custom_cache_file 73 | .as_ref() 74 | .map_or_else(|| self.default_cache_file_path().into(), Into::into) 75 | } 76 | 77 | pub fn output_cache_file_path(&self) -> Cow<'_, Path> { 78 | self.output_cache_file 79 | .as_ref() 80 | .map_or_else(|| self.cache_file_path(), Into::into) 81 | } 82 | 83 | pub fn default_backup_cache_file_path(&self) -> PathBuf { 84 | self.root_dir 85 | .join(R6_DIR) 86 | .join(CACHE_DIR) 87 | .join(BACKUP_BUNDLE_FILE) 88 | } 89 | 90 | pub fn unmodified_cache_file_path(&self) -> PathBuf { 91 | let path = self.default_backup_cache_file_path(); 92 | if path.exists() { 93 | path 94 | } else { 95 | path.with_extension(CACHE_FILE_EXT) 96 | } 97 | } 98 | 99 | pub fn does_have_separate_output_cache(&self) -> io::Result { 100 | let Some(output) = self.output_cache_file.as_ref() else { 101 | return Ok(false); 102 | }; 103 | match (get_file_id(output), get_file_id(self.cache_file_path())) { 104 | (Ok(id1), Ok(id2)) => Ok(id1 != id2), 105 | (Err(err), Ok(_)) if err.kind() == io::ErrorKind::NotFound => Ok(true), 106 | (Err(err), _) | (_, Err(err)) => Err(err), 107 | } 108 | } 109 | 110 | pub fn should_show_error_report(&self) -> bool { 111 | self.show_error_report 112 | } 113 | 114 | pub fn set_custom_cache_file(&mut self, path: PathBuf) { 115 | self.custom_cache_file = Some(path); 116 | } 117 | 118 | pub fn set_output_cache_file(&mut self, path: PathBuf) { 119 | self.output_cache_file = Some(path); 120 | } 121 | 122 | pub fn set_show_error_report(&mut self, show: bool) { 123 | self.show_error_report = show; 124 | } 125 | 126 | pub fn add_script_path(&mut self, path: PathBuf) { 127 | self.additional_script_paths.push(path); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/scc/shared/src/timestamp.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | use std::{fs, io}; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 5 | pub struct CompileTimestamp { 6 | nanos: u128, 7 | } 8 | 9 | impl CompileTimestamp { 10 | pub fn read(input: &mut R) -> io::Result> { 11 | if input.seek(io::SeekFrom::End(0))? == 0 { 12 | return Ok(None); 13 | } 14 | input.rewind()?; 15 | 16 | let mut buf: [u8; 16] = [0; 16]; 17 | input.read_exact(&mut buf)?; 18 | 19 | let nanos = u128::from_le_bytes(buf); 20 | Ok(Some(Self { nanos })) 21 | } 22 | 23 | pub fn write(&self, output: &mut W) -> io::Result<()> { 24 | output.rewind()?; 25 | output.write_all(&self.nanos.to_le_bytes())?; 26 | Ok(()) 27 | } 28 | } 29 | 30 | impl TryFrom<&fs::Metadata> for CompileTimestamp { 31 | type Error = io::Error; 32 | 33 | fn try_from(metadata: &fs::Metadata) -> io::Result { 34 | let nanos = metadata 35 | .modified()? 36 | .duration_since(SystemTime::UNIX_EPOCH) 37 | .unwrap() 38 | .as_nanos(); 39 | Ok(Self { nanos }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/syntax/ast/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-ast" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | derive-where.workspace = true 9 | bitflags.workspace = true 10 | elsa.workspace = true 11 | chumsky = { workspace = true, optional = true } 12 | ignore = { workspace = true, optional = true } 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [features] 18 | chumsky = ["dep:chumsky"] 19 | ignore = ["dep:ignore"] 20 | testing = [] 21 | -------------------------------------------------------------------------------- /crates/syntax/ast/src/files.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::{fmt, fs, io}; 3 | 4 | use elsa::FrozenVec; 5 | 6 | use crate::Span; 7 | use crate::span::FileId; 8 | 9 | #[derive(Default)] 10 | pub struct SourceMap { 11 | files: StableDeque, 12 | } 13 | 14 | impl SourceMap { 15 | pub fn new() -> Self { 16 | Self::default() 17 | } 18 | 19 | pub fn from_files(it: impl IntoIterator>) -> io::Result { 20 | let files = Self::new(); 21 | for path in it { 22 | let path = path.into(); 23 | let source = fs::read_to_string(&path)?; 24 | files.push_back(path, source); 25 | } 26 | Ok(files) 27 | } 28 | 29 | #[cfg(feature = "ignore")] 30 | pub fn from_paths_recursively( 31 | it: impl IntoIterator>, 32 | ) -> io::Result { 33 | use std::ffi::OsStr; 34 | 35 | let it = it.into_iter().flat_map(|path| { 36 | ignore::WalkBuilder::new(path.into()) 37 | .follow_links(true) 38 | .build() 39 | .filter_map(Result::ok) 40 | .filter(|entry| entry.path().extension() == Some(OsStr::new("reds"))) 41 | .map(ignore::DirEntry::into_path) 42 | }); 43 | Self::from_files(it) 44 | } 45 | 46 | pub fn push_front(&self, path: impl Into, source: impl Into) -> FileId { 47 | FileId(self.files.push_front(File::new(path, source))) 48 | } 49 | 50 | pub fn push_back(&self, path: impl Into, source: impl Into) -> FileId { 51 | FileId(self.files.push_back(File::new(path, source))) 52 | } 53 | 54 | #[inline] 55 | pub fn get(&self, id: FileId) -> Option<&File> { 56 | self.files.get(id.0) 57 | } 58 | 59 | pub fn files(&self) -> impl Iterator { 60 | self.files.iter().map(|(id, file)| (FileId(id), file)) 61 | } 62 | 63 | pub fn len(&self) -> usize { 64 | self.files.len() 65 | } 66 | 67 | pub fn is_empty(&self) -> bool { 68 | self.files.len() == 0 69 | } 70 | 71 | pub fn display_at<'a>(&'a self, root: &'a Path) -> DisplaySourceMap<'a> { 72 | DisplaySourceMap { map: self, root } 73 | } 74 | } 75 | 76 | pub struct DisplaySourceMap<'a> { 77 | map: &'a SourceMap, 78 | root: &'a Path, 79 | } 80 | 81 | impl fmt::Display for DisplaySourceMap<'_> { 82 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 83 | self.map.files().enumerate().try_for_each(|(i, (_, file))| { 84 | if i > 0 { 85 | writeln!(f)?; 86 | } 87 | let path = file.path.strip_prefix(self.root).unwrap_or(&file.path); 88 | write!(f, "{}", path.display()) 89 | }) 90 | } 91 | } 92 | 93 | #[derive(Debug)] 94 | pub struct File { 95 | path: PathBuf, 96 | source: String, 97 | lines: Vec, 98 | } 99 | 100 | impl File { 101 | pub fn new(path: impl Into, source: impl Into) -> Self { 102 | let mut lines = vec![]; 103 | let source = source.into(); 104 | for (offset, _) in source.match_indices('\n') { 105 | lines.push(offset as u32 + 1); 106 | } 107 | Self { 108 | path: path.into(), 109 | source, 110 | lines, 111 | } 112 | } 113 | 114 | #[inline] 115 | pub fn source(&self) -> &str { 116 | &self.source 117 | } 118 | 119 | #[inline] 120 | pub fn path(&self) -> &Path { 121 | &self.path 122 | } 123 | 124 | pub fn lookup(&self, offset: u32) -> SourceLoc { 125 | let (line, line_offset) = self.line_and_offset(offset); 126 | SourceLoc { 127 | line, 128 | col: self.source[line_offset as usize..offset as usize] 129 | .chars() 130 | .count(), 131 | } 132 | } 133 | 134 | pub fn span_contents(&self, span: Span) -> &str { 135 | &self.source[span.start as usize..span.end as usize] 136 | } 137 | 138 | pub fn line_contents(&self, line: usize) -> Option<&str> { 139 | let start = if line == 0 { 140 | 0 141 | } else { 142 | self.lines.get(line - 1).copied()? 143 | }; 144 | let end = self.lines.get(line).copied().unwrap_or_else(|| { 145 | u32::try_from(self.source.len()).expect("source size should fit in u32") 146 | }); 147 | Some(&self.source[start as usize..end as usize]) 148 | } 149 | 150 | pub fn line_and_offset(&self, offset: u32) -> (usize, u32) { 151 | if self.lines.first().is_some_and(|&p| p > offset) { 152 | (0, 0) 153 | } else { 154 | let line = self 155 | .lines 156 | .binary_search(&offset) 157 | .map(|i| i + 1) 158 | .unwrap_or_else(|i| i); 159 | (line, self.lines[line - 1]) 160 | } 161 | } 162 | } 163 | 164 | #[derive(Debug, Clone)] 165 | pub struct SourceLoc { 166 | pub line: usize, 167 | pub col: usize, 168 | } 169 | 170 | impl fmt::Display for SourceLoc { 171 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 172 | write!(f, "{}:{}", self.line + 1, self.col + 1) 173 | } 174 | } 175 | 176 | struct StableDeque { 177 | front: FrozenVec>, 178 | back: FrozenVec>, 179 | } 180 | 181 | impl StableDeque { 182 | pub fn get(&self, index: i32) -> Option<&T> { 183 | if index < 0 { 184 | self.front.get((-index - 1) as usize) 185 | } else { 186 | self.back.get(index as usize) 187 | } 188 | } 189 | 190 | fn push_front(&self, value: T) -> i32 { 191 | self.front.push(Box::new(value)); 192 | -i32::try_from(self.front.len()).expect("deque size overflows i32") 193 | } 194 | 195 | fn push_back(&self, value: T) -> i32 { 196 | self.back.push(Box::new(value)); 197 | i32::try_from(self.back.len()).expect("deque size overflows i32") - 1 198 | } 199 | 200 | pub fn len(&self) -> usize { 201 | self.front.len() + self.back.len() 202 | } 203 | 204 | fn iter(&self) -> impl Iterator { 205 | self.front 206 | .iter() 207 | .enumerate() 208 | .map(|(i, v)| (-(i as i32) - 1, v)) 209 | .chain(self.back.iter().enumerate().map(|(i, v)| (i as i32, v))) 210 | } 211 | } 212 | 213 | impl Default for StableDeque { 214 | fn default() -> Self { 215 | Self { 216 | front: FrozenVec::default(), 217 | back: FrozenVec::default(), 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /crates/syntax/ast/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ast; 2 | mod files; 3 | mod span; 4 | mod visitor; 5 | 6 | pub use ast::{ 7 | Aggregate, Annotation, Assoc, AstKind, BinOp, Block, Case, ConditionalBlock, Constant, Enum, 8 | EnumVariant, Expr, Field, Function, FunctionBody, Import, Item, ItemDecl, ItemQualifiers, 9 | Module, Param, ParamQualifiers, Path, QueryResult, Stmt, StrPart, Type, TypeParam, UnOp, 10 | Variance, Visibility, WithSpan, Wrapper, 11 | }; 12 | pub use files::{File, SourceLoc, SourceMap}; 13 | pub use span::{FileId, Span}; 14 | pub use visitor::{AstNode, AstVisitor, NodeId}; 15 | 16 | pub type Spanned = (A, Span); 17 | 18 | pub type SourceAggregate<'src> = Aggregate<'src, WithSpan>; 19 | pub type SourceAnnotation<'src> = Annotation<'src, WithSpan>; 20 | pub type SourceBlock<'src> = Block<'src, WithSpan>; 21 | pub type SourceConditionalBlock<'src> = ConditionalBlock<'src, WithSpan>; 22 | pub type SourceCase<'src> = Case<'src, WithSpan>; 23 | pub type SourceEnum<'src> = Enum<'src, WithSpan>; 24 | pub type SourceExpr<'src> = Expr<'src, WithSpan>; 25 | pub type SourceField<'src> = Field<'src, WithSpan>; 26 | pub type SourceFunction<'src> = Function<'src, WithSpan>; 27 | pub type SourceItem<'src> = Item<'src, WithSpan>; 28 | pub type SourceItemDecl<'src> = ItemDecl<'src, WithSpan>; 29 | pub type SourceModule<'src> = Module<'src, WithSpan>; 30 | pub type SourceParam<'src> = Param<'src, WithSpan>; 31 | pub type SourceStmt<'src> = Stmt<'src, WithSpan>; 32 | pub type SourceType<'src> = Type<'src, WithSpan>; 33 | pub type SourceTypeParam<'src> = TypeParam<'src, WithSpan>; 34 | pub type SourceFunctionBody<'src> = FunctionBody<'src, WithSpan>; 35 | pub type SourceAstNode<'a, 'src> = AstNode<'a, 'src, WithSpan>; 36 | -------------------------------------------------------------------------------- /crates/syntax/ast/src/span.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, ops}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 4 | pub struct Span { 5 | pub start: u32, 6 | pub end: u32, 7 | pub file: FileId, 8 | } 9 | 10 | impl Span { 11 | pub fn new(start: u32, end: u32, file: FileId) -> Self { 12 | Span { start, end, file } 13 | } 14 | 15 | #[inline] 16 | pub fn contains(&self, pos: u32) -> bool { 17 | self.start <= pos && pos < self.end 18 | } 19 | 20 | pub fn cmp_pos(&self, pos: u32) -> std::cmp::Ordering { 21 | if self.start > pos { 22 | std::cmp::Ordering::Greater 23 | } else if self.end <= pos { 24 | std::cmp::Ordering::Less 25 | } else { 26 | std::cmp::Ordering::Equal 27 | } 28 | } 29 | 30 | pub fn merge(&self, other: &Self) -> Self { 31 | assert_eq!(self.file, other.file); 32 | Self { 33 | start: self.start.min(other.start), 34 | end: self.end.max(other.end), 35 | file: self.file, 36 | } 37 | } 38 | } 39 | 40 | impl ops::Add for Span { 41 | type Output = Span; 42 | 43 | fn add(self, other: Span) -> Self::Output { 44 | assert_eq!(self.file, other.file); 45 | Self { 46 | start: self.start + other.start, 47 | end: self.start + other.end, 48 | file: self.file, 49 | } 50 | } 51 | } 52 | 53 | impl ops::Add for Span { 54 | type Output = Span; 55 | 56 | fn add(self, other: u8) -> Self::Output { 57 | Self { 58 | start: self.start + other as u32, 59 | end: self.end + other as u32, 60 | file: self.file, 61 | } 62 | } 63 | } 64 | 65 | #[cfg(feature = "chumsky")] 66 | impl From<(FileId, chumsky::span::SimpleSpan)> for Span { 67 | #[inline] 68 | fn from((file, sp): (FileId, chumsky::span::SimpleSpan)) -> Self { 69 | Span { 70 | start: sp.start as u32, 71 | end: sp.end as u32, 72 | file, 73 | } 74 | } 75 | } 76 | 77 | impl From<(FileId, std::ops::Range)> for Span { 78 | #[inline] 79 | fn from((file, range): (FileId, std::ops::Range)) -> Self { 80 | Span { 81 | start: range.start, 82 | end: range.end, 83 | file, 84 | } 85 | } 86 | } 87 | 88 | impl fmt::Display for Span { 89 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 | write!(f, "{}-{} in file {}", self.start, self.end, self.file.0) 91 | } 92 | } 93 | 94 | #[cfg(feature = "chumsky")] 95 | impl chumsky::span::Span for Span { 96 | type Context = FileId; 97 | type Offset = u32; 98 | 99 | #[inline] 100 | fn new(context: Self::Context, range: std::ops::Range) -> Self { 101 | Span { 102 | start: range.start, 103 | end: range.end, 104 | file: context, 105 | } 106 | } 107 | 108 | #[inline] 109 | fn context(&self) -> Self::Context { 110 | self.file 111 | } 112 | 113 | #[inline] 114 | fn start(&self) -> Self::Offset { 115 | self.start 116 | } 117 | 118 | #[inline] 119 | fn end(&self) -> Self::Offset { 120 | self.end 121 | } 122 | } 123 | 124 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 125 | pub struct FileId(pub(super) i32); 126 | 127 | impl FileId { 128 | #[cfg(feature = "testing")] 129 | pub fn from_i32(id: i32) -> Self { 130 | Self(id) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /crates/syntax/formatter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-formatter" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-ast.workspace = true 9 | redscript-parser.workspace = true 10 | 11 | hashbrown.workspace = true 12 | pretty_dtoa.workspace = true 13 | 14 | [dev-dependencies] 15 | insta.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/data/commented.reds: -------------------------------------------------------------------------------- 1 | // the entire file is commented out 2 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/data/control-flow.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test() { 3 | if true 4 | && false 5 | && true 6 | && false 7 | && true 8 | && false { 9 | } else if false { 10 | FTLog("a"); 11 | } else { 12 | FTLog("b"); 13 | } 14 | 15 | if true { 16 | FTLog("c"); 17 | } 18 | 19 | while true 20 | && false 21 | && true 22 | && false 23 | && true 24 | && false { 25 | FTLog("d"); 26 | } 27 | 28 | while false { 29 | FTLog("e"); 30 | } 31 | 32 | for x in [ 33 | "lorem", 34 | "ipsum", 35 | "dolor", 36 | "sit", 37 | "amet", 38 | "consectetur", 39 | "adipiscing", 40 | "elit", 41 | "sed", 42 | "do", 43 | "eiusmod", 44 | "tempor", 45 | "incididunt", 46 | "ut", 47 | "labore", 48 | "et", 49 | "dolore", 50 | "magna", 51 | "aliqua" 52 | ] { 53 | FTLog(x); 54 | } 55 | 56 | switch 1 { 57 | case 0: 58 | FTLog("f"); 59 | case 1: 60 | FTLog("g"); 61 | default: 62 | FTLog("h"); 63 | } 64 | 65 | for y in [1, 2, 3] { 66 | FTLog(s"f: \(y)"); 67 | } 68 | 69 | let f1 = (a) -> a; 70 | let f2 = (a) -> { 71 | return a; 72 | }; 73 | let f3 = (a: Int32) -> a; 74 | } 75 | 76 | // trailing comment 77 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/data/module.reds: -------------------------------------------------------------------------------- 1 | module Test.Module 2 | 3 | import Dummy.* 4 | 5 | enum Enum { 6 | A = -1, 7 | B = 0, 8 | C = 1, 9 | } 10 | 11 | /// A class doc comment. 12 | private native class NativeClass extends IScriptable { 13 | let a: Int32 = 0; 14 | /// A field doc comment. 15 | let b: Int64; 16 | 17 | private native func NativeMethod(opt arg: Int16) -> Uint16; 18 | 19 | private native func NativeMethodTwo(); 20 | 21 | final func ScriptedMethod(out arg: Int32) { 22 | arg = 0; 23 | } 24 | } 25 | 26 | public class ScriptedClass { 27 | let field: String = "Hello, World!"; 28 | 29 | public static func New() -> ref { 30 | return new ScriptedClass(); 31 | } 32 | 33 | func ScriptedMethod() -> Int32 { 34 | return 1; 35 | } 36 | 37 | func InlineMethod() -> Int32 = 2; 38 | } 39 | 40 | public class GenericClass<+A extends ScriptedClass> { 41 | public func GenericMethod() { 42 | } 43 | } 44 | 45 | struct Struct { 46 | let a: Int32; 47 | let b: Int64; 48 | } 49 | 50 | func LongFunc1( 51 | a: Int32, 52 | very: Int64, 53 | large: Uint32, 54 | number: Uint64, 55 | of: Float, 56 | parameters: String 57 | ) { 58 | LongFunc2( 59 | 0, 60 | 1l, 61 | 2u, 62 | 3ul, 63 | 4.0, 64 | "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore" 65 | ); 66 | 67 | LongFunc2( 68 | LongFunc2( 69 | 0, 70 | 1l, 71 | 2u, 72 | 3ul, 73 | 4.0, 74 | "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore" 75 | ), 76 | 1l, 77 | 2u, 78 | 3ul, 79 | 4.0, 80 | "lorem" 81 | ); 82 | } 83 | 84 | func LongFunc2( 85 | a: Int32, 86 | very: Int64, 87 | large: Uint32, 88 | number: Uint64, 89 | of: Float, 90 | parameters: String 91 | ) -> Int32 { 92 | return a; 93 | } 94 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/data/operators.reds: -------------------------------------------------------------------------------- 1 | 2 | func Test1() { 3 | let a = 1 * (2 + 3); 4 | let b = (4 + 5) * 6; 5 | let c = -7 + 8; 6 | let d = -(9 + 10); 7 | } 8 | 9 | func Test2() { 10 | let tru = true; 11 | let fal = false; 12 | let a = !tru && fal; 13 | let b = !(tru && fal); 14 | let c = -new Vector4(0, 0, 0, 0) + new Vector4(1, 1, 1, 1); 15 | let d = -(new Vector4(0, 0, 0, 0) + new Vector4(1, 1, 1, 1)); 16 | let e = (new Damage() += 1.0).GetValue(); 17 | let f = new Damage(); 18 | let g = (fal as Damage).GetValue(); 19 | let h = !tru ? tru : fal && fal; 20 | let i = (!tru ? tru : fal) && fal; 21 | let j = !(tru ? tru : fal) && fal; 22 | let k = !(tru ? tru : fal && fal); 23 | } 24 | 25 | func Test3() { 26 | let test = new TestClass(); 27 | let allEqual = test.GetPositionX() 28 | == test.GetPositionX() 29 | && test.GetPositionY() == test.GetPositionY() 30 | && test.GetPositionZ() == test.GetPositionZ() 31 | && test.GetPositionW() == test.GetPositionW(); 32 | let anyUnequal1 = test.GetPositionX() 33 | != test.GetPositionX() 34 | || test.GetPositionY() != test.GetPositionY() 35 | || test.GetPositionZ() != test.GetPositionZ() 36 | || test.GetPositionW() != test.GetPositionW(); 37 | let anyUnequal2 = !(test.GetPositionX() == test.GetPositionX()) 38 | || !(test.GetPositionY() == test.GetPositionY()) 39 | || !(test.GetPositionZ() == test.GetPositionZ()) 40 | || !(test.GetPositionW() == test.GetPositionW()); 41 | } 42 | 43 | func Test4() { 44 | (new PlayerPuppet()) 45 | .GetBumpComponent() 46 | .FindComponentByName(n"whatever") 47 | .GetEntity() 48 | .PrefetchAppearanceChange(n"whatever"); 49 | } 50 | 51 | class TestClass { 52 | func GetPositionX() -> Float = 0; 53 | 54 | func GetPositionY() -> Float = 0; 55 | 56 | func GetPositionZ() -> Float = 0; 57 | 58 | func GetPositionW() -> Float = 0; 59 | } 60 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/formatted.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use redscript_ast::SourceMap; 4 | use redscript_formatter::{FormatSettings, format_document}; 5 | 6 | #[test] 7 | fn formatted_files() { 8 | insta::glob!("data/*.reds", |path| { 9 | let current = env::current_dir().unwrap().canonicalize().unwrap(); 10 | let relative = path.strip_prefix(¤t).unwrap(); 11 | let sources = SourceMap::from_files(&[relative]).unwrap(); 12 | 13 | let settings = FormatSettings::default(); 14 | for (id, file) in sources.files() { 15 | let (module, errors) = format_document(file.source(), id, &settings); 16 | if let (Some(module), []) = (module, &errors[..]) { 17 | insta::assert_snapshot!(module); 18 | } else { 19 | panic!("failed to parse {}: {errors:?}", file.path().display()); 20 | } 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/snapshots/formatted__formatted_files@commented.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/syntax/formatter/tests/formatted.rs 3 | expression: module 4 | input_file: crates/syntax/formatter/tests/data/commented.reds 5 | snapshot_kind: text 6 | --- 7 | // the entire file is commented out 8 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/snapshots/formatted__formatted_files@control-flow.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/syntax/formatter/tests/formatted.rs 3 | expression: module 4 | input_file: crates/syntax/formatter/tests/data/control-flow.reds 5 | snapshot_kind: text 6 | --- 7 | func Test() { 8 | if true 9 | && false 10 | && true 11 | && false 12 | && true 13 | && false { 14 | } else if false { 15 | FTLog("a"); 16 | } else { 17 | FTLog("b"); 18 | } 19 | 20 | if true { 21 | FTLog("c"); 22 | } 23 | 24 | while true 25 | && false 26 | && true 27 | && false 28 | && true 29 | && false { 30 | FTLog("d"); 31 | } 32 | 33 | while false { 34 | FTLog("e"); 35 | } 36 | 37 | for x in [ 38 | "lorem", 39 | "ipsum", 40 | "dolor", 41 | "sit", 42 | "amet", 43 | "consectetur", 44 | "adipiscing", 45 | "elit", 46 | "sed", 47 | "do", 48 | "eiusmod", 49 | "tempor", 50 | "incididunt", 51 | "ut", 52 | "labore", 53 | "et", 54 | "dolore", 55 | "magna", 56 | "aliqua" 57 | ] { 58 | FTLog(x); 59 | } 60 | 61 | switch 1 { 62 | case 0: 63 | FTLog("f"); 64 | case 1: 65 | FTLog("g"); 66 | default: 67 | FTLog("h"); 68 | } 69 | 70 | for y in [1, 2, 3] { 71 | FTLog(s"f: \(y)"); 72 | } 73 | 74 | let f1 = (a) -> a; 75 | let f2 = (a) -> { 76 | return a; 77 | }; 78 | let f3 = (a: Int32) -> a; 79 | } 80 | 81 | // trailing comment 82 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/snapshots/formatted__formatted_files@module.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/syntax/formatter/tests/formatted.rs 3 | expression: module 4 | input_file: crates/syntax/formatter/tests/data/module.reds 5 | snapshot_kind: text 6 | --- 7 | module Test.Module 8 | 9 | import Dummy.* 10 | 11 | enum Enum { 12 | A = -1, 13 | B = 0, 14 | C = 1, 15 | } 16 | 17 | /// A class doc comment. 18 | private native class NativeClass extends IScriptable { 19 | let a: Int32 = 0; 20 | /// A field doc comment. 21 | let b: Int64; 22 | 23 | private native func NativeMethod(opt arg: Int16) -> Uint16; 24 | 25 | private native func NativeMethodTwo(); 26 | 27 | final func ScriptedMethod(out arg: Int32) { 28 | arg = 0; 29 | } 30 | } 31 | 32 | public class ScriptedClass { 33 | let field: String = "Hello, World!"; 34 | 35 | public static func New() -> ref { 36 | return new ScriptedClass(); 37 | } 38 | 39 | func ScriptedMethod() -> Int32 { 40 | return 1; 41 | } 42 | 43 | func InlineMethod() -> Int32 = 2; 44 | } 45 | 46 | public class GenericClass<+A extends ScriptedClass> { 47 | public func GenericMethod() { 48 | } 49 | } 50 | 51 | struct Struct { 52 | let a: Int32; 53 | let b: Int64; 54 | } 55 | 56 | func LongFunc1( 57 | a: Int32, 58 | very: Int64, 59 | large: Uint32, 60 | number: Uint64, 61 | of: Float, 62 | parameters: String 63 | ) { 64 | LongFunc2( 65 | 0, 66 | 1l, 67 | 2u, 68 | 3ul, 69 | 4.0, 70 | "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore" 71 | ); 72 | 73 | LongFunc2( 74 | LongFunc2( 75 | 0, 76 | 1l, 77 | 2u, 78 | 3ul, 79 | 4.0, 80 | "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore" 81 | ), 82 | 1l, 83 | 2u, 84 | 3ul, 85 | 4.0, 86 | "lorem" 87 | ); 88 | } 89 | 90 | func LongFunc2( 91 | a: Int32, 92 | very: Int64, 93 | large: Uint32, 94 | number: Uint64, 95 | of: Float, 96 | parameters: String 97 | ) -> Int32 { 98 | return a; 99 | } 100 | -------------------------------------------------------------------------------- /crates/syntax/formatter/tests/snapshots/formatted__formatted_files@operators.reds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/syntax/formatter/tests/formatted.rs 3 | expression: module 4 | input_file: crates/syntax/formatter/tests/data/operators.reds 5 | snapshot_kind: text 6 | --- 7 | func Test1() { 8 | let a = 1 * (2 + 3); 9 | let b = (4 + 5) * 6; 10 | let c = -7 + 8; 11 | let d = -(9 + 10); 12 | } 13 | 14 | func Test2() { 15 | let tru = true; 16 | let fal = false; 17 | let a = !tru && fal; 18 | let b = !(tru && fal); 19 | let c = -new Vector4(0, 0, 0, 0) + new Vector4(1, 1, 1, 1); 20 | let d = -(new Vector4(0, 0, 0, 0) + new Vector4(1, 1, 1, 1)); 21 | let e = (new Damage() += 1.0).GetValue(); 22 | let f = new Damage(); 23 | let g = (fal as Damage).GetValue(); 24 | let h = !tru ? tru : fal && fal; 25 | let i = (!tru ? tru : fal) && fal; 26 | let j = !(tru ? tru : fal) && fal; 27 | let k = !(tru ? tru : fal && fal); 28 | } 29 | 30 | func Test3() { 31 | let test = new TestClass(); 32 | let allEqual = test.GetPositionX() 33 | == test.GetPositionX() 34 | && test.GetPositionY() == test.GetPositionY() 35 | && test.GetPositionZ() == test.GetPositionZ() 36 | && test.GetPositionW() == test.GetPositionW(); 37 | let anyUnequal1 = test.GetPositionX() 38 | != test.GetPositionX() 39 | || test.GetPositionY() != test.GetPositionY() 40 | || test.GetPositionZ() != test.GetPositionZ() 41 | || test.GetPositionW() != test.GetPositionW(); 42 | let anyUnequal2 = !(test.GetPositionX() == test.GetPositionX()) 43 | || !(test.GetPositionY() == test.GetPositionY()) 44 | || !(test.GetPositionZ() == test.GetPositionZ()) 45 | || !(test.GetPositionW() == test.GetPositionW()); 46 | } 47 | 48 | func Test4() { 49 | (new PlayerPuppet()) 50 | .GetBumpComponent() 51 | .FindComponentByName(n"whatever") 52 | .GetEntity() 53 | .PrefetchAppearanceChange(n"whatever"); 54 | } 55 | 56 | class TestClass { 57 | func GetPositionX() -> Float = 0; 58 | 59 | func GetPositionY() -> Float = 0; 60 | 61 | func GetPositionZ() -> Float = 0; 62 | 63 | func GetPositionW() -> Float = 0; 64 | } 65 | -------------------------------------------------------------------------------- /crates/syntax/parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-parser" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | redscript-ast = { workspace = true, features = ["chumsky"] } 9 | 10 | chumsky.workspace = true 11 | 12 | [dev-dependencies] 13 | redscript-ast = { workspace = true, features = ["testing"] } 14 | 15 | similar-asserts.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | -------------------------------------------------------------------------------- /crates/syntax/parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod lexer; 2 | pub mod parser; 3 | 4 | use std::fmt; 5 | 6 | use chumsky::prelude::*; 7 | pub use lexer::Token; 8 | use parser::{Parse, ParserInput}; 9 | use redscript_ast::{ 10 | self as ast, FileId, SourceExpr, SourceItem, SourceItemDecl, SourceLoc, SourceMap, 11 | SourceModule, SourceStmt, Span, Spanned, 12 | }; 13 | 14 | pub type ParseResult = (Option, Vec); 15 | 16 | // this can't be written as a generic function due to a GAT bug 17 | macro_rules! parse { 18 | ($src:expr, $parser:expr, $ctx:expr) => {{ 19 | let (toks, mut errs) = lex($src, $ctx); 20 | if let Some(toks) = toks { 21 | let (res, e) = parse($parser, &toks, $ctx); 22 | errs.extend(e); 23 | (res, errs) 24 | } else { 25 | (None, errs) 26 | } 27 | }}; 28 | } 29 | 30 | pub fn parse_modules<'a>( 31 | it: impl IntoIterator, 32 | ) -> (Vec>, Vec) { 33 | let mut errs = vec![]; 34 | let toks = it 35 | .into_iter() 36 | .filter_map(|(file, src)| { 37 | let (t, e) = lex(src, file); 38 | errs.extend(e); 39 | Some((file, t?)) 40 | }) 41 | .collect::>(); 42 | 43 | let parser = parser::module(); 44 | let modules = toks 45 | .iter() 46 | .filter_map(|(file, toks)| { 47 | let (res, e) = parse(parser.clone(), toks, *file); 48 | errs.extend(e); 49 | res 50 | }) 51 | .collect::>(); 52 | (modules, errs) 53 | } 54 | 55 | pub fn parse_module(src: &str, file: FileId) -> ParseResult> { 56 | parse!(src, parser::module(), file) 57 | } 58 | 59 | pub fn parse_item_decl(src: &str, file: FileId) -> ParseResult> { 60 | parse!(src, parser::item_decl(), file) 61 | } 62 | 63 | pub fn parse_item(src: &str, file: FileId) -> ParseResult> { 64 | parse!(src, parser::item(), file) 65 | } 66 | 67 | pub fn parse_stmt(src: &str, file: FileId) -> ParseResult> { 68 | parse!(src, parser::stmt(), file) 69 | } 70 | 71 | pub fn parse_expr(src: &str, file: FileId) -> ParseResult> { 72 | parse!(src, parser::expr(), file) 73 | } 74 | 75 | pub fn lex(src: &str, f: FileId) -> ParseResult>>> { 76 | lex_internal(src, f, false) 77 | } 78 | 79 | pub fn lex_with_lf_and_comments( 80 | src: &str, 81 | f: FileId, 82 | ) -> ParseResult>>> { 83 | lex_internal(src, f, true) 84 | } 85 | 86 | fn lex_internal( 87 | src: &str, 88 | file_id: FileId, 89 | keep_lf_and_comments: bool, 90 | ) -> ParseResult>>> { 91 | let (output, errors) = lexer::lex(keep_lf_and_comments) 92 | .parse(src) 93 | .into_output_errors(); 94 | let errors = errors 95 | .into_iter() 96 | .map(|err| { 97 | let span = Span::from((file_id, *err.span())); 98 | Error::Lex(FormattedError(err).to_string(), span) 99 | }) 100 | .collect(); 101 | let Some(tokens) = output else { 102 | return (None, errors); 103 | }; 104 | 105 | let output = tokens 106 | .into_iter() 107 | .map(|(tok, span)| { 108 | let span = Span::from((file_id, span)); 109 | let tok = tok.map_span(span, |s| Span::from((file_id, s))); 110 | (tok, span) 111 | }) 112 | .collect(); 113 | (Some(output), errors) 114 | } 115 | 116 | pub fn parse<'tok, 'src: 'tok, A>( 117 | parser: impl Parse<'tok, 'src, A>, 118 | tokens: &'tok [(Token<'src>, Span)], 119 | file: FileId, 120 | ) -> ParseResult { 121 | let parser: &dyn Parser<'tok, _, A, extra::Err<_>> = &parser.with_ctx(file); 122 | let (output, errors) = parser 123 | .parse(parser_input(tokens, file)) 124 | .into_output_errors(); 125 | let errors = errors.into_iter().map(Error::new_parse).collect(); 126 | (output, errors) 127 | } 128 | 129 | fn parser_input<'tok, 'src>( 130 | tokens: &'tok [(Token<'src>, Span)], 131 | file: FileId, 132 | ) -> ParserInput<'tok, 'src> { 133 | let max = tokens.last().map(|(_, span)| span.end()).unwrap_or(0); 134 | tokens.spanned(Span { 135 | start: max, 136 | end: max, 137 | file, 138 | }) 139 | } 140 | 141 | #[derive(Debug, PartialEq, Eq)] 142 | pub enum Error { 143 | Parse(String, Span), 144 | Lex(String, Span), 145 | } 146 | 147 | impl Error { 148 | fn new_parse(error: chumsky::error::Rich<'_, T, Span>) -> Self 149 | where 150 | T: fmt::Display, 151 | { 152 | let span = *error.span(); 153 | Error::Parse(FormattedError(error).to_string(), span) 154 | } 155 | 156 | pub fn display<'a>(&'a self, sources: &'a SourceMap) -> Option> { 157 | let span = self.span(); 158 | let file = sources.get(span.file)?; 159 | let start = file.lookup(span.start); 160 | let end = file.lookup(span.end); 161 | let line = file.line_contents(start.line)?; 162 | Some(ErrorDisplay { 163 | file, 164 | start, 165 | end, 166 | line, 167 | err: self, 168 | }) 169 | } 170 | 171 | pub fn span(&self) -> Span { 172 | match self { 173 | Error::Parse(_, span) | Error::Lex(_, span) => *span, 174 | } 175 | } 176 | } 177 | 178 | impl fmt::Display for Error { 179 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 180 | match self { 181 | Error::Parse(msg, _) | Error::Lex(msg, _) => write!(f, "{msg}"), 182 | } 183 | } 184 | } 185 | 186 | impl std::error::Error for Error {} 187 | 188 | struct FormattedError<'a, T, S>(chumsky::error::Rich<'a, T, S>); 189 | 190 | impl fmt::Display for FormattedError<'_, T, S> { 191 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 192 | write!(f, "{}", self.0.reason())?; 193 | self.0 194 | .contexts() 195 | .try_for_each(|(label, _)| write!(f, " in {label}")) 196 | } 197 | } 198 | 199 | #[derive(Debug)] 200 | struct ErrorDisplay<'a> { 201 | file: &'a ast::File, 202 | start: SourceLoc, 203 | end: SourceLoc, 204 | line: &'a str, 205 | err: &'a Error, 206 | } 207 | 208 | impl fmt::Display for ErrorDisplay<'_> { 209 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 210 | writeln!( 211 | f, 212 | "At {}:{}:{}", 213 | self.file.path().display(), 214 | self.start.line + 1, 215 | self.start.col + 1 216 | )?; 217 | 218 | writeln!(f, "{}", self.line.trim_end())?; 219 | 220 | let pad = self.start.col; 221 | let underline_len = if self.start.line == self.end.line { 222 | (self.end.col - self.start.col).max(1) 223 | } else { 224 | 3 225 | }; 226 | writeln!(f, "{:>pad$}{:^>underline_len$}", "", "")?; 227 | writeln!(f, "{}", self.err) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /docs/types/Array.md: -------------------------------------------------------------------------------- 1 | A dynamically allocated array. Instances of this type can be created using the array literal syntax, e.g. `[element1, element2, ...]`. 2 | 3 | ### Example 4 | 5 | ``` 6 | let array = [1, 2, 3]; 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/Bool.md: -------------------------------------------------------------------------------- 1 | A boolean value. Instances of this type can be created using the boolean literal syntax, e.g. `true` or `false`. 2 | 3 | ### Example 4 | 5 | ``` 6 | let bool = true; 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/Cname.md: -------------------------------------------------------------------------------- 1 | An immutable name. Instances of this type can be created using the name literal syntax, e.g. `n"tintColor"`. 2 | 3 | ### Example 4 | 5 | ``` 6 | input.RegisterToCallback(n"OnInput", this, n"OnTextInput"); 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/Double.md: -------------------------------------------------------------------------------- 1 | A 64-bit floating-point number. Instances of this type can be created using the 2 | double literal syntax, e.g. `1.23d`. 3 | 4 | ### Example 5 | 6 | ``` 7 | let x = 1.23d; 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/types/Float.md: -------------------------------------------------------------------------------- 1 | A 32-bit floating-point number. Instances of this type can be created using the float literal syntax, e.g. `1.23`. 2 | 3 | ### Example 4 | 5 | ``` 6 | let x = 1.23; 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/Int16.md: -------------------------------------------------------------------------------- 1 | A 16-bit signed integer. 2 | -------------------------------------------------------------------------------- /docs/types/Int32.md: -------------------------------------------------------------------------------- 1 | A 32-bit signed integer. 2 | -------------------------------------------------------------------------------- /docs/types/Int64.md: -------------------------------------------------------------------------------- 1 | A 64-bit signed integer. Instances of this type can be created using the Int64 literal syntax, e.g. `123l`. 2 | -------------------------------------------------------------------------------- /docs/types/Int8.md: -------------------------------------------------------------------------------- 1 | A 8-bit signed integer. 2 | -------------------------------------------------------------------------------- /docs/types/Nothing.md: -------------------------------------------------------------------------------- 1 | The nothing type. 2 | -------------------------------------------------------------------------------- /docs/types/Ref.md: -------------------------------------------------------------------------------- 1 | A strong reference to a value. This is the default reference type for newly created objects. It guarantees that the underlying value is kept alive as long as the reference exists (unlike a weak reference, `wref`). 2 | -------------------------------------------------------------------------------- /docs/types/ResRef.md: -------------------------------------------------------------------------------- 1 | A resource reference. Instances of this type can be created using the resource path syntax, e.g. `r"base\\gameplay\\gui\\common\\main_colors.inkstyle"`. 2 | 3 | ### Example 4 | 5 | ``` 6 | counter.SetStyle(r"base\\gameplay\\gui\\common\\main_colors.inkstyle"); 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/ScriptRef.md: -------------------------------------------------------------------------------- 1 | A reference to a script value. This type allows passing certain values by reference when they would otherwise be copied like arrays and strings. Script references are created implicitly when passing values to functions that accept a `script_ref` parameter. 2 | -------------------------------------------------------------------------------- /docs/types/String.md: -------------------------------------------------------------------------------- 1 | A dynamically allocated string. Instances of this type can be created using the string literal syntax, e.g. `"Hello"`. 2 | 3 | ### Example 4 | 5 | ``` 6 | let myString = "Hello " + "world!"; 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/TweakDbId.md: -------------------------------------------------------------------------------- 1 | An identifier for a TweakDB record. Instances of this type can be created using the TweakDBID literal syntax, e.g. `t"AttachmentSlots.Head"`. 2 | 3 | ### Example 4 | 5 | ``` 6 | let isIconic = TweakDBInterface.GetBool(id + t".isPresetIconic", false); 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/types/Uint16.md: -------------------------------------------------------------------------------- 1 | A 16-bit unsigned integer. 2 | -------------------------------------------------------------------------------- /docs/types/Uint32.md: -------------------------------------------------------------------------------- 1 | A 32-bit unsigned integer. Instances of this type can be created using the unsigned literal syntax, e.g. `1u`. 2 | -------------------------------------------------------------------------------- /docs/types/Uint64.md: -------------------------------------------------------------------------------- 1 | A 64-bit unsigned integer. Instances of this type can be created using the Uint64 literal syntax, e.g. `123ul`. 2 | -------------------------------------------------------------------------------- /docs/types/Uint8.md: -------------------------------------------------------------------------------- 1 | An 8-bit unsigned integer. 2 | -------------------------------------------------------------------------------- /docs/types/Variant.md: -------------------------------------------------------------------------------- 1 | Variant is a type that represents values that can hold a set of different types. You can create such instances with the `ToVariant` intrinsic. The inner value can later be retrieved using the `FromVariant` intrinsic (as long as `FromVariant` is called with precisely the correct type). 2 | 3 | ### Example 4 | 5 | ``` 6 | let int: Int32 = 42; 7 | let variant = ToVariant(int); 8 | let value = FromVariant(variant); 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/types/Void.md: -------------------------------------------------------------------------------- 1 | The void type. 2 | -------------------------------------------------------------------------------- /docs/types/Wref.md: -------------------------------------------------------------------------------- 1 | A weak reference is a type of reference that does not prevent the underlying value from being dropped. When no more strong references (`ref`s) exist, the value will be dropped and any remaining weak references (`wref`s) will no longer be able to access it. 2 | 3 | ### Example 4 | 5 | ``` 6 | let map: wref = new inkHashMap(); 7 | IsDefined(map); // false 8 | ``` 9 | -------------------------------------------------------------------------------- /scripts/build-pgo.ps1: -------------------------------------------------------------------------------- 1 | # This script downloads Codeware, a script cache, and runs REDscript multiple times to 2 | # generate a PGO profile. It then merges the profiles and builds REDscript with the PGO profile. 3 | 4 | $codewareVersion = "1.14.1" 5 | $codewareUrl = "https://github.com/psiberx/cp2077-codeware/releases/download/v$codewareVersion/Codeware-$codewareVersion.zip" 6 | $cacheUrl = "https://file.io/01QAMSSG91um" 7 | 8 | $codewarePath = Join-Path $env:TEMP "codeware" 9 | $cacheDir = Join-Path $env:TEMP "redscript-cache" 10 | $cachePath = Join-Path $cacheDir "final.redscripts" 11 | $pgoPath = Join-Path $env:TEMP "redscript-pgo" 12 | 13 | if (-Not (Test-Path -Path $codewarePath)) { 14 | $tempDir = New-Item -ItemType Directory -Path $codewarePath -Force 15 | $outFile = Join-Path $tempDir.FullName (New-Object System.Uri($codewareUrl)).Segments[-1] 16 | Invoke-WebRequest -Uri $codewareUrl -OutFile $outFile 17 | Expand-Archive -Path $zipFilePath -DestinationPath $tempDir.FullName -Force 18 | } else { 19 | Write-Host "The temporary directory already exists. Skipping download." 20 | } 21 | 22 | if (-Not (Test-Path -Path $cacheDir)) { 23 | $tempDir = New-Item -ItemType Directory -Path $cacheDir -Force 24 | Invoke-WebRequest -Uri $cacheUrl -OutFile $cachePath 25 | } else { 26 | Write-Host "The cache directory already exists. Skipping download." 27 | } 28 | 29 | if (Test-Path -Path $pgoPath) { 30 | Remove-Item -Path $pgoPath -Recurse -Force 31 | } 32 | 33 | $cargoDir = rustup which cargo 34 | $defaultToolchain = rustup show active-toolchain 35 | $defaultToolchain -match 'stable-(.+?) \(default\)' 36 | $triplet = $matches[1] 37 | $llvmProfdata = "$cargoDir\..\..\lib\rustlib\$triplet\bin\llvm-profdata.exe" 38 | 39 | rustup component add llvm-tools-preview 40 | 41 | & { $env:RUSTFLAGS = "-Cprofile-generate=$pgoPath"; cargo build --release --target=$triplet } 42 | 43 | foreach ($i in 1..10) { 44 | & ".\target\$triplet\release\scc.exe" -compile "$codewarePath\red4ext\plugins\Codeware\Scripts" $cachePath 45 | } 46 | & $llvmProfdata merge -o "$pgoPath\merged.profdata" $pgoPath 47 | 48 | & { $env:RUSTFLAGS = "-Cprofile-use=$pgoPath\merged.profdata"; cargo build --release --target=$triplet } 49 | -------------------------------------------------------------------------------- /scripts/package.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$zipPath 3 | ) 4 | 5 | if (-not $zipPath) { 6 | Write-Error "No output path provided. Please provide a path to write the zip archive to." 7 | exit 1 8 | } 9 | 10 | if (-not (Test-Path "target/release/scc.exe")) { 11 | Write-Error "scc.exe not found in target/release. Please ensure you have built it." 12 | exit 1 13 | } 14 | 15 | if ($env:RUNNER_TEMP) { 16 | $tempDir = $env:RUNNER_TEMP 17 | } else { 18 | $tempDir = $env:TEMP 19 | } 20 | 21 | $stagingDir = Join-Path $tempDir "redscript-archive" 22 | if (Test-Path $stagingDir) { 23 | Remove-Item -Recurse -Force $stagingDir 24 | } 25 | 26 | New-Item -ItemType Directory -Path $stagingDir 27 | 28 | Copy-Item -Path "assets/archive/*" -Destination $stagingDir -Recurse 29 | New-Item -ItemType Directory -Path (Join-Path $stagingDir "engine/tools") 30 | Copy-Item -Path "target/release/scc.exe" -Destination (Join-Path $stagingDir "engine/tools/scc.exe") 31 | Copy-Item -Path "target/release/scc_lib.dll" -Destination (Join-Path $stagingDir "engine/tools/scc_lib.dll") 32 | 33 | Compress-Archive -Path (Join-Path $stagingDir '*') -DestinationPath $zipPath 34 | 35 | Write-Output "Archive created at $zipPath" 36 | --------------------------------------------------------------------------------