diff --git a/.clang-format b/.clang-format index b715e3c98..743a823c0 100644 --- a/.clang-format +++ b/.clang-format @@ -12,7 +12,21 @@ BreakConstructorInitializers: BeforeComma BreakInheritanceList: BeforeComma ColumnLimit: 150 IndentCaseLabels: false -SortIncludes: false +SortIncludes: CaseInsensitive +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^["](commands|generated/).*' + Priority: 1 + - Regex: '^["](lute)/.*' + Priority: 2 + - Regex: '^["](Luau)/.*' + Priority: 3 + - Regex: '^["](lua).*' + Priority: 4 + - Regex: '^["](openssl/|sodium/|uv|curl/|zlib).*' + Priority: 5 + - Regex: '^<[^/]*>' + Priority: 6 IndentWidth: 4 TabWidth: 4 ObjCBlockIndentWidth: 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..ff36c5bc1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.tune linguist-language=TOML diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index def71560d..ecfc89091 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -16,6 +16,10 @@ inputs: token: description: "Token for foreman" required: true + use-bootstrap: + description: "Whether to use the bootstrap script to build the Lute used to run luthier" + required: false + default: "false" outputs: exe_path: @@ -60,6 +64,14 @@ runs: path: extern key: extern-${{ hashFiles('extern/*.tune') }} + - name: Bootstrap Lute + if: ${{ inputs.use-bootstrap == 'true' }} + shell: bash + run: | + echo "Bootstrapping Lute..." + ./tools/bootstrap.sh + alias lute="./build/lute0" + - name: Fetch ${{ inputs.target }} if: steps.cache-extern.outputs.cache-hit != 'true' shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde0f7a06..3bb8d022a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: build +name: CI on: push: @@ -6,9 +6,16 @@ on: pull_request: branches: ["primary"] +# CI on the "primary" branch opts out of concurrency grouping by using the run's +# unique run_id. +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/primary' && github.run_id || github.ref }} + cancel-in-progress: true + jobs: - build: + run-lutecli-luau-tests: runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: matrix: @@ -16,24 +23,39 @@ jobs: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers - os: macos-latest + options: --enable-sanitizers + - os: ubuntu-latest + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers + use-bootstrap: true + - os: ubuntu-latest + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers + use-bootstrap: true fail-fast: false steps: - uses: actions/checkout@v4 - name: Setup and Build Lute + id: build_lute uses: ./.github/actions/setup-and-build with: target: Lute.CLI config: debug options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} + + - name: Use CI .luaurc file + run: rm .luaurc && mv .luaurc.ci .luaurc + + - name: Run Luau Tests (POSIX) + run: ${{ steps.build_lute.outputs.exe_path }} test - test: + run-lute-cxx-unittests: runs-on: ${{ matrix.os }} strategy: @@ -42,17 +64,18 @@ jobs: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers - os: macos-latest + options: --enable-sanitizers fail-fast: false steps: - name: Checkout code uses: actions/checkout@v4 - - name: Setup and Build Tests + - name: Setup and Build C++ Unit Tests id: build_tests uses: ./.github/actions/setup-and-build with: @@ -60,11 +83,10 @@ jobs: config: debug options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - - name: Remove .luaurc file run: rm .luaurc - - name: Run Tests + - name: Run C++ Tests run: ${{ steps.build_tests.outputs.exe_path }} check-format: @@ -82,13 +104,67 @@ jobs: - name: Run StyLua run: stylua --check . + build-docs-site: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + + - name: Install tools + uses: Roblox/setup-foreman@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + allow-external-github-orgs: true + + - name: Install dependencies + run: npm install + working-directory: docs + + - name: Build docs site + run: npm run build + working-directory: docs + + - name: Upload docs artifact + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist/ + + lute-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: debug + options: ${{ matrix.options }} + token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: true + + - name: Run Lute Lint + run: ${{ steps.build_lute.outputs.exe_path }} lint -v . + aggregator: name: Gated Commits runs-on: ubuntu-latest - needs: - - build + needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format, build-docs-site, lute-lint] if: ${{ always() }} steps: - - name: Aggregate + - name: Aggregator run: | - echo "All required jobs completed" + if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ] && [ "${{ needs.lute-lint.result }}" == "success" ]; then + echo "All jobs succeeded" + else + echo "One or more jobs failed" + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a52f96517..c198d413d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest outputs: proceed: ${{ steps.check.outputs.proceed }} + nightly_tag: ${{ steps.set_nightly.outputs.nightly_tag }} permissions: contents: read @@ -58,6 +59,13 @@ jobs: echo "proceed=$PROCEED" >> $GITHUB_OUTPUT + - name: Set nightly version suffix + id: set_nightly + if: github.event_name == 'schedule' || github.event.inputs.nightly + run: | + DATE=$(date +%Y%m%d) + echo "nightly_tag=nightly.$DATE" >> "$GITHUB_OUTPUT" + build: strategy: matrix: @@ -82,6 +90,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set nightly version suffix + if: needs.check.outputs.nightly_tag + run: echo "LUTE_VERSION_SUFFIX=${{ needs.check.outputs.nightly_tag }}" >> "$GITHUB_ENV" + - name: Setup and Build Lute id: build_lute uses: ./.github/actions/setup-and-build @@ -98,7 +110,7 @@ jobs: name: ${{ matrix.artifact_name }} release: - needs: build + needs: [check, build] runs-on: ubuntu-latest permissions: @@ -130,11 +142,9 @@ jobs: - name: Create Nightly Tag id: tag_step - if: github.event_name == 'schedule' || github.event.inputs.nightly + if: needs.check.outputs.nightly_tag run: | - DATE=$(date +%Y%m%d) - TAG="-nightly.$DATE" - echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "tag=-${{ needs.check.outputs.nightly_tag }}" >> $GITHUB_OUTPUT - name: Create Release Tag id: tag_release @@ -144,11 +154,20 @@ jobs: RELEASE_TAG="$VERSION$NIGHTLY_TAG" echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT - - name: Create Release + - name: Create Draft Release uses: luau-lang/action-gh-release@v2 with: tag_name: ${{ steps.tag_release.outputs.tag }} name: ${{ steps.tag_release.outputs.tag }} - draft: false + draft: true prerelease: ${{ github.event.inputs.nightly || github.event_name == 'schedule' }} + generate_release_notes: true files: release/*.zip + + - name: Wait for Tag Creation in GitHub + run: sleep 5 + + - name: Publish Release + run: gh release edit "${{ steps.tag_release.outputs.tag }}" --draft=false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-prs.yml b/.github/workflows/update-prs.yml new file mode 100644 index 000000000..af32f437b --- /dev/null +++ b/.github/workflows/update-prs.yml @@ -0,0 +1,132 @@ +name: Update Luau/Lute + +on: + schedule: + - cron: '0 0 * * 6' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-luau: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get latest Luau release + id: latest-release + run: | + # Fetch the latest release tag from luau-lang/luau + LATEST_TAG=$(curl -s https://api.github.com/repos/luau-lang/luau/releases/latest | jq -r '.tag_name') + echo "Latest Luau release: $LATEST_TAG" + echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + # Fetch the commit hash for this tag + COMMIT_HASH=$(curl -s https://api.github.com/repos/luau-lang/luau/git/ref/tags/$LATEST_TAG | jq -r '.object.sha') + echo "Commit hash: $COMMIT_HASH" + echo "commit=$COMMIT_HASH" >> $GITHUB_OUTPUT + + - name: Check current version + id: current-version + run: | + CURRENT_BRANCH=$(grep '^branch' extern/luau.tune | sed -E 's/^branch *= *"(.*)"/\1/') + echo "Current Luau version: $CURRENT_BRANCH" + echo "branch=$CURRENT_BRANCH" >> $GITHUB_OUTPUT + + - name: Update version files + if: steps.current-version.outputs.branch != steps.latest-release.outputs.tag + run: | + # Update the branch and revision in luau.tune if needed + if [ "${{ steps.current-version.outputs.branch }}" != "${{ steps.latest-release.outputs.tag }}" ]; then + sed -i 's/^branch = ".*"/branch = "${{ steps.latest-release.outputs.tag }}"/' extern/luau.tune + sed -i 's/^revision = ".*"/revision = "${{ steps.latest-release.outputs.commit }}"/' extern/luau.tune + + echo "Updated luau.tune:" + cat extern/luau.tune + fi + + - name: Create Pull Request + if: steps.current-version.outputs.branch != steps.latest-release.outputs.tag + uses: luau-lang/create-pull-request@v7 + with: + commit-message: "chore: update dependencies" + title: "Update Luau to ${{ steps.latest-release.outputs.tag }}" + body: | + ${{ format('**Luau**: Updated from `{0}` to `{1}`', steps.current-version.outputs.branch, steps.latest-release.outputs.tag) }} + + **Release Notes:** + ${{ format('https://github.com/luau-lang/luau/releases/tag/{0}', steps.latest-release.outputs.tag) }} + + --- + *This PR was automatically created by the [Update Luau workflow](https://github.com/${{ github.repository }}/actions/workflows/update-prs.yml)* + branch: update-luau + delete-branch: true + + - name: No update needed + if: steps.current-version.outputs.branch == steps.latest-release.outputs.tag + run: | + echo "Luau is already up to date: ${{ steps.current-version.outputs.branch }}" + + update-lute: + runs-on: ubuntu-latest + needs: update-luau + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get latest Lute release + id: latest-release + run: | + # Fetch the latest release tag from luau-lang/lute + LATEST_LUTE=$( + curl -s https://api.github.com/repos/luau-lang/lute/releases \ + | jq -r 'map(select(.draft | not))[0]?.tag_name' + ) + echo "Latest Lute release: $LATEST_LUTE" + echo "tag=$LATEST_LUTE" >> $GITHUB_OUTPUT + + - name: Check current version + id: current-version + run: | + CURRENT_LUTE=$(grep '^lute = ' rokit.toml | sed -E 's/^lute = ".*@(.*)"/\1/') + echo "Current Lute version: $CURRENT_LUTE" + echo "lute=$CURRENT_LUTE" >> $GITHUB_OUTPUT + + - name: Update version files + run: | + # Update lute version in rokit.toml and foreman.toml + if [ "${{ steps.current-version.outputs.lute }}" != "${{ steps.latest-release.outputs.tag }}" ]; then + sed -i 's/^lute = "luau-lang\/lute@.*"/lute = "luau-lang\/lute@${{ steps.latest-release.outputs.tag }}"/' rokit.toml + echo "Updated rokit.toml:" + cat rokit.toml + echo "" + sed -i 's/^lute = { github = "luau-lang\/lute", version = ".*" }/lute = { github = "luau-lang\/lute", version = "=${{ steps.latest-release.outputs.tag }}" }/' foreman.toml + echo "Updated foreman.toml:" + cat foreman.toml + fi + + - name: Create Pull Request + if: steps.current-version.outputs.lute != steps.latest-release.outputs.tag + uses: luau-lang/create-pull-request@v7 + with: + commit-message: "chore: update dependencies" + title: "Update Lute to ${{ steps.latest-release.outputs.tag }}" + body: | + ${{ format('**Lute**: Updated from `{0}` to `{1}`', steps.current-version.outputs.lute, steps.latest-release.outputs.tag)}} + + **Release Notes:** + ${{ format('https://github.com/luau-lang/lute/releases/tag/{0}', steps.latest-release.outputs.tag)}} + + --- + *This PR was automatically created by the [Update Luau workflow](https://github.com/${{ github.repository }}/actions/workflows/update-prs.yml)* + branch: update-lute + delete-branch: true + + - name: No update needed + if: steps.current-version.outputs.lute == steps.latest-release.outputs.tag + run: | + echo "Lute is already up to date: ${{ steps.current-version.outputs.lute }}" diff --git a/.gitignore b/.gitignore index 8a03e7908..6e0df9ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,10 +13,9 @@ __pycache__ .cache .clangd +**/.DS_Store compile_commands.json *~ -extern/libuv/libuv-static.pc -extern/libuv/DartConfiguration.tcl extern/*/ generated **/generated-types.luau diff --git a/.luaurc b/.luaurc index df8328cad..01c26f8f6 100644 --- a/.luaurc +++ b/.luaurc @@ -4,5 +4,6 @@ "batteries": "./batteries", "std": "./lute/std/libs", "lute": "./definitions", + "commands": "./lute/cli/commands" } } diff --git a/.luaurc.ci b/.luaurc.ci new file mode 100644 index 000000000..99ef608b7 --- /dev/null +++ b/.luaurc.ci @@ -0,0 +1,7 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "./batteries", + "commands": "./lute/cli/commands" + } +} diff --git a/.lute/luthier.luau b/.lute/luthier.luau new file mode 100644 index 000000000..4c0c1ddbc --- /dev/null +++ b/.lute/luthier.luau @@ -0,0 +1,10 @@ +--!strict + +local process = require("@lute/process") + +local args = table.pack(...) + +local cmd = { process.execpath(), "tools/luthier.luau", table.unpack(args, 2, args.n) } + +local result = process.run(cmd, { stdio = "inherit" }) +process.exit(result.exitcode) diff --git a/CMakeBuild/get_version.cmake b/CMakeBuild/get_version.cmake index 4463e578a..fb7f2109b 100644 --- a/CMakeBuild/get_version.cmake +++ b/CMakeBuild/get_version.cmake @@ -1,4 +1,17 @@ cmake_minimum_required(VERSION 3.13) set(LUTE_VERSION 0.1.0) -message("${LUTE_VERSION}") + +set(LUTE_VERSION_SUFFIX_DEFAULT "$ENV{LUTE_VERSION_SUFFIX}") +if(NOT DEFINED LUTE_VERSION_SUFFIX AND NOT "${LUTE_VERSION_SUFFIX_DEFAULT}" STREQUAL "") + set(LUTE_VERSION_SUFFIX "${LUTE_VERSION_SUFFIX_DEFAULT}") +endif() +set(LUTE_VERSION_SUFFIX "${LUTE_VERSION_SUFFIX}" CACHE STRING "Optional suffix appended to the Lute version string") +set(LUTE_VERSION_FULL "${LUTE_VERSION}") +if(LUTE_VERSION_SUFFIX) + set(LUTE_VERSION_FULL "${LUTE_VERSION_FULL}-${LUTE_VERSION_SUFFIX}") +endif() + +# The release workflow depends on this exact message format to extract the +# version. If making changes, update the workflow accordingly. +message("${LUTE_VERSION_FULL}") diff --git a/CMakeLists.txt b/CMakeLists.txt index 78547e596..b4bf119be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # set the module path -list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeBuild") +list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeBuild") include(get_version) @@ -16,6 +16,15 @@ project( LANGUAGES CXX C ) +# Enable sanitizers globally before any subdirectory is added +option(LUTE_ENABLE_SANITIZERS "Enable sanitizers" OFF) +if (LUTE_ENABLE_SANITIZERS) + if(NOT MSVC) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) + endif() +endif() + # luau setup set(LUAU_BUILD_CLI OFF) set(LUAU_BUILD_TESTS OFF) @@ -59,6 +68,11 @@ add_subdirectory(extern/boringssl) set(BORINGSSL_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extern/boringssl") set(BORINGSSL_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/boringssl/include) +# Suppress frame-size warnings in boringssl when sanitizers are enabled +if (LUTE_ENABLE_SANITIZERS AND NOT MSVC) + target_compile_options(crypto PRIVATE -Wno-frame-larger-than) +endif() + # curl setup set(USE_LIBIDN2 OFF) @@ -66,6 +80,7 @@ set(USE_NGHTTP2 OFF) set(CURL_USE_LIBPSL OFF) set(CURL_USE_LIBSSH2 OFF) set(CURL_ZLIB ON) +SET(CURL_ZSTD OFF CACHE STRING "Disable ZSTD" FORCE) set(CURL_BROTLI OFF CACHE STRING "Disable brotli support" FORCE) set(CURL_ENABLE_SSL ON) set(CURL_USE_OPENSSL ON) @@ -90,7 +105,6 @@ include(uWebSockets) # Define include directories for uWebSockets and uSockets set(UWEBSOCKETS_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/uWebSockets/src) - # libsodium for `pwhash` include(libsodium) @@ -102,8 +116,9 @@ if(MSVC) list(APPEND LUTE_OPTIONS /D_CRT_SECURE_NO_WARNINGS) # We need to use the portable CRT functions. list(APPEND LUTE_OPTIONS "/we4018") # Signed/unsigned mismatch list(APPEND LUTE_OPTIONS "/we4388") # Also signed/unsigned mismatch - list(APPEND LUTE_OPTIONS "/Zc:externConstexpr") # MSVC does not comply with C++ standard by default - if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + list(APPEND LUTE_OPTIONS "/Zc:externConstexpr") # MSVC does not comply with C++ standard by default + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") list(APPEND LUTE_OPTIONS "/EHsc") # The CMake clang-cl target doesn't enable exceptions by default endif() # FIXME: list(APPEND LUTE_OPTIONS /WX) # Warnings are errors @@ -127,6 +142,7 @@ add_subdirectory(lute/vm) add_subdirectory(lute/process) add_subdirectory(lute/system) add_subdirectory(lute/time) +add_subdirectory(lute/io) # executables add_subdirectory(lute/cli) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..bbb44a400 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Contributing to Lute + +## Getting Started + +First off, thank you for your interest in contributing to Lute! +We're really excited about building a powerful standalone runtime for Luau, and we're glad that you are too! +Whether you're fixing bugs, improving our documentation, or posting feature requests, we value your contributions! +If you're interested in contributing code in particular, please continue reading this document and +check out the [open issues](https://github.com/luau-lang/lute/issues) on our issue tracker for possible avenues to contribute! + +## Ways to Contribute + +- **Bug Reports:** Please open a [GitHub Issue](https://github.com/luau-lang/lute/issues/new) with a clear description, label, and reproduction steps. +- **Feature Requests:** Open an issue with your idea and rationale. +- **Code Contributions:** Fork this repository, create a branch, and submit a PR (see "Code Guidelines" below). + +## Repository Structure + +Lute as a project is organized to use [GitHub](https://github.com/luau-lang/lute) for issue tracking, project milestones, managing contributions, and so forth. +Lute's [documentation site](https://luau-lang.github.io/lute) is also built from this repository, and accepts pull requests just like any other code does. + +Navigating the repository benefits from knowing its structure, namely: +- Source code lives in the [`/lute/`](./lute) directory, +- type definitions are in [`/definitions/`](./definitions), +- user-facing documentation is defined in [`/docs/`](./docs), +- supporting tooling for working on lute is provided in [`/tools/`](./tools), and finally, +- tests can be found in the [`/tests/`](./tests/) directory. + +As a codebase, Lute consists of three components: +1. The runtime itself, written in C++, defined in various modules under `/lute/` and accessible from Luau scripts via `require("@lute/module")`. + These runtime libraries provide core functionality that extends the expressivity of Luau itself, and allows programmers to write general-purpose programs at all. +2. The standard library, written in Luau, defined under `/lute/std/libs` and _embedded into the Lute executable_. + The standard library provides a general interface for programming in Luau, one that we strive to make as delightful as it can be. + It includes both wrappers of runtime functionality that allow runtime authors to support cross-runtime code, as well as + general Luau libraries that improve the developer experience and increase developer productivity on new projects. +3. batteries, written in Luau, defined in various modules under `/batteries/` which are not included in any formal distribution of Lute. + A goal of Lute as a project is to build the foundations for a healthy open source ecosystem for Luau, and achieving that means building + core developer tooling like _a package manager_. In the mean time, we need to write some Luau code to help us build the future, and we + need a place for it to live. batteries are exactly that! Useful Luau code that should run everywhere, and that we can use to build our tooling. + Once Luau has a proper package manager built on Lute, batteries will be pulled out of the repository and made available as a separate package. + +## Code Guidelines + +In the interest of having a consistent standard for code, we expect that incoming contributions will follow existing code styling. +In support of this, we've provided appropriate configuration for autoformatter tools to help enforce this. +For C++ code, we provide a `.clang-format` file; please make sure to format your code before opening a PR. +All Luau scripts should be formatted with [StyLua](https://github.com/JohnnyMorganz/StyLua). + +There are some additional style considerations that we do not have automated enforcement for today, but that we will address in reviews: + +1. All Luau APIs exposed publically by Lute in both the runtime and the standard library must have identifiers written in `luacase`. + This means module names, function names, table fields, exported types, etc. should be written in all lowercase, and should be named ideally with + one or two words succinctly. We know this style is not everyone's favorite, and we do not advocate for external software to use it, but we would like + to retain consistency with Luau's builtin library which inherits this convention from Lua. +2. All other Luau code, including the internals of those Luau APIs, should be written using `camelCase` for identifiers and table fields, and `PascalCase` for type names. +3. Every functionality change should come with tests that express the desired behavior of the code being added. +4. Tests should be placed along side existing tests, and use the appropriate testing tools provided for Lute. + C++ code should be tested using `doctest` and linked into a test executable. + Luau code should be tested using Lute's testing framework, and should be written in a `.test.luau` file. +5. Small, incremental contributions are always preferred over sweeping changes. You should expect that any large sweeping change will be rejected summarily without review. + If you're interested in working on something that _requires_ such a change, you should open an issue first to discuss the idea and get buy-in from the team. + +## Pull Request Guidelines + +When submitting a pull request: + +1. **PR Title:** Your PR title should be a clear, one-line sentence that makes sense as a changelog entry. This title will appear in the release notes, so write it to be informative to users. Focus on what changed from a user's perspective. + - Good: "Adds support for custom error handlers in the CLI" + - Good: "Fixes memory leak in filesystem operations" + - Bad: "Update code" + - Bad: "WIP: testing changes" + +2. **PR Labels:** Please label your PR with one of the following labels so it appears in the correct section of the release notes: + - `documentation` - Documentation improvements + - `std` - Changes to the standard library + - `batteries` - Changes to batteries (additional libraries that are useful for writing lute code) + - `cli` - Changes to the command line - either the c++ subcommands (run, compile, etc) or lute tooling (lint, transform, test) + - `runtime` - Changes to the C++ runtime + - `infra` - Changes to CI/CD, build system, or project infrastructure + - `bug` - Bug fixes + +## Licensing + +By providing code in an issue or opening a pull request, you agree to license that code under the MIT License, and indicate that you have the legal right to do so. diff --git a/LICENSE b/LICENSE index d3e2c46f5..d33f22ed8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Roblox Corporation +Copyright (c) 2024–2025 Roblox Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index ef1376b32..2eacddd8e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Lute [![CI](https://github.com/luau-lang/lute/actions/workflows/ci.yml/badge.svg)](https://github.com/luau-lang/lute/actions/workflows/ci.yml) -==== +# Lute [![CI](https://github.com/luau-lang/lute/actions/workflows/ci.yml/badge.svg)](https://github.com/luau-lang/lute/actions/workflows/ci.yml) Lute is a standalone runtime for general-purpose programming in [Luau](https://luau.org), and a collection of optional extension libraries for Luau embedders to include to expand the capabilities of the Luau scripts in their software. It is designed to make it readily feasible to use Luau to write any sort of general-purpose programs, including manipulating files, making network requests, opening sockets, and even making tooling that directly manipulates Luau scripts. @@ -11,8 +10,49 @@ We would love to hear from you about your experiences working with other Luau or ### Lute Libraries The Lute repository fundamentally contains three sets of libraries. These are as follows: + - `lute`: The core runtime libraries in C++, which provides the basic functionality for general-purpose Luau programming. - `std`: The standard library, which extends those core C++ libraries with additional functionality in Luau. - `batteries`: A collection of useful, standalone Luau libraries that do not depend on `lute`. Contributions to any of these libraries are welcome, and we encourage you to open issues or pull requests if you have any feedback or contributions to make. + +## Building Lute + +Lute has a fairly conventional C++ build system built atop CMake. However, in the interest of dogfooding Lute itself, and avoiding the trap of shipping elaborate, difficult-to-maintain CMake configurations that attempt to perform dependency resolution and code generation, we've written a build tool called `luthier` (located at `./tools/luthier.luau`). +`luthier` is written to appropriately run or re-run any of the steps in the build process as needed based on local changes, which affords a more pleasant developer experience for folks working on `lute` than invoking each step manually. Some +`luthier` subcommands like `configure` and `build` just wrap the standard CMake configuration and ninja invocations. Other commands include `fetch` which implements the logic to parse dependency information from the TOML files (named `extern/\*.tune`) and resolve them efficiently using `git`, and `generate` which performs the code generation steps necessary to embed both Lute's CLI frontend commands and Lute's standard library into the executable. +The `generate` step in particular is necessary to producing a full `lute` executable, but we provide empty versions of both embeddings (in `./tools/templates`) to be used for any builds that do _not_ embed the standard library. Since you'll need `lute` to execute `luthier` and you'll need `luthier` to run the code generation step in particular, there are a few different paths to building Lute. + +### Building Lute with `lute` installed + +The most straightforward, and generally recommended, way to build a local version of `lute` is to have already installed an existing version of `lute`. Today, you can do that using a toolchain manager like [`foreman`](https://github.com/Roblox/foreman), [`rokit`](https://github.com/rojo-rbx/rokit), and [mise-en-place](https://mise.jdx.dev/), or by manually installing a prebuilt binary from our [Releases](https://github.com/luau-lang/lute/releases). With a copy of `lute` present on your system, you can then run the following to perform a clean build: + +```bash +# with `lute` on your path... +lute tools/luthier.luau build --clean {lute | Lute.CLI | Lute.Test} +# or referring directly to a specific location... +/path/to/lute tools/luthier.luau build --clean {lute | Lute.CLI | Lute.Test} +# you can also use `run`, instead of `build`, to also invoke the appropriate executable afterwards! +``` + +### Building Lute from scratch + +If you wish to build `lute` from scratch without a version of `lute` already present on your machine, this is still possible! We've provided a small, easily auditable shell script `./tools/bootstrap.sh` that will perform the entire build process in sequence for a totally fresh build. This entails first building a debug version of `lute` called `lute0` without any of the CLI commands implemented in Luau, and without the standard library embedded into the executable. We can then use `lute0` to run `luthier`, perform the requisite code generation step, and then build a fresh release version of `lute`. The script supports a single command-line option `--install` which can be used to install this release executable to a desired location on your machine. By default, this is `$HOME/.lute/bin/lute`, but the script will provide a prompt during its execution about where `lute` should be placed. In order to then use this `lute` executable, please ensure that it is accessible on your `$PATH`. + +### Manually building Lute + +If you wish to work very manually with the build system, you can, of course, still invoke `cmake` and `ninja` directly after pulling external dependencies into `extern` by hand. In order for the build to succeed, you'll have to provide _some_ version of a handful of generated files, but we provide empty versions to use in `./tools/templates`. A manual build therefore would look something like: + +- Copy all of the templates from `./tools/templates` into the right locations to support building a version with no embedded Luau functionality, e.g. + ```bash + mkdir -p ./lute/std/src/generated + cp ./tools/templates/std_impl.cpp ./lute/std/src/generated/modules.cpp + cp ./tools/templates/std_header.h ./lute/std/src/generated/modules.h + mkdir -p ./lute/cli/generated + cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp + cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h + ``` +- Configure with `cmake -G=Ninja -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1` +- Build a `lute` executable with `ninja -C build lute/cli/lute` or our test suite with `ninja -C build tests/lute-tests`. +- Optionally, use this version to run `luthier generate` to generate the files for embedded Luau functionality. diff --git a/batteries/base64.luau b/batteries/base64.luau index b582b764f..4978f155a 100644 --- a/batteries/base64.luau +++ b/batteries/base64.luau @@ -1,189 +1,142 @@ ---[[ - Pulled from https://github.com/Reselim/Base64 - - Copyright (c) 2020 Reselim - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -]] - ---!native --!optimize 2 +--!strict + +local BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +local BASE64_ENCODING_LUT = table.create(4096) :: { number } +local BASE64_DECODING_LUT = buffer.create(256) + +do + for i = 0, 4095 do + local hi = bit32.rshift(i, 6) + 1 + local lo = bit32.band(i, 0x3F) + 1 + BASE64_ENCODING_LUT[i + 1] = + bit32.bor(string.byte(BASE64_ALPHABET, hi), bit32.lshift(string.byte(BASE64_ALPHABET, lo), 8)) + end -local lookupValueToCharacter = buffer.create(64) -local lookupCharacterToValue = buffer.create(256) + -- prefill lookup table with invalid base64 value (0xFF) + buffer.fill(BASE64_DECODING_LUT, 0, 0xFF) + for i = 1, #BASE64_ALPHABET do + buffer.writeu8(BASE64_DECODING_LUT, string.byte(BASE64_ALPHABET, i), i - 1) + end +end -local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -local padding = string.byte("=") +@native +local function encode(input_buffer: buffer): buffer + assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") -for index = 1, 64 do - local value = index - 1 - local character = string.byte(alphabet, index) + local input_length = buffer.len(input_buffer) - buffer.writeu8(lookupValueToCharacter, value, character) - buffer.writeu8(lookupCharacterToValue, character, value) -end + if input_length == 0 then + return buffer.create(0) + end -local function encode(input: buffer): buffer - local inputLength = buffer.len(input) - local inputChunks = math.ceil(inputLength / 3) + local output = buffer.create(((input_length + 2) // 3) * 4) + local output_idx = 0 + local i = 0 - local outputLength = inputChunks * 4 - local output = buffer.create(outputLength) + while i + 3 <= input_length do + local triple = bit32.bor( + bit32.lshift(buffer.readu8(input_buffer, i), 16), + bit32.lshift(buffer.readu8(input_buffer, i + 1), 8), + buffer.readu8(input_buffer, i + 2) + ) - -- Since we use readu32 and chunks are 3 bytes large, we can't read the last chunk here - for chunkIndex = 1, inputChunks - 1 do - local inputIndex = (chunkIndex - 1) * 3 - local outputIndex = (chunkIndex - 1) * 4 + local high = bit32.band(bit32.rshift(triple, 12), 0xFFF) + 1 + local low = bit32.band(triple, 0xFFF) + 1 - local chunk = bit32.byteswap(buffer.readu32(input, inputIndex)) + local pair0 = BASE64_ENCODING_LUT[high] + local pair1 = BASE64_ENCODING_LUT[low] - -- 8 + 24 - (6 * index) - local value1 = bit32.rshift(chunk, 26) - local value2 = bit32.band(bit32.rshift(chunk, 20), 0b111111) - local value3 = bit32.band(bit32.rshift(chunk, 14), 0b111111) - local value4 = bit32.band(bit32.rshift(chunk, 8), 0b111111) + buffer.writeu32(output, output_idx, bit32.bor(pair0, bit32.lshift(pair1, 16))) - buffer.writeu8(output, outputIndex, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputIndex + 1, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputIndex + 2, buffer.readu8(lookupValueToCharacter, value3)) - buffer.writeu8(output, outputIndex + 3, buffer.readu8(lookupValueToCharacter, value4)) + i += 3 + output_idx += 4 end - local inputRemainder = inputLength % 3 - - if inputRemainder == 1 then - local chunk = buffer.readu8(input, inputLength - 1) - - local value1 = bit32.rshift(chunk, 2) - local value2 = bit32.band(bit32.lshift(chunk, 4), 0b111111) - - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputLength - 2, padding) - buffer.writeu8(output, outputLength - 1, padding) - elseif inputRemainder == 2 then - local chunk = - bit32.bor(bit32.lshift(buffer.readu8(input, inputLength - 2), 8), buffer.readu8(input, inputLength - 1)) - - local value1 = bit32.rshift(chunk, 10) - local value2 = bit32.band(bit32.rshift(chunk, 4), 0b111111) - local value3 = bit32.band(bit32.lshift(chunk, 2), 0b111111) - - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) - buffer.writeu8(output, outputLength - 1, padding) - elseif inputRemainder == 0 and inputLength ~= 0 then - local chunk = bit32.bor( - bit32.lshift(buffer.readu8(input, inputLength - 3), 16), - bit32.lshift(buffer.readu8(input, inputLength - 2), 8), - buffer.readu8(input, inputLength - 1) - ) + local rem = input_length - i - local value1 = bit32.rshift(chunk, 18) - local value2 = bit32.band(bit32.rshift(chunk, 12), 0b111111) - local value3 = bit32.band(bit32.rshift(chunk, 6), 0b111111) - local value4 = bit32.band(chunk, 0b111111) + if rem == 1 then + local high = bit32.band(bit32.lshift(buffer.readu8(input_buffer, i), 4), 0xFF0) - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) - buffer.writeu8(output, outputLength - 1, buffer.readu8(lookupValueToCharacter, value4)) + local TWO_EQUALS = 0x3D3D + buffer.writeu32(output, output_idx, bit32.bor(BASE64_ENCODING_LUT[high + 1], bit32.lshift(TWO_EQUALS, 16))) + elseif rem == 2 then + local first = buffer.readu8(input_buffer, i) + local second = buffer.readu8(input_buffer, i + 1) + local high = bit32.bor(bit32.lshift(first, 4), bit32.rshift(second, 4)) + local low_idx = bit32.lshift(bit32.band(second, 0x0F), 2) + local low_equals = bit32.bor(string.byte(BASE64_ALPHABET, low_idx + 1), bit32.lshift(0x3D, 8)) + + buffer.writeu32(output, output_idx, bit32.bor(BASE64_ENCODING_LUT[high + 1], bit32.lshift(low_equals, 16))) end return output end -local function decode(input: buffer): buffer - local inputLength = buffer.len(input) - local inputChunks = math.ceil(inputLength / 4) - - -- TODO: Support input without padding - local inputPadding = 0 - if inputLength ~= 0 then - if buffer.readu8(input, inputLength - 1) == padding then - inputPadding += 1 - end - if buffer.readu8(input, inputLength - 2) == padding then - inputPadding += 1 - end - end - - local outputLength = inputChunks * 3 - inputPadding - local output = buffer.create(outputLength) +@native +local function decode(input_buffer: buffer): buffer + assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") - for chunkIndex = 1, inputChunks - 1 do - local inputIndex = (chunkIndex - 1) * 4 - local outputIndex = (chunkIndex - 1) * 3 + local input_length = buffer.len(input_buffer) - local value1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex)) - local value2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 1)) - local value3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 2)) - local value4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 3)) - - local chunk = bit32.bor(bit32.lshift(value1, 18), bit32.lshift(value2, 12), bit32.lshift(value3, 6), value4) - - local character1 = bit32.rshift(chunk, 16) - local character2 = bit32.band(bit32.rshift(chunk, 8), 0b11111111) - local character3 = bit32.band(chunk, 0b11111111) - - buffer.writeu8(output, outputIndex, character1) - buffer.writeu8(output, outputIndex + 1, character2) - buffer.writeu8(output, outputIndex + 2, character3) + if input_length == 0 then + return buffer.create(0) end - if inputLength ~= 0 then - local lastInputIndex = (inputChunks - 1) * 4 - local lastOutputIndex = (inputChunks - 1) * 3 - - local lastValue1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex)) - local lastValue2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 1)) - local lastValue3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 2)) - local lastValue4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 3)) - - local lastChunk = bit32.bor( - bit32.lshift(lastValue1, 18), - bit32.lshift(lastValue2, 12), - bit32.lshift(lastValue3, 6), - lastValue4 - ) - - if inputPadding <= 2 then - local lastCharacter1 = bit32.rshift(lastChunk, 16) - buffer.writeu8(output, lastOutputIndex, lastCharacter1) + -- strip padding (rfc-4648 section 3.3: the excess pad characters MAY also be ignored) + while input_length > 0 and buffer.readu8(input_buffer, input_length - 1) == 0x3D do + input_length -= 1 + end - if inputPadding <= 1 then - local lastCharacter2 = bit32.band(bit32.rshift(lastChunk, 8), 0b11111111) - buffer.writeu8(output, lastOutputIndex + 1, lastCharacter2) + -- rfc-4648 section 3.3 forbids padding that isn't preceded by at least one Base64 digit + if input_length == 0 then + error("Invalid base64 input", 2) + end - if inputPadding == 0 then - local lastCharacter3 = bit32.band(lastChunk, 0b11111111) - buffer.writeu8(output, lastOutputIndex + 2, lastCharacter3) - end - end + -- get correct output size + local output_length = (3 * input_length) // 4 + local output = buffer.create(output_length) + + local read_offset = 0 + local write_offset = 0 + -- loop invariant: at least 4 bytes to write + while write_offset + 4 <= output_length do + local b4 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset + 3)) + local b3 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset + 2)) + local b2 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset + 1)) + local b1 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset)) + if bit32.bor(b1, b2, b3, b4) >= 64 then + error("Invalid base64 input", 2) end + read_offset += 4 + -- u32 BE: [B1, B2, B3, 0] = b1<<26|b2<<20|b3<<14|b4<<8, trailing 0 will be overwritten next iteration + buffer.writeu32(output, write_offset, bit32.byteswap(b1 * 0x4000000 + b2 * 0x100000 + b3 * 0x4000 + b4 * 0x100)) + write_offset += 3 end + local u24be, nbits = 0, 0 + while read_offset < input_length do + local b = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset)) + read_offset += 1 + if b >= 64 then + error("Invalid base64 input", 2) + end + u24be = u24be * 0x40 + b + nbits += 6 + end + while nbits >= 8 do + buffer.writeu8(output, write_offset, bit32.rshift(u24be, nbits - 8)) + nbits -= 8 + write_offset += 1 + end + -- 2 or 4 leftover bits must be zero + if nbits == 6 or (nbits == 2 and bit32.btest(u24be, 0x03)) or (nbits == 4 and bit32.btest(u24be, 0x0F)) then + error("Invalid base64 input", 2) + end return output end -return { +return table.freeze({ encode = encode, decode = decode, -} +}) diff --git a/batteries/cli.luau b/batteries/cli.luau index 28018634d..178637f82 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -151,7 +151,7 @@ function cli.parse(self: Parser, args: { string }): () end end -function cli.get(self: Parser, name: string): string +function cli.get(self: Parser, name: string): string? return self.parsed.values[name] end diff --git a/batteries/collections/deque.luau b/batteries/collections/deque.luau new file mode 100644 index 000000000..a78d085b1 --- /dev/null +++ b/batteries/collections/deque.luau @@ -0,0 +1,70 @@ +export type Deque = { + _head: number, + _tail: number, + _values: { [number]: T }, + pushfront: (self: Deque, value: T) -> (), + pushback: (self: Deque, value: T) -> (), + popfront: (self: Deque) -> T, + popback: (self: Deque) -> T, + peekfront: (self: Deque) -> T?, + peekback: (self: Deque) -> T?, +} + +local deque = {} +deque.__index = deque +deque.__len = function(self: Deque) + return self._tail - self._head + 1 +end + +local dequeLib = {} + +function dequeLib.new(initValue: T?): Deque + local self = {} :: Deque + self._values = {} + self._head, self._tail = 0, if initValue then 0 else -1 + if initValue then + self._values[self._head] = initValue + end + + return setmetatable(self, deque) +end + +function deque.pushfront(self: Deque, value: T) + self._head -= 1 + self._values[self._head] = value +end + +function deque.pushback(self: Deque, value: T) + self._tail += 1 + self._values[self._tail] = value +end + +function deque.popfront(self: Deque): T + if self._tail < self._head then + error("Popping from empty deque") + end + local toReturn = self._values[self._head] + self._values[self._head] = nil + self._head += 1 + return toReturn +end + +function deque.popback(self: Deque): T + if self._tail < self._head then + error("Popping from empty deque") + end + local toReturn = self._values[self._tail] + self._values[self._tail] = nil + self._tail -= 1 + return toReturn +end + +function deque.peekfront(self: Deque) + return self._values[self._head] +end + +function deque.peekback(self: Deque) + return self._values[self._tail] +end + +return table.freeze(dequeLib) diff --git a/batteries/difftext/init.luau b/batteries/difftext/init.luau new file mode 100644 index 000000000..4838d2b17 --- /dev/null +++ b/batteries/difftext/init.luau @@ -0,0 +1,19 @@ +local myersdiff = require("@self/myersdiff") +local printdiff = require("@self/printdiff") +local types = require("@self/types") + +export type diff = types.diff +export type diffoptions = types.diffoptions + +local function diff(a: string, b: string, options: diffoptions): diff + if options and options.byChar then + return myersdiff.myersdiffbychar(a, b) + else + return myersdiff.myersdiffbyline(a, b) + end +end + +return { + diff = diff, + prettydiff = printdiff, +} diff --git a/batteries/difftext/myersdiff.luau b/batteries/difftext/myersdiff.luau new file mode 100644 index 000000000..791a6bce6 --- /dev/null +++ b/batteries/difftext/myersdiff.luau @@ -0,0 +1,143 @@ +local deque = require("@batteries/collections/deque") +local tableext = require("@std/tableext") +local types = require("./types") + +type diffoperation = types.diffoperation +type diff = types.diff + +type EditGraphNode = { + x: number, + y: number, + prev: EditGraphNode?, +} + +local function editGraphNode(x: number, y: number, prev: EditGraphNode?): EditGraphNode + return { + x = x, + y = y, + prev = prev, + } +end + +-- true if a[x+1] == b[y+1]; indicates an EQUAL operation (no diff) is valid +local function hasDiagonalEdge(node: EditGraphNode, a: string | { string }, b: string | { string }): boolean + local x, y = node.x, node.y + if typeof(a) == "table" then + if typeof(b) == "table" then + return x < #a and y < #b and a[x + 1] == b[y + 1] + else + error("hasDiagonalEdge inputs should have the same type.") + end + elseif typeof(b) == "table" then + error("hasDiagonalEdge inputs should have the same type.") + end + + return x < #a and y < #b and a:sub(x + 1, x + 1) == b:sub(y + 1, y + 1) +end + +local function edgeTodiff(curNode: EditGraphNode, a: string | { string }, b: string | { string }): diffoperation? + -- maps stepping from curNode.prev -> curNode to a diff operation + -- (n-1, n) -> (n, n) == DELETION of a[n] + -- (n, n-1) -> (n, n) == ADDITION of b[n] + -- (n-1, n-1) -> (n, n) == EQUAL (a[n] == b[n]) + if not curNode.prev then + return nil + end + + local prev = curNode.prev + local xdiff = curNode.x - prev.x + local ydiff = curNode.y - prev.y + if xdiff == 1 and ydiff == 1 then + return { + key = "EQUAL", + text = if typeof(a) == "table" then a[curNode.x] else a:sub(curNode.x, curNode.x), + } + elseif xdiff == 1 then + return { + key = "DELETE", + text = if typeof(a) == "table" then a[curNode.x] else a:sub(curNode.x, curNode.x), + } + else + return { + key = "ADD", + text = if typeof(b) == "table" then b[curNode.y] else b:sub(curNode.y, curNode.y), + } + end +end + +local function myersdiff(a: string | { string }, b: string | { string }): diff + local start = editGraphNode(0, 0) + local editGraphDeque = deque.new(start) + + local diff = {} :: diff + local seen = {} :: { [string]: true } + seen["0,0"] = true + + while #editGraphDeque do + local curNode = editGraphDeque:popfront() + + -- we've reached bottom right vertex; indicates full edit from a -> b + if curNode.x == #a and curNode.y == #b then + -- reconstruct diff operations by tracing path from termination node to start + local edgeOp = edgeTodiff(curNode, a, b) + while edgeOp do + table.insert(diff, edgeOp) + curNode = curNode.prev + edgeOp = if curNode then edgeTodiff(curNode, a, b) else nil + end + break + end + + -- add neighbors (diag prioritzed, then deletion, then insertion) + if hasDiagonalEdge(curNode, a, b) then + -- algorithm optimizes for diagonal edges since they represent no diff. We want the shortest-edit-sequence (least # of diff operations) + -- so diagonal edges representing equality in a[n+1] & b[n+1] is always preferred + local edgePositionStr = `{curNode.x + 1},{curNode.y + 1}` + if not seen[edgePositionStr] then + seen[edgePositionStr] = true + editGraphDeque:pushfront(editGraphNode(curNode.x + 1, curNode.y + 1, curNode)) + end + continue + end + + if curNode.x < #a then + local edgePositionStr = `{curNode.x + 1},{curNode.y}` + if not seen[edgePositionStr] then + seen[edgePositionStr] = true + editGraphDeque:pushback(editGraphNode(curNode.x + 1, curNode.y, curNode)) + end + end + + if curNode.y < #b then + local edgePositionStr = `{curNode.x},{curNode.y + 1}` + if not seen[edgePositionStr] then + seen[edgePositionStr] = true + editGraphDeque:pushback(editGraphNode(curNode.x, curNode.y + 1, curNode)) + end + end + end + + tableext.reverse(diff, true) + return diff +end + +local function myersdiffbyline(a: string | { string }, b: string | { string }): diff + local src = a + local dest = b + if typeof(a) == "string" then + src = a:split("\n") + end + if typeof(b) == "string" then + dest = b:split("\n") + end + return myersdiff(src, dest) +end + +local function myersdiffbychar(a: string, b: string): diff + return myersdiff(a, b) +end + +return table.freeze({ + myersdiffbyline = myersdiffbyline, + myersdiffbychar = myersdiffbychar, +}) diff --git a/batteries/difftext/printdiff.luau b/batteries/difftext/printdiff.luau new file mode 100644 index 000000000..88d7b109a --- /dev/null +++ b/batteries/difftext/printdiff.luau @@ -0,0 +1,218 @@ +local richterm = require("@batteries/richterm") +local myersdiff = require("./myersdiff") +local myersdiffbychar = myersdiff.myersdiffbychar +local myersdiffbyline = myersdiff.myersdiffbyline +local types = require("./types") +local brightGreen = richterm.brightGreen +local brightRed = richterm.brightRed +local bgGreen = richterm.bgGreen +local bgRed = richterm.bgRed + +type diff = types.diff + +-- Collects consecutive DELETEs and/or ADDs starting at index i +-- Returns: deletes array, adds array, new index position +local function collectConsecutiveChanges(diff: diff, startIndex: number): ({ string }, { string }, number) + local i = startIndex + local deletes = {} + while i <= #diff and diff[i].key == "DELETE" do + table.insert(deletes, diff[i].text) + i += 1 + end + + local adds = {} + while i <= #diff and diff[i].key == "ADD" do + table.insert(adds, diff[i].text) + i += 1 + end + + return deletes, adds, i +end + +local function visualizeCharDiff(a: string, b: string): (string, string) + local charOps = myersdiffbychar(a, b) + -- diffs two strings by char and returns colored visual for src and destination text + local srcVisual = {} + local destVisual = {} + + for _, op in charOps do + if op.key == "EQUAL" then + table.insert(srcVisual, brightRed(op.text)) + table.insert(destVisual, brightGreen(op.text)) + elseif op.key == "DELETE" then + table.insert(srcVisual, bgRed(op.text)) + elseif op.key == "ADD" then + table.insert(destVisual, bgGreen(op.text)) + end + end + + return table.concat(srcVisual, ""), table.concat(destVisual, "") +end + +local function formatLineSideHeader( + operation: types.diffoperationkey, + maxNumWidth: number, + lineNumbers: { + oldLine: number?, + newLine: number?, + } +): string + -- side header structure: + -- if line numbers included: + -- ALWAYS has length maxNumWidth*2 + 4 + -- the padded structure is essentialy: + -- {operation} {oldLine# padded to len of maxNumWidth} {newLine# padded to len = maxNumWidth}| + + local oldStr = tostring(if lineNumbers then lineNumbers.oldLine else "") + local newStr = tostring(if lineNumbers then lineNumbers.newLine else "") + maxNumWidth = maxNumWidth or 1 + if operation == "EQUAL" then + return string.format(` %{maxNumWidth}s %{maxNumWidth}s| `, oldStr, newStr) + elseif operation == "ADD" then + return string.format(`+ %{maxNumWidth}s %{maxNumWidth}s| `, "", newStr) + elseif operation == "DELETE" then + return string.format(`- %{maxNumWidth}s %{maxNumWidth}s| `, oldStr, "") + end + + error(`formatLineSideHeader called with invalid diff operation key: {operation}`) +end + +local function printDiffByLineDetailed(a: string, b: string, includeLineNumbers: boolean?): string + local src, dest = a:split("\n"), b:split("\n") + local maxNumLines = math.max(#src, #dest) + local maxLineNumberLen = math.floor(math.log10(maxNumLines)) + 1 + local diff = myersdiffbyline(src, dest) + + local i = 1 + local result: { string } = {} + local oldLine, newLine = 1, 1 + + -- Helper to format line numbers + local function lineNums(old: number?, new: number?) + return { oldLine = old, newLine = new } + end + + while i <= #diff do + local op = diff[i] + + if op.key == "DELETE" or op.key == "ADD" then + local deletes, adds + deletes, adds, i = collectConsecutiveChanges(diff, i) + local pair = #deletes == 1 and #adds == 1 + if pair then + local srcLine, destLine = visualizeCharDiff(deletes[1], adds[1]) + if includeLineNumbers then + table.insert( + result, + brightRed(formatLineSideHeader("DELETE", maxLineNumberLen, lineNums(oldLine, nil))) .. srcLine + ) + table.insert( + result, + brightGreen(formatLineSideHeader("ADD", maxLineNumberLen, lineNums(nil, newLine))) .. destLine + ) + oldLine, newLine = oldLine + 1, newLine + 1 + else + table.insert(result, brightRed("- ") .. srcLine) + table.insert(result, brightGreen("+ ") .. destLine) + end + else + for j, deleted in deletes do + if includeLineNumbers then + table.insert( + result, + brightRed( + formatLineSideHeader("DELETE", maxLineNumberLen, lineNums(oldLine, nil)) .. deleted + ) + ) + oldLine += 1 + else + table.insert(result, brightRed("- " .. deleted)) + end + end + for j, added in adds do + if includeLineNumbers then + table.insert( + result, + brightGreen(formatLineSideHeader("ADD", maxLineNumberLen, lineNums(nil, newLine)) .. added) + ) + newLine += 1 + else + table.insert(result, brightGreen("+ " .. added)) + end + end + end + else -- EQUAL + table.insert( + result, + if includeLineNumbers + then formatLineSideHeader("EQUAL", maxLineNumberLen, lineNums(oldLine, newLine)) .. op.text + else " " .. op.text + ) + oldLine, newLine = oldLine + 1, newLine + 1 + i += 1 + end + end + return table.concat(result, "\n") +end + +local function prettydiff( + a: string, + b: string, + options: { + detailed: boolean?, + includeLineNumbers: boolean?, + }? +): string + if options and options.detailed then + return printDiffByLineDetailed(a, b, options.includeLineNumbers) + end + + local result = {} :: { string } + local src, dest = a:split("\n"), b:split("\n") + local diff = myersdiffbyline(src, dest) + if options and options.includeLineNumbers then + -- Helper to format line number tables so we don't get too verbose + local function lineNums(old: number?, new: number?) + return { oldLine = old, newLine = new } + end + + local maxNumLines = math.max(#src, #dest) + local maxLineNumberLen = math.floor(math.log10(maxNumLines)) + 1 + local oldLine, newLine = 1, 1 + + for i, op in diff do + if op.key == "ADD" then + table.insert( + result, + brightGreen(formatLineSideHeader(op.key, maxLineNumberLen, lineNums(nil, newLine)) .. op.text) + ) + newLine += 1 + elseif op.key == "DELETE" then + table.insert( + result, + brightRed(formatLineSideHeader(op.key, maxLineNumberLen, lineNums(oldLine, nil)) .. op.text) + ) + oldLine += 1 + else -- EQUAL + table.insert( + result, + formatLineSideHeader(op.key, maxLineNumberLen, lineNums(oldLine, newLine)) .. op.text + ) + oldLine, newLine = oldLine + 1, newLine + 1 + end + end + else + for i, op in diff do + if op.key == "ADD" then + table.insert(result, brightGreen("+ " .. op.text)) + elseif op.key == "DELETE" then + table.insert(result, brightRed("- " .. op.text)) + else -- EQUAL + table.insert(result, " " .. op.text) + end + end + end + return table.concat(result, "\n") +end + +return prettydiff diff --git a/batteries/difftext/types.luau b/batteries/difftext/types.luau new file mode 100644 index 000000000..19be452e2 --- /dev/null +++ b/batteries/difftext/types.luau @@ -0,0 +1,14 @@ +export type diffoperationkey = "EQUAL" | "ADD" | "DELETE" + +export type diffoperation = { + key: diffoperationkey, + text: string, +} + +export type diff = { diffoperation } + +export type diffoptions = { + byChar: boolean?, +}? + +return table.freeze({}) diff --git a/batteries/toml.luau b/batteries/toml.luau index b6a191adc..4971c763b 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -159,6 +159,7 @@ local function deserialize(input: string) elseif value == "-inf" then value = -math.huge elseif value == "nan" then + --lute-lint-ignore(divide_by_zero) value = 0 / 0 end diff --git a/definitions/crypto.luau b/definitions/crypto.luau index 821e95548..b3a37fab3 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -1,20 +1,41 @@ local crypto = {} -export type Hash = { __hash: T } - -crypto.hash = { - md5 = {} :: Hash<"md5">, - sha1 = {} :: Hash<"sha1">, - sha256 = {} :: Hash<"sha256">, - sha512 = {} :: Hash<"sha512">, - blake2b256 = {} :: Hash<"blake2b256">, +export type hash = { __hash: T } + +crypto.hash = table.freeze({ + md5 = {} :: hash<"md5">, + sha1 = {} :: hash<"sha1">, + sha256 = {} :: hash<"sha256">, + sha512 = {} :: hash<"sha512">, + blake2b256 = {} :: hash<"blake2b256">, +}) + +function crypto.digest(hash: hash, message: string | buffer): buffer + error("not implemented") +end + +crypto.secretbox = {} + +export type secretbox = { + read ciphertext: buffer, + read nonce: buffer, + read key: buffer, } -crypto.password = {} -function crypto.digest(hash: Hash, message: string | buffer): buffer +function crypto.secretbox.keygen(): buffer error("not implemented") end +function crypto.secretbox.seal(message: string | buffer, key: buffer?): secretbox + error("not implemented") +end + +function crypto.secretbox.open(box: secretbox): buffer + error("not implemented") +end + +crypto.password = {} + function crypto.password.hash(password: string): buffer error("not implemented") end diff --git a/definitions/fs.luau b/definitions/fs.luau index 4dbfe83ed..8ebe3b6fd 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -12,9 +12,9 @@ export type FileMetadata = { type: FileType, permissions: { readonly: boolean }, size: number, - created_at: any, - accessed_at: any, - modified_at: any, + created: any, + accessed: any, + modified: any, } export type DirectoryEntry = { @@ -91,16 +91,4 @@ function fs.rmdir(path: string): () error("not implemented") end -function fs.readfiletostring(filepath: string): string - error("not implemented") -end - -function fs.writestringtofile(filepath: string, contents: string): () - error("not implemented") -end - -function fs.readasync(filepath: string): string - error("not implemented") -end - return fs diff --git a/definitions/io.luau b/definitions/io.luau new file mode 100644 index 000000000..32fd620dc --- /dev/null +++ b/definitions/io.luau @@ -0,0 +1,7 @@ +local io = {} + +function io.read(): string + error("unimplemented") +end + +return io diff --git a/definitions/luau.luau b/definitions/luau.luau index 2318ae9f6..3b605073b 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,30 +1,38 @@ local luau = {} -export type Position = { - line: number, - column: number, +-- this is a userdata, not a table, but it has this interface +type spandata = { + beginline: number, + begincolumn: number, + endline: number, + endcolumn: number, } - -export type Location = { - begin: Position, - ["end"]: Position, -- TODO: do we really want to have to use brackets everywhere? +type spanMT = { + __lt: (a: spandata, b: spandata) -> boolean, } +export type span = setmetatable + +luau.span = {} + +function luau.span.create(tbl: { beginline: number, begincolumn: number, endline: number, endcolumn: number }): span + error("not implemented") +end export type Whitespace = { tag: "whitespace", - location: Location, + location: span, text: string, } export type SingleLineComment = { tag: "comment", - location: Location, + location: span, text: string, } export type MultiLineComment = { tag: "blockcomment", - location: Location, + location: span, text: string, -- TODO: depth: number, } @@ -32,10 +40,11 @@ export type MultiLineComment = { export type Trivia = Whitespace | SingleLineComment | MultiLineComment export type Token = { - leadingTrivia: { Trivia }, - position: Position, + leadingtrivia: { Trivia }, + location: span, text: Kind, - trailingTrivia: { Trivia }, + trailingtrivia: { Trivia }, + istoken: true, } export type Eof = Token<""> & { tag: "eof" } @@ -44,6 +53,8 @@ export type Pair = { node: T, separator: Token? } export type Punctuated = { Pair } export type AstLocal = { + location: span, + kind: "local", name: Token, colon: Token<":">?, annotation: AstType?, @@ -51,119 +62,154 @@ export type AstLocal = { } export type AstExprGroup = { + location: span, tag: "group", - openParens: Token<"(">, + openparens: Token<"(">, expression: AstExpr, - closeParens: Token<")">, + closeparens: Token<")">, } -export type AstExprConstantNil = Token<"nil"> & { tag: "nil" } +export type AstExprConstantNil = Token<"nil"> & { location: span, kind: "expr", tag: "nil" } -export type AstExprConstantBool = Token<"true" | "false"> & { tag: "boolean", value: boolean } - -export type AstExprConstantNumber = Token & { tag: "number", value: number } +export type AstExprConstantBool = + Token<"true" | "false"> + & { location: span, kind: "expr", tag: "boolean", value: boolean } +export type AstExprConstantNumber = Token & { location: span, kind: "expr", tag: "number", value: number } export type AstExprConstantString = Token & { + location: span, + kind: "expr", tag: "string", - quoteStyle: "single" | "double" | "block" | "interp", - blockDepth: number, + quotestyle: "single" | "double" | "block" | "interp", + blockdepth: number, } export type AstExprLocal = { + location: span, + kind: "expr", tag: "local", token: Token, ["local"]: AstLocal, upvalue: boolean, } -export type AstExprGlobal = { - tag: "global", - name: Token, -} +export type AstExprGlobal = { location: span, kind: "expr", tag: "global", name: Token } -export type AstExprVarargs = Token<"..."> & { tag: "vararg" } +export type AstExprVarargs = Token<"..."> & { location: span, kind: "expr", tag: "vararg" } export type AstExprCall = { + location: span, + kind: "expr", tag: "call", func: AstExpr, - openParens: Token<"(">?, + openparens: Token<"(">?, arguments: Punctuated, - closeParens: Token<")">?, + closeparens: Token<")">?, self: boolean, - argLocation: Location, + argLocation: span, } export type AstExprIndexName = { + location: span, + kind: "expr", tag: "indexname", expression: AstExpr, accessor: Token<"." | ":">, index: Token, - indexLocation: Location, + indexlocation: span, } export type AstExprIndexExpr = { + location: span, + kind: "expr", tag: "index", expression: AstExpr, - openBrackets: Token<"[">, + openbrackets: Token<"[">, index: AstExpr, - closeBrackets: Token<"]">, + closebrackets: Token<"]">, } +-- I don't like that we have this type; a lot of its data should live in the parents, which should then have `AstStatBlock` as its body export type AstFunctionBody = { - openGenerics: Token<"<">?, + opengenerics: Token<"<">?, generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, + genericpacks: Punctuated?, + closegenerics: Token<">">?, self: AstLocal?, - openParens: Token<"(">, + openparens: Token<"(">, parameters: Punctuated, vararg: Token<"...">?, - varargColon: Token<":">?, - varargAnnotation: AstTypePack?, - closeParens: Token<")">, - returnSpecifier: Token<":">?, - returnAnnotation: AstTypePack?, + varargcolon: Token<":">?, + varargannotation: AstTypePack?, + closeparens: Token<")">, + returnspecifier: Token<":">?, + returnannotation: AstTypePack?, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstExprAnonymousFunction = { + location: span, + kind: "expr", tag: "function", attributes: { AstAttribute }, - functionKeyword: Token<"function">, + functionkeyword: Token<"function">, body: AstFunctionBody, } -export type AstExprTableItem = | { kind: "list", value: AstExpr, separator: Token<"," | ";">? } | { +-- helper types for items contained in an AstExprTable, not actually AstExprs themselves +export type AstExprTableItemList = { + location: span, + kind: "list", + value: AstExpr, + separator: Token<"," | ";">?, + istableitem: true, +} + +export type AstExprTableItemRecord = { + location: span, kind: "record", key: Token, equals: Token<"=">, value: AstExpr, separator: Token<"," | ";">?, -} | { + istableitem: true, +} + +export type AstExprTableItemGeneral = { + location: span, kind: "general", - indexerOpen: Token<"[">, + indexeropen: Token<"[">, key: AstExpr, - indexerClose: Token<"]">, + indexerclose: Token<"]">, equals: Token<"=">, value: AstExpr, separator: Token<"," | ";">?, + istableitem: true, } +export type AstExprTableItem = AstExprTableItemList | AstExprTableItemRecord | AstExprTableItemGeneral + export type AstExprTable = { + location: span, + kind: "expr", tag: "table", - openBrace: Token<"{">, + openbrace: Token<"{">, entries: { AstExprTableItem }, - closeBrace: Token<"}">, + closebrace: Token<"}">, } export type AstExprUnary = { + location: span, + kind: "expr", tag: "unary", operator: Token<"not" | "-" | "#">, operand: AstExpr, } export type AstExprBinary = { + location: span, + kind: "expr", tag: "binary", lhsoperand: AstExpr, operator: Token, -- TODO: enforce token type @@ -171,37 +217,44 @@ export type AstExprBinary = { } export type AstExprInterpString = { + location: span, + kind: "expr", tag: "interpolatedstring", strings: { Token }, expressions: { AstExpr }, } export type AstExprTypeAssertion = { + location: span, + kind: "expr", tag: "cast", operand: AstExpr, operator: Token<"::">, annotation: AstType, } -export type AstExprIfElseIfs = { - elseifKeyword: Token<"elseif">, +-- helper type for elseif clauses of an if-else expression, not actually an AstExpr itself +export type AstElseIfExpr = { + elseifkeyword: Token<"elseif">, condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstExpr, + thenkeyword: Token<"then">, + thenexpr: AstExpr, } export type AstExprIfElse = { + location: span, + kind: "expr", tag: "conditional", - ifKeyword: Token<"if">, + ifkeyword: Token<"if">, condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstExpr, - elseifs: { AstExprIfElseIfs }, - elseKeyword: Token<"else">, - antecedent: AstExpr, + thenkeyword: Token<"then">, + thenexpr: AstExpr, + elseifs: { AstElseIfExpr }, + elsekeyword: Token<"else">, + elseexpr: AstExpr, } -export type AstExpr = +export type AstExpr = { kind: "expr" } & ( | AstExprGroup | AstExprConstantNil | AstExprConstantBool @@ -220,98 +273,123 @@ export type AstExpr = | AstExprInterpString | AstExprTypeAssertion | AstExprIfElse +) export type AstStatBlock = { + location: span, + kind: "stat", tag: "block", statements: { AstStat }, } -export type AstStatElseIf = { - elseifKeyword: Token<"elseif">, +export type AstStatDo = { + location: span, + kind: "stat", + tag: "do", + dokeyword: Token<"do">, + body: { AstStat }, -- TODO: change to AstStatBlock when Luau adds AstStatDo on the C++ side + endkeyword: Token<"end">, +} + +export type AstElseIfStat = { + elseifkeyword: Token<"elseif">, condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstStatBlock, + thenkeyword: Token<"then">, + thenblock: AstStatBlock, } export type AstStatIf = { + location: span, + kind: "stat", tag: "conditional", - ifKeyword: Token<"if">, + ifkeyword: Token<"if">, condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstStatBlock, - elseifs: { AstStatElseIf }, - elseKeyword: Token<"else">?, -- TODO: this could be elseif! - antecedent: AstStatBlock?, - endKeyword: Token<"end">, + thenkeyword: Token<"then">, + thenblock: AstStatBlock, + elseifs: { AstElseIfStat }, + elsekeyword: Token<"else">?, -- TODO: this could be elseif! + elseblock: AstStatBlock?, + endkeyword: Token<"end">, } export type AstStatWhile = { + location: span, + kind: "stat", tag: "while", - whileKeyword: Token<"while">, + whilekeyword: Token<"while">, condition: AstExpr, - doKeyword: Token<"do">, + dokeyword: Token<"do">, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatRepeat = { + location: span, + kind: "stat", tag: "repeat", - repeatKeyword: Token<"repeat">, + repeatkeyword: Token<"repeat">, body: AstStatBlock, - untilKeyword: Token<"until">, + untilkeyword: Token<"until">, condition: AstExpr, } -export type AstStatBreak = Token<"break"> & { tag: "break" } +export type AstStatBreak = Token<"break"> & { location: span, kind: "stat", tag: "break" } -export type AstStatContinue = Token<"continue"> & { tag: "continue" } +export type AstStatContinue = Token<"continue"> & { location: span, kind: "stat", tag: "continue" } export type AstStatReturn = { + location: span, + kind: "stat", tag: "return", - returnKeyword: Token<"return">, + returnkeyword: Token<"return">, expressions: Punctuated, } -export type AstStatExpr = { - tag: "expression", - expression: AstExpr, -} +export type AstStatExpr = { location: span, kind: "stat", tag: "expression", expression: AstExpr } export type AstStatLocal = { + location: span, + kind: "stat", tag: "local", - localKeyword: Token<"local">, + localkeyword: Token<"local">, variables: Punctuated, equals: Token<"=">?, values: Punctuated, } export type AstStatFor = { + location: span, + kind: "stat", tag: "for", - forKeyword: Token<"for">, + forkeyword: Token<"for">, variable: AstLocal, equals: Token<"=">, from: AstExpr, - toComma: Token<",">, + tocomma: Token<",">, to: AstExpr, - stepComma: Token<",">?, + stepcomma: Token<",">?, step: AstExpr?, - doKeyword: Token<"do">, + dokeyword: Token<"do">, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatForIn = { + location: span, + kind: "stat", tag: "forin", - forKeyword: Token<"for">, - variables: Punctuated>, - inKeyword: Token<"in">, + forkeyword: Token<"for">, + variables: Punctuated, + inkeyword: Token<"in">, values: Punctuated, - doKeyword: Token<"do">, + dokeyword: Token<"do">, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatAssign = { + location: span, + kind: "stat", tag: "assign", variables: Punctuated, equals: Token<"=">, @@ -319,55 +397,66 @@ export type AstStatAssign = { } export type AstStatCompoundAssign = { + location: span, + kind: "stat", tag: "compoundassign", variable: AstExpr, operand: Token, -- TODO: enforce token type, value: AstExpr, } -export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { tag: "attribute" } +export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { location: span, kind: "attribute" } export type AstStatFunction = { + location: span, + kind: "stat", tag: "function", attributes: { AstAttribute }, - functionKeyword: Token<"function">, + functionkeyword: Token<"function">, name: AstExpr, body: AstFunctionBody, } export type AstStatLocalFunction = { + location: span, + kind: "stat", tag: "localfunction", attributes: { AstAttribute }, - localKeyword: Token<"local">, - functionKeyword: Token<"function">, + localkeyword: Token<"local">, + functionkeyword: Token<"function">, name: AstLocal, body: AstFunctionBody, } export type AstStatTypeAlias = { + location: span, + kind: "stat", tag: "typealias", export: Token<"export">?, - typeToken: Token<"type">, + typetoken: Token<"type">, name: Token, - openGenerics: Token<"<">?, + opengenerics: Token<"<">?, generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, + genericpacks: Punctuated?, + closegenerics: Token<">">?, equals: Token<"=">, type: AstType, } export type AstStatTypeFunction = { + location: span, + kind: "stat", tag: "typefunction", export: Token<"export">?, type: Token<"type">, - functionKeyword: Token<"function">, + functionkeyword: Token<"function">, name: Token, body: AstFunctionBody, } -export type AstStat = +export type AstStat = { kind: "stat" } & ( | AstStatBlock + | AstStatDo | AstStatIf | AstStatWhile | AstStatRepeat @@ -384,6 +473,7 @@ export type AstStat = | AstStatLocalFunction | AstStatTypeAlias | AstStatTypeFunction +) export type AstGenericType = { tag: "generic", @@ -401,43 +491,52 @@ export type AstGenericTypePack = { } export type AstTypeReference = { + location: span, + kind: "type", tag: "reference", prefix: Token?, - prefixPoint: Token<".">?, + prefixpoint: Token<".">?, name: Token, - openParameters: Token<"<">?, + openparameters: Token<"<">?, parameters: Punctuated?, - closeParameters: Token<">">?, + closeparameters: Token<">">?, } -export type AstTypeSingletonBool = Token<"true" | "false"> & { - tag: "boolean", - value: boolean, -} +export type AstTypeSingletonBool = + Token<"true" | "false"> + & { location: span, kind: "type", tag: "boolean", value: boolean } export type AstTypeSingletonString = Token & { + location: span, + kind: "type", tag: "string", - quoteStyle: "single" | "double", + quotestyle: "single" | "double", } export type AstTypeTypeof = { + location: span, + kind: "type", tag: "typeof", typeof: Token<"typeof">, - openParens: Token<"(">, + openparens: Token<"(">, expression: AstExpr, - closeParens: Token<")">, + closeparens: Token<")">, } export type AstTypeGroup = { + location: span, + kind: "type", tag: "group", - openParens: Token<"(">, + openparens: Token<"(">, type: AstType, - closeParens: Token<">">, + closeparens: Token<")">, } -export type AstTypeOptional = Token<"?"> & { tag: "optional" } +export type AstTypeOptional = Token<"?"> & { location: span, kind: "type", tag: "optional" } export type AstTypeUnion = { + location: span, + kind: "type", tag: "union", leading: Token<"|">?, -- Separator may be nil for AstTypeOptional @@ -445,75 +544,89 @@ export type AstTypeUnion = { } export type AstTypeIntersection = { + location: span, + kind: "type", tag: "intersection", leading: Token<"&">?, types: Punctuated, } export type AstTypeArray = { + location: span, + kind: "type", tag: "array", - openBrace: Token<"{">, + openbrace: Token<"{">, access: Token<"read" | "write">?, type: AstType, - closeBrace: Token<"}">, + closebrace: Token<"}">, } -export type AstTypeTableItem = { +export type AstTypeTableItemIndexer = { kind: "indexer", access: Token<"read" | "write">?, - indexerOpen: Token<"[">, + indexeropen: Token<"[">, key: AstType, - indexerClose: Token<"]">, + indexerclose: Token<"]">, colon: Token<":">, value: AstType, separator: Token<"," | ";">?, -} | { +} + +export type AstTypeTableItemStringProperty = { kind: "stringproperty", access: Token<"read" | "write">?, - indexerOpen: Token<"[">, + indexeropen: Token<"[">, key: AstTypeSingletonString, - indexerClose: Token<"]">, + indexerclose: Token<"]">, colon: Token<":">, value: AstType, separator: Token<"," | ";">?, -} | { +} + +export type AstTypeTableItemProperty = { kind: "property", access: Token<"read" | "write">?, - key: AstTypeSingletonString, - indexerClose: Token<"]">, + key: Token, colon: Token<":">, value: AstType, separator: Token<"," | ";">?, } +export type AstTypeTableItem = AstTypeTableItemIndexer | AstTypeTableItemStringProperty | AstTypeTableItemProperty + export type AstTypeTable = { + location: span, + kind: "type", tag: "table", - openBrace: Token<"{">, + openbrace: Token<"{">, entries: { AstTypeTableItem }, - closeBrace: Token<"}">, + closebrace: Token<"}">, } export type AstTypeFunctionParameter = { + location: span, name: Token?, colon: Token<":">?, type: AstType, } export type AstTypeFunction = { + location: span, + kind: "type", tag: "function", - openGenerics: Token<"<">?, + opengenerics: Token<"<">?, generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, - openParens: Token<"(">, + genericpacks: Punctuated?, + closegenerics: Token<">">?, + openparens: Token<"(">, parameters: Punctuated, vararg: AstTypePack?, - closeParens: Token<")">, - returnArrow: Token<"->">, - returnTypes: AstTypePack, + closeparens: Token<")">, + returnarrow: Token<"->">, + returntypes: AstTypePack, } -export type AstType = +export type AstType = { kind: "type" } & ( | AstTypeReference | AstTypeSingletonBool | AstTypeSingletonString @@ -525,35 +638,38 @@ export type AstType = | AstTypeArray | AstTypeTable | AstTypeFunction +) export type AstTypePackExplicit = { + location: span, + kind: "typepack", tag: "explicit", - openParens: Token<"(">?, + openparens: Token<"(">?, types: Punctuated, - tailType: AstTypePack?, - closeParens: Token<")">?, + tailtype: AstTypePack?, + closeparens: Token<")">?, } -export type AstTypePackGeneric = { - tag: "generic", - name: Token, - ellipsis: Token<"...">, -} +export type AstTypePackGeneric = { location: span, kind: "typepack", tag: "generic", name: Token, ellipsis: Token<"..."> } export type AstTypePackVariadic = { + location: span, + kind: "typepack", tag: "variadic", --- May be nil when present as the vararg annotation in a function body ellipsis: Token<"...">?, type: AstType, } -export type AstTypePack = AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic +export type AstTypePack = { kind: "typepack" } & (AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic) + +export type AstNode = { location: span } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) export type ParseResult = { root: AstStatBlock, eof: Eof, lines: number, - lineOffsets: { number }, + lineoffsets: { number }, } function luau.parse(source: string): ParseResult @@ -570,7 +686,16 @@ function luau.compile(source: string): Bytecode error("not implemented") end -function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }): (...any) -> ...any +function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }?): (...any) -> ...any + error("not implemented") +end + +function luau.resolverequire(path: string, fromchunkname: string): string + error("not implemented") +end + +-- For now, we return a string representation of the module's type, but we will expand it to some Luau data structure representation of Type (similar to the AST types) in a subsequent PR. +function luau.typeofmodule(modulepath: string): string error("not implemented") end diff --git a/definitions/net.luau b/definitions/net.luau index eb80bab32..3135efb52 100644 --- a/definitions/net.luau +++ b/definitions/net.luau @@ -6,14 +6,14 @@ export type Metadata = { headers: { [string]: string }?, } -export type Request = { +export type Response = { body: string, headers: { [string]: string }, status: number, ok: boolean, } -function net.request(url: string, metadata: Metadata?): Request +function net.request(url: string, metadata: Metadata?): Response error("not implemented") end @@ -41,7 +41,13 @@ export type Configuration = { handler: Handler, } -function net.serve(config: Handler | Configuration) +export type Server = { + hostname: string, + port: number, + close: () -> (), +} + +function net.serve(config: Handler | Configuration): Server error("not implemented") end diff --git a/definitions/process.luau b/definitions/process.luau index b839ecd9c..9a21bad1e 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -1,7 +1,14 @@ -export type StdioKind = "default" | "inherit" | "none" | "" +export type StdioKind = "default" | "inherit" | "none" export type ProcessRunOptions = { - shell: (string | boolean)?, + cwd: string?, + stdio: StdioKind?, + + env: { [string]: string }?, +} + +export type ProcessSystemOptions = { + system: string?, cwd: string?, stdio: StdioKind?, @@ -29,7 +36,19 @@ function process.cwd(): string error("not implemented") end -function process.run(args: string | { string }, options: ProcessRunOptions?): ProcessResult +function process.run(args: { string }, options: ProcessRunOptions?): ProcessResult + error("not implemented") +end + +function process.system(command: string, options: ProcessSystemOptions?): ProcessResult + error("not implemented") +end + +function process.exit(exitcode: number): never + error("not implemented") +end + +function process.execpath(): string error("not implemented") end diff --git a/definitions/system.luau b/definitions/system.luau index b51285996..c7d14f1c9 100644 --- a/definitions/system.luau +++ b/definitions/system.luau @@ -21,6 +21,10 @@ function system.hostname(): string error("unimplemented") end +function system.tmpdir(): string + error("unimplemented") +end + function system.totalmemory(): number error("unimplemented") end diff --git a/definitions/task.luau b/definitions/task.luau index 5b280aea2..3b80c6f34 100644 --- a/definitions/task.luau +++ b/definitions/task.luau @@ -6,15 +6,19 @@ function task.defer() error("unimplemented") end -function task.wait(dur: (number | time.Duration)?) +function task.wait(dur: (number | time.Duration)?): number error("unimplemented") end -function task.spawn(routine: ((T...) -> U...) | thread, ...: T...) +function task.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread error("unimplemented") end -function task.resume(thread: thread) +function task.resume(thread: thread): thread + error("unimplemented") +end + +function task.delay(dur: number | time.Duration, routine: (T...) -> U..., ...: T...): thread error("unimplemented") end diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c00548994..920b54cf0 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,44 +1,33 @@ import { defineConfig } from 'vitepress' +import { withSidebar } from 'vitepress-sidebar' -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "Lute", - description: "Luau for General-Purpose Programming", - base: "/lute/", - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: 'Guide', link: '/guide/installation' }, - { text: 'Reference', link: '/reference/fs' } - ], - - sidebar: [ - { - text: "Getting Started", - items: [ - { text: 'Installation', link: '/guide/installation' }, - ] - }, - { - text: "Reference", - items: [ - { text: 'fs', link: '/reference/fs' }, - { text: 'luau', link: '/reference/luau' }, - { text: 'net', link: '/reference/net' }, - { text: 'process', link: '/reference/process' }, - { text: 'system', link: '/reference/system' }, - { text: 'task', link: '/reference/task' }, - { text: 'vm', link: '/reference/vm' }, - ] - } - ], - - search: { - provider: 'local' +export default withSidebar( + defineConfig({ + title: 'Lute', + description: 'Luau for General-Purpose Programming', + base: "/", + themeConfig: { + nav: [ + { text: 'Guide', link: '/guide/installation' }, + { text: 'CLI', link: '/cli/' }, + { text: 'Reference', link: '/reference' }, + ], + search: { provider: 'local' }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/luau-lang/lute' }, + ], }, - - socialLinks: [ - { icon: 'github', link: 'https://github.com/luau-lang/lute' } - ] + }), + { + // ============ [ SIDEBAR OPTIONS ] ============ + useFolderLinkFromIndexFile: true, + useFolderTitleFromIndexFile: true, + useTitleFromFileHeading: true, + useTitleFromFrontmatter: true, + hyphenToSpace: true, + underscoreToSpace: true, + sortMenusByFrontmatterOrder: true, + frontmatterOrderDefaultValue: 100, + excludeByGlobPattern: ['**/test/**'], } -}) +) diff --git a/docs/cli/check.md b/docs/cli/check.md new file mode 100755 index 000000000..09684901d --- /dev/null +++ b/docs/cli/check.md @@ -0,0 +1,15 @@ +# check + +Type check Luau files. + +## Usage + +```bash +lute check [file2.luau...] +``` + +## Options + +### `-h, --help` + +Display this usage message. diff --git a/docs/cli/compile.md b/docs/cli/compile.md new file mode 100755 index 000000000..c4517d79c --- /dev/null +++ b/docs/cli/compile.md @@ -0,0 +1,41 @@ +# compile + +Compile a Luau script, along with its dependencies, into a standalone executable. + +## Usage + +```bash +lute compile [options] +``` + +## Options + +### `--output ` + +Name for the compiled executable. If omitted, defaults to entry file's base name (with .exe on Windows). + +### `--bundle-stats` + +Display compiled bytecode bundle size and compression statistics. + +### `--show-require-graph` + +Print the dependency graph of files that have been included in the bundle. + +### `-h, --help` + +Display this usage message. + +## Examples + +Outputs a standalone executable called foo(.exe on windows): + +```bash +lute compile foo.luau +``` + +Outputs a standalone executable called main(.exe on windows): + +```bash +lute compile foo.luau --output main +``` \ No newline at end of file diff --git a/docs/cli/index.md b/docs/cli/index.md new file mode 100755 index 000000000..f7fdd60c9 --- /dev/null +++ b/docs/cli/index.md @@ -0,0 +1,34 @@ +--- +order: 2 +--- + +# CLI Reference + +Lute provides a command-line interface for running, type checking, compiling, testing, and linting Luau code. + +## Usage + +```bash +lute [options] [arguments...] +``` + +If no command is specified, `run` is used by default. + +## Commands + +| Command | Description | +| ------- | ----------- | +| [check](./check) | Type check Luau files. | +| [compile](./compile) | Compile a Luau script into a standalone executable. | +| [lint](./lint) | Lint the specified Luau file using the specified lint rule(s) or using the default rules. | +| [run](./run) | Run a Luau script. | +| [setup](./setup) | Generate type definition files for the language server. | +| [test](./test) | Run tests discovered in .test.luau and .spec.luau files. | +| [transform](./transform) | Run a specified code transformation on specified Luau files. | + +## Global Options + +| Option | Description | +| ------ | ----------- | +| `-h`, `--help` | Display help message for the command. | +| `--version` | Show the lute version. | diff --git a/docs/cli/lint.md b/docs/cli/lint.md new file mode 100755 index 000000000..154ef8e15 --- /dev/null +++ b/docs/cli/lint.md @@ -0,0 +1,49 @@ +# lint + +`lute lint` is a programmable linter for Luau code, shipped as part of Lute. +As a linter, it works to statically analyze the user's code to warn them about common pitfalls they may be falling into, or to nudge them away from discouraged coding practices. +It is _programmable_ meaning that you can write a new lint rule for your Luau code _in Luau_. +It's also built on top of the official Luau language stack, allowing it to leverage the same parser used by Luau and Roblox, unlike third-party linters that rely on separate, custom parser implementations. +The examples folder contains two instances of sample lint rules, and you can find the full suite of `lute lint`'s default rules [here](https://github.com/luau-lang/lute/tree/primary/lute/cli/commands/lint/rules). + +## Usage + +```bash +lute lint [OPTIONS] [...PATHS] +``` + +## Options + +### `-h, --help` + +Show this help message + +### `-v, --verbose` + +Enable verbose output + +### `-r, --rules [RULE]` + +Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. If unspecified, the default lint rules are used. + +### `-j, --json` + +Output lint violations in JSON format matching the LSP diagnostic spec. + +### `-s, --string-input` + +Lint the provided string input instead of reading from files. + +## Arguments + +Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. + +## Examples + +```bash +lute lint -r examples/lints/almost_swapped.luau bad_swap.luau +``` + +```bash +lute lint -r examples/lints/ lintee.luau src_code/ +``` diff --git a/docs/cli/run.md b/docs/cli/run.md new file mode 100755 index 000000000..b7afcf4e9 --- /dev/null +++ b/docs/cli/run.md @@ -0,0 +1,21 @@ +# run + +Run a Luau script. Can be omitted if you just pass the name of the script +to Lute. + +## Usage + +```bash +lute run [args...] +``` +is equivalent to: + +```bash +lute [args...] +``` + +## Options + +### `-h, --help` + +Display this usage message. diff --git a/docs/cli/setup.md b/docs/cli/setup.md new file mode 100755 index 000000000..b30602e5c --- /dev/null +++ b/docs/cli/setup.md @@ -0,0 +1,15 @@ +# setup + +Generate type definition files for the language server. + +## Usage + +```bash +lute setup +``` + +## Options + +### `--with-luaurc` + +Defines aliases to the type definition files in the working directory's luaurc file. diff --git a/docs/cli/test.md b/docs/cli/test.md new file mode 100755 index 000000000..0efe2c4d1 --- /dev/null +++ b/docs/cli/test.md @@ -0,0 +1,69 @@ +# test + +Run tests discovered in .test.luau and .spec.luau files (defaults to looking for files in a tests/ directory). + +## Usage + +```bash +lute test [OPTIONS] [PATHS...] +``` + +## Options + +### `-h, --help` + +Show this help message + +### `--list` + +List all discovered test cases without running them + +### `-s, --suite SUITE` + +Run only tests in the specified suite + +### `-c, --case CASE` + +Run only test cases matching the specified name + +## Arguments + +Directories or files to search for tests (default: ./tests) + +## Examples + +Run all tests in ./tests: + +```bash +lute test +``` + +List all test cases: + +```bash +lute test --list +``` + +Run all tests in MyTestSuite: + +```bash +lute test -s MyTestSuite +``` + +Run specific test in suite: + +```bash +lute test --suite MyTestSuite --case mytest +``` + +Run all test cases named "some case": + +```bash +lute test --case "some case" +``` + +List tests that were discovered in my/other/testdir: + +```bash +lute test --list my/other/testdir +``` \ No newline at end of file diff --git a/docs/cli/transform.md b/docs/cli/transform.md new file mode 100755 index 000000000..816d892b4 --- /dev/null +++ b/docs/cli/transform.md @@ -0,0 +1,20 @@ +# transform + +Run a specified code transformation on specified Luau files. +Individual code transformers can specify custom migration options, which are parsed as additional arguments on the command line (i.e. `lute transform transformer.luau --custom-arg=value transformee.luau`). + +## Usage + +```bash +lute transform [options...] +``` + +## Options + +### `--dry-run` + +Runs the transformation without actually overwriting or deleting any files. + +### `--output ` + +Specifies an output file for a transformed file. Only valid when transforming a single file. If not specified, files are overwritten in place. diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 000000000..3ebfb3ce4 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,7 @@ +--- +order: 1 +--- + +# Guide + +- [Installation](./installation) diff --git a/docs/index.md b/docs/index.md index 07de07168..2ae69a9d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ hero: link: /guide/installation - theme: alt text: Reference - link: /reference/fs + link: /reference/lute/crypto # TODO: add buzz words # features: diff --git a/docs/package-lock.json b/docs/package-lock.json index d5b18208c..5555a0bc1 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -4,10 +4,29 @@ "requires": true, "packages": { "": { + "dependencies": { + "vitepress-sidebar": "^1.33.0" + }, "devDependencies": { "vitepress": "^1.6.3" } }, + "node_modules/@algolia/abtesting": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.9.0.tgz", + "integrity": "sha512-4q9QCxFPiDIx1n5w41A1JMkrXI8p0ugCQnCGFtCKZPmWtwgWCqwVRncIbp++81xSELFZVQUfiB7Kbsla1tIBSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@algolia/autocomplete-core": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", @@ -58,41 +77,41 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.23.4.tgz", - "integrity": "sha512-WIMT2Kxy+FFWXWQxIU8QgbTioL+SGE24zhpj0kipG4uQbzXwONaWt7ffaYLjfge3gcGSgJVv+1VlahVckafluQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.43.0.tgz", + "integrity": "sha512-YsKYkohIMxiYEAu8nppZi5EioYDUIo9Heoor8K8vMUnkUtGCOEU/Q4p5OWaYSSBx3evo09Ga9rG4jsKViIcDzQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.23.4.tgz", - "integrity": "sha512-4B9gChENsQA9kFmFlb+x3YhBz2Gx3vSsm81FHI1yJ3fn2zlxREHmfrjyqYoMunsU7BybT/o5Nb7ccCbm/vfseA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.43.0.tgz", + "integrity": "sha512-kDGJWt3nzf0nu5RPFXQhNGl6Q0cn35fazxVWXhd0Fw3Vo6gcVfrcezcBenHb66laxnVJ7uwr1uKhmsu3Wy25sQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.23.4.tgz", - "integrity": "sha512-bsj0lwU2ytiWLtl7sPunr+oLe+0YJql9FozJln5BnIiqfKOaseSDdV42060vUy+D4373f2XBI009K/rm2IXYMA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.43.0.tgz", + "integrity": "sha512-RAFipkAnI8xhL/Sgi/gpXgNWN5HDM6F7z4NNNOcI8ZMYysZEBsqVXojg/WdKEKkQCOHVTZ3mooIjc5BaQdyVtA==", "dev": true, "license": "MIT", "engines": { @@ -100,160 +119,160 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.23.4.tgz", - "integrity": "sha512-XSCtAYvJ/hnfDHfRVMbBH0dayR+2ofVZy3jf5qyifjguC6rwxDsSdQvXpT0QFVyG+h8UPGtDhMPoUIng4wIcZA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.43.0.tgz", + "integrity": "sha512-PmVs83THco8Qig3cAjU9a5eAGaSxsfgh7PdmWMQFE/MCmIcLPv0MVpgfcGGyPjZGYvPC4cg+3q7JJxcNSsEaTg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.23.4.tgz", - "integrity": "sha512-l/0QvqgRFFOf7BnKSJ3myd1WbDr86ftVaa3PQwlsNh7IpIHmvVcT83Bi5zlORozVGMwaKfyPZo6O48PZELsOeA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.43.0.tgz", + "integrity": "sha512-Bs4zMLXvkAr19FSOZWNizlNUpRFxZVxtvyEJ+q3n3+hPZUcKjo0LIh15qghhRcQPEihjBN6Gr/U+AqRfOCsvnA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.23.4.tgz", - "integrity": "sha512-TB0htrDgVacVGtPDyENoM6VIeYqR+pMsDovW94dfi2JoaRxfqu/tYmLpvgWcOknP6wLbr8bA+G7t/NiGksNAwQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.43.0.tgz", + "integrity": "sha512-pwHv+z8TZAKbwAWt9+v2gIqlqcCFiMdteTdgdPn2yOBRx4WUQdsIWAaG9GiV3by8jO51FuFQnTohhauuI63y3A==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.23.4.tgz", - "integrity": "sha512-uBGo6KwUP6z+u6HZWRui8UJClS7fgUIAiYd1prUqCbkzDiCngTOzxaJbEvrdkK0hGCQtnPDiuNhC5MhtVNN4Eg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.43.0.tgz", + "integrity": "sha512-wKy6x6fKcnB1CsfeNNdGp4dzLzz04k8II3JLt6Sp81F8s57Ks3/K9qsysmL9SJa8P486s719bBttVLE8JJYurQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.23.4.tgz", - "integrity": "sha512-Si6rFuGnSeEUPU9QchYvbknvEIyCRK7nkeaPVQdZpABU7m4V/tsiWdHmjVodtx3h20VZivJdHeQO9XbHxBOcCw==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.43.0.tgz", + "integrity": "sha512-TA21h2KwqCUyPXhSAWF3R2UES/FAnzjaVPDI6cRPXeadX+pdrGN0GWat5gSUATJVcMHECn+lGvuMMRxO86o2Pg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.23.4.tgz", - "integrity": "sha512-EXGoVVTshraqPJgr5cMd1fq7Jm71Ew6MpGCEaxI5PErBpJAmKdtjRIzs6JOGKHRaWLi+jdbJPYc2y8RN4qcx5Q==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.43.0.tgz", + "integrity": "sha512-rvWVEiA1iLcFmHS3oIXGIBreHIxNZqEFDjiNyRtLEffgd62kul2DjXM7H5bOouDMTo1ywMWT9OeQnzrhlTGAwA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.23.4.tgz", - "integrity": "sha512-1t6glwKVCkjvBNlng2itTf8fwaLSqkL4JaMENgR3WTGR8mmW2akocUy/ZYSQcG4TcR7qu4zW2UMGAwLoWoflgQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.43.0.tgz", + "integrity": "sha512-scCijGd38npvH2uHbYhO4f1SR8It5R2FZqOjNcMfw/7Ph7Hxvl+cd7Mo6RzIxsNRcLW5RrwjtpTK3gpDe8r/WQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.23.4.tgz", - "integrity": "sha512-UUuizcgc5+VSY8hqzDFVdJ3Wcto03lpbFRGPgW12pHTlUQHUTADtIpIhkLLOZRCjXmCVhtr97Z+eR6LcRYXa3Q==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.43.0.tgz", + "integrity": "sha512-jMkRLWJYr4Hcmpl89e4vIWs69Mkf8Uwx7MG5ZKk2UxW3G3TmouGjI0Ph5mVPmg3Jf1UG3AdmVDc4XupzycT1Jw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.23.4.tgz", - "integrity": "sha512-UhDg6elsek6NnV5z4VG1qMwR6vbp+rTMBEnl/v4hUyXQazU+CNdYkl++cpdmLwGI/7nXc28xtZiL90Es3I7viQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.43.0.tgz", + "integrity": "sha512-KyQiVz+HdYtissC0J9KIGhHhKytQyJX+82GVsbv5rSCXbETnAoojvUyCn+3KRtWUvMDYCsZ+Y7hM71STTUJUJg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.23.4.tgz", - "integrity": "sha512-jXGzGBRUS0oywQwnaCA6mMDJO7LoC3dYSLsyNfIqxDR4SNGLhtg3je0Y31lc24OA4nYyKAYgVLtjfrpcpsWShg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.43.0.tgz", + "integrity": "sha512-UnUBNY0U+oT0bkYDsEqVsCkErC2w7idk4CRiLSzicqY8tGylD9oP0j13X/fse1CuiAFCCr3jfl+cBlN6dC0OFw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -261,9 +280,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -271,13 +290,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -287,14 +306,14 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -743,9 +762,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.33", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.33.tgz", - "integrity": "sha512-nL5/UmI9x5PQ/AHv6bOaL2pH6twEdEz4pI89efB/K7HFn5etQnxMtGx9DFlOg/sRA2/yFpX8KXvc95CSDv5bJA==", + "version": "1.2.58", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.58.tgz", + "integrity": "sha512-XtXEoRALqztdNc9ujYBj2tTCPKdIPKJBdLNDebFF46VV1aOAwTbAYMgNsK5GMCpTJupLCmpBWDn+gX5SpECorQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -759,17 +778,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", - "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -781,9 +827,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", - "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -795,9 +841,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", - "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -809,9 +855,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", - "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -823,9 +869,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", - "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -837,9 +883,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", - "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -851,9 +897,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", - "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -865,9 +911,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", - "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -879,9 +925,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", - "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -893,9 +939,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", - "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -906,10 +952,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", - "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -920,10 +966,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", - "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -935,9 +981,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", - "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -949,9 +995,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", - "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -963,9 +1009,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", - "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -977,9 +1023,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", - "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -991,9 +1037,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", - "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1004,10 +1050,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", - "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1019,9 +1079,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", - "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1032,10 +1092,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", - "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1134,9 +1208,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -1207,9 +1281,9 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", - "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", "engines": { @@ -1221,77 +1295,77 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz", + "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz", + "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.24", + "@vue/shared": "3.5.24" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", + "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.24", + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz", + "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.24", + "@vue/shared": "3.5.24" } }, "node_modules/@vue/devtools-api": { - "version": "7.7.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.5.tgz", - "integrity": "sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz", + "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.7.5" + "@vue/devtools-kit": "^7.7.7" } }, "node_modules/@vue/devtools-kit": { - "version": "7.7.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.5.tgz", - "integrity": "sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", + "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.7.5", + "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1301,9 +1375,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.7.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.5.tgz", - "integrity": "sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", + "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", "dev": true, "license": "MIT", "dependencies": { @@ -1311,57 +1385,57 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", + "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.24" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", + "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.24", + "@vue/shared": "3.5.24" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", + "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", + "@vue/reactivity": "3.5.24", + "@vue/runtime-core": "3.5.24", + "@vue/shared": "3.5.24", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", + "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.24" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz", + "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==", "dev": true, "license": "MIT" }, @@ -1472,40 +1546,89 @@ } }, "node_modules/algoliasearch": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.23.4.tgz", - "integrity": "sha512-QzAKFHl3fm53s44VHrTdEo0TkpL3XVUYQpnZy1r6/EHvMAyIg+O4hwprzlsNmcCHTNyVcF2S13DAUn7XhkC6qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-abtesting": "5.23.4", - "@algolia/client-analytics": "5.23.4", - "@algolia/client-common": "5.23.4", - "@algolia/client-insights": "5.23.4", - "@algolia/client-personalization": "5.23.4", - "@algolia/client-query-suggestions": "5.23.4", - "@algolia/client-search": "5.23.4", - "@algolia/ingestion": "1.23.4", - "@algolia/monitoring": "1.23.4", - "@algolia/recommend": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.43.0.tgz", + "integrity": "sha512-hbkK41JsuGYhk+atBDxlcKxskjDCh3OOEDpdKZPtw+3zucBqhlojRG5e5KtCmByGyYvwZswVeaSWglgLn2fibg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.9.0", + "@algolia/client-abtesting": "5.43.0", + "@algolia/client-analytics": "5.43.0", + "@algolia/client-common": "5.43.0", + "@algolia/client-insights": "5.43.0", + "@algolia/client-personalization": "5.43.0", + "@algolia/client-query-suggestions": "5.43.0", + "@algolia/client-search": "5.43.0", + "@algolia/ingestion": "1.43.0", + "@algolia/monitoring": "1.43.0", + "@algolia/recommend": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/birpc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", - "integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", + "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1539,6 +1662,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1551,21 +1692,35 @@ } }, "node_modules/copy-anything": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "dev": true, "license": "MIT", "dependencies": { - "is-what": "^4.1.8" + "is-what": "^5.2.0" }, "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1597,6 +1752,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/emoji-regex-xs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", @@ -1656,6 +1823,19 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -1663,14 +1843,42 @@ "dev": true, "license": "MIT" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/focus-trap": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", - "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "dev": true, "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fsevents": { @@ -1688,6 +1896,41 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -1744,27 +1987,94 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/mark.js": { @@ -1890,10 +2200,34 @@ ], "license": "MIT" }, - "node_modules/minisearch": { + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz", - "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", "dev": true, "license": "MIT" }, @@ -1935,6 +2269,37 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1950,9 +2315,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1970,7 +2335,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1979,9 +2344,9 @@ } }, "node_modules/preact": { - "version": "10.26.5", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.5.tgz", - "integrity": "sha512-fmpDkgfGU6JYux9teDWLhj9mKN55tyepwYbxHgQuIxbWQzgFg5vk7Mrrtfx7xRxq798ynkY4DDDxZr235Kk+4w==", + "version": "10.27.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "dev": true, "license": "MIT", "funding": { @@ -1990,9 +2355,9 @@ } }, "node_modules/property-information": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", - "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "dev": true, "license": "MIT", "funding": { @@ -2000,6 +2365,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/qsu": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/qsu/-/qsu-1.10.3.tgz", + "integrity": "sha512-OoA9UClkVuWvfcOiQrXrqvcJU1x3v/Mx0A+zMTELw3zZ6TWDeGRVnObQuUusz5rksTYo3QcyXLi9zVQMLxg2cQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", @@ -2035,13 +2409,13 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", - "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2051,26 +2425,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.0", - "@rollup/rollup-android-arm64": "4.40.0", - "@rollup/rollup-darwin-arm64": "4.40.0", - "@rollup/rollup-darwin-x64": "4.40.0", - "@rollup/rollup-freebsd-arm64": "4.40.0", - "@rollup/rollup-freebsd-x64": "4.40.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", - "@rollup/rollup-linux-arm-musleabihf": "4.40.0", - "@rollup/rollup-linux-arm64-gnu": "4.40.0", - "@rollup/rollup-linux-arm64-musl": "4.40.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-musl": "4.40.0", - "@rollup/rollup-linux-s390x-gnu": "4.40.0", - "@rollup/rollup-linux-x64-gnu": "4.40.0", - "@rollup/rollup-linux-x64-musl": "4.40.0", - "@rollup/rollup-win32-arm64-msvc": "4.40.0", - "@rollup/rollup-win32-ia32-msvc": "4.40.0", - "@rollup/rollup-win32-x64-msvc": "4.40.0", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -2082,6 +2458,40 @@ "license": "MIT", "peer": true }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shiki": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", @@ -2099,6 +2509,18 @@ "@types/hast": "^3.0.4" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2130,6 +2552,71 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -2145,23 +2632,69 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/superjson": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", - "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", + "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", "dev": true, "license": "MIT", "dependencies": { - "copy-anything": "^3.0.2" + "copy-anything": "^4" }, "engines": { "node": ">=16" } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", "dev": true, "license": "MIT" }, @@ -2177,9 +2710,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2235,9 +2768,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2265,9 +2798,9 @@ } }, "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dev": true, "license": "MIT", "dependencies": { @@ -2280,9 +2813,9 @@ } }, "node_modules/vite": { - "version": "5.4.18", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", - "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -2340,9 +2873,9 @@ } }, "node_modules/vitepress": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.3.tgz", - "integrity": "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", "dev": true, "license": "MIT", "dependencies": { @@ -2381,18 +2914,32 @@ } } }, + "node_modules/vitepress-sidebar": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/vitepress-sidebar/-/vitepress-sidebar-1.33.0.tgz", + "integrity": "sha512-+z45vGG6OQRKNU7OMhmrp5y9dk9Y8loFZGE2LwzqloqA4p+ECP3/Ti6qrS8kB3N8Rol4v0Nf8HSnsgZEDU4HXQ==", + "license": "MIT", + "dependencies": { + "glob": "10.4.5", + "gray-matter": "4.0.3", + "qsu": "^1.10.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", + "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-sfc": "3.5.24", + "@vue/runtime-dom": "3.5.24", + "@vue/server-renderer": "3.5.24", + "@vue/shared": "3.5.24" }, "peerDependencies": { "typescript": "*" @@ -2403,6 +2950,112 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/docs/package.json b/docs/package.json index 03fe8a475..4764fb0e4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,5 +7,8 @@ }, "devDependencies": { "vitepress": "^1.6.3" + }, + "dependencies": { + "vitepress-sidebar": "^1.33.0" } } diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 7ec155498..0a97407e4 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -1,56 +1,176 @@ -local fs = require("@lute/fs") -local process = require("@lute/process") +local fs = require("@std/fs") +local process = require("@std/process") +local path = require("@std/path") +local stringext = require("@std/stringext") -local function projectRelative(...) - local cwd = process.cwd() - local pathSep = if string.find(string.lower(require("@lute/system").os), "windows") then "\\" else "/" - return table.concat({ cwd, "..", ... }, pathSep) -end +local STDLIB_MODULE_ALIASES = { + assert = "assertions", + fs = "fslib", + io = "iolib", + process = "processlib", + task = "tasklib", + path = "pathlib", + system = "systemlib", + trivia = "retrieverLib", + visitor = "visitorlib", + query = "queryLib", + printer = "printerLib", +} + +local tripleDashCommentPattern = "^%s*%-%-%-%s?(.*)$" +local blockCommentStartPattern = "^%s*%-%-%[(=+)%[" +local whitespacePattern = "^%s*$" + +type PropertySignature = { + name: string?, + signature: string?, + doc: string?, + pendingDoc: { string }, + inBlock: boolean, + blockEq: string?, +} + +-- extractPropertySignature: (this function runs on one line at a time so we need to keep some states for block comments) +-- module (module name) +-- line (current line) +-- pendingDoc (accumulated doc lines so far) +-- inBlock (are we currently inside a block comment?) +-- blockEq (the "=" sequence used for block comment delimiting, e.g. "=" for --[=[ ; nil when not inside) +local function extractPropertySignature( + module: string, + line: string, + pendingDoc: { string }, + inBlock: boolean, + blockEq: string? +): PropertySignature + -- 1) If we're *not* in a block, detect block start that may include = signs: + if not inBlock then + local eq = string.match(line, blockCommentStartPattern) + if eq ~= nil then + -- start of block comment, capture part after the opening delimiter, if any + local after = string.match(line, "^%s*%-%-%[" .. eq .. "%[(.*)$") + if after then + after = stringext.trim(after) + if after ~= "" then + table.insert(pendingDoc, after) + end + end + + return { + pendingDoc = pendingDoc, + inBlock = true, + blockEq = eq, -- remember the = count for closing pattern + } + end + end + + -- 2) If we're inside a block, accumulate until we find the matching close + if inBlock then + local eq = blockEq or "" + -- find the closing pattern "]=]" that matches the # of "=" used in opening + local closingPattern = "%]" .. eq .. "%]" + local before = string.match(line, "^(.-)" .. closingPattern) + if before then + -- closing found on this line + before = stringext.trim(before) + if before ~= "" then + table.insert(pendingDoc, before) + end + + -- there might be trailing comment markers like "]]" on the same line; + -- we've already captured inner content, so we just exit block mode. + inBlock = false + blockEq = nil + else + -- still inside block: append whole line (trimmed) + local trimmed = stringext.trim(line) + if trimmed ~= "" then + table.insert(pendingDoc, trimmed) + end + end -local function extractPropertySignature(module: string, line: string): (string?, string?) - -- Match property declarations like: process.env = {} :: { [string]: string } + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 3) Triple dash comment? (---) + local triple = string.match(line, tripleDashCommentPattern) + if triple then + local txt = stringext.trim(triple) + if txt ~= "" then + table.insert(pendingDoc, txt) + end + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 4) Match property declarations like: process.env = {} :: { [string]: string } local propName, propType = string.match(line, `^{module}%.([%w_]+)%s*=%s*.-::%s*(.+)$`) if propName and propType then - return propName, propType + return { + name = propName, + signature = propType, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + } end - -- Match function declarations like: function crypto.password.hash(password: string): buffer + -- 5) Match function declarations like: function crypto.password.hash(password: string): buffer local fullName, params, _return, returnType = string.match(line, `^function%s+{module}%.(.-)(%b())(:?)%s*(.*)$`) if fullName then fullName = string.match(fullName, "^(.*)%b<>$") or fullName returnType = if returnType == "" then "()" else returnType - local signature = `{params} -> {returnType}` - return fullName, signature - end - - return nil, nil -end -local function parseDefinitionFile(module: string, filePath: string): { { name: string, signature: string } } - local content = fs.readfiletostring(filePath) - local lines = string.split(content, "\n") - local definitions = {} - - for _, line in lines do - local name, signature = extractPropertySignature(module, line) + return { + name = fullName, + signature = `{params} -> {returnType}`, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + } + end - if name and signature then - table.insert(definitions, { - name = name, - signature = signature, - }) - end + -- 6) If we reach here and there's any non-whitespace (i.e., code), reset pendingDoc to clear old comments + if not string.match(line, whitespacePattern) then + pendingDoc = {} end - return definitions + -- nothing found on this line + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } end -local function generateMarkdown(moduleName: string, definitions: { { name: string, signature: string } }): string +local function generateMarkdown( + moduleName: string, + definitions: { { name: string, signature: string, doc: string? } }, + filePath: path.pathlike, + libraryPath: path.pathlike, + requireLibraryAlias: string +): string + local libPathStr = tostring(libraryPath) + local filePathStr = tostring(filePath) + + local relativePath = string.sub(filePathStr, #libPathStr + 2) -- gets the path relative to the require path (only relevant for subdirs like std/libs) + relativePath = string.gsub(relativePath, "%.luau$", "") + local requirePath = string.gsub(relativePath, "/init$", "") -- remove trailing /init and use the module dir name + local lines = { - "# " .. moduleName, + `# {moduleName}`, "", "```luau", - "local " .. moduleName .. ' = require("@lute/' .. moduleName .. '")', + `local {moduleName} = require("@{requireLibraryAlias}/{requirePath}")`, "```", "", } @@ -60,7 +180,12 @@ local function generateMarkdown(moduleName: string, definitions: { { name: strin end) for _, def in definitions do - table.insert(lines, "## " .. def.name) + table.insert(lines, `## {moduleName}.{def.name}\n`) + + -- Insert the documentation if it exists + if def.doc then + table.insert(lines, `{def.doc}\n`) + end table.insert(lines, "```luau") table.insert(lines, def.signature) table.insert(lines, "```") @@ -70,28 +195,160 @@ local function generateMarkdown(moduleName: string, definitions: { { name: strin return table.concat(lines, "\n") end -local definitionsPath = projectRelative("definitions") -local referencePath = projectRelative("docs", "reference") +local function parseModule( + module: path.pathlike, + filePath: path.pathlike +): { { name: string, signature: string, doc: string? } } + local content = fs.readfiletostring(filePath) + local lines = string.split(content, "\n") -pcall(fs.mkdir, referencePath) + local moduleDefinitions = {} + local pendingDoc = {} + local inBlock = false + local blockEq = nil -local definitionFiles = fs.listdir(definitionsPath) + for _, line in lines do + local ps = extractPropertySignature(tostring(module), line, pendingDoc, inBlock, blockEq) -for _, file in definitionFiles do - if file.type == "file" and string.find(file.name, "%.luau$") then - local moduleName = string.gsub(file.name, "%.luau$", "") - local definitionFilePath = definitionsPath .. "/" .. file.name - local referenceFilePath = referencePath .. "/" .. moduleName .. ".md" + -- update state carried across lines + pendingDoc = ps.pendingDoc + inBlock = ps.inBlock + blockEq = ps.blockEq - print("Processing " .. moduleName .. "...") + if ps.name and ps.signature then + table.insert(moduleDefinitions, { + name = ps.name, + signature = ps.signature, + doc = ps.doc, + }) + end + end - local definitions = parseDefinitionFile(moduleName, definitionFilePath) - local markdown = generateMarkdown(moduleName, definitions) + return moduleDefinitions +end - fs.writestringtofile(referenceFilePath, markdown) +local function processDirectory( + modulePath: path.pathlike, + documentationPath: path.pathlike, + libraryPath: path.pathlike, + requireLibraryAlias: string +) + fs.createdirectory(documentationPath, { makeparents = true }) - print("Generated " .. referenceFilePath) + local entries = fs.listdirectory(modulePath) + + for _, entry in entries do + local entryPath = path.join(modulePath, entry.name) + local docPath = path.join(documentationPath, entry.name) + + if entry.type == "dir" then + fs.createdirectory(docPath, { makeparents = true }) + processDirectory(entryPath, docPath, libraryPath, requireLibraryAlias) + elseif entry.type == "file" and string.find(entry.name, "%.luau$") then + local moduleName = string.gsub(entry.name, "%.luau$", "") + local isInit = moduleName == "init" + if isInit then + moduleName = string.match(tostring(modulePath), "([^/\\]+)$") or moduleName + end + + -- Use index.md for init.luau files, otherwise use moduleName.md + local docFileName = if isInit then "index.md" else moduleName .. ".md" + local documentationFilePath = path.join(documentationPath, docFileName) + + print(`Processing {moduleName}...`) + + local moduleAlias = if requireLibraryAlias == "std" + then STDLIB_MODULE_ALIASES[moduleName] or moduleName + else moduleName + + local definitions = parseModule(moduleAlias, entryPath) + local markdown = generateMarkdown(moduleName, definitions, entryPath, libraryPath, requireLibraryAlias) + + fs.writestringtofile(documentationFilePath, markdown) + + print(`Generated {tostring(documentationFilePath)}`) + end + end +end + +local referenceBasePath = path.join(process.cwd(), "reference") +fs.createdirectory(referenceBasePath, { makeparents = true }) + +-- Generate reference index with frontmatter for sidebar ordering +local referenceIndex = [[--- +order: 3 +--- + +# Reference + +API reference documentation for Lute's built-in libraries. + +- [Lute Libraries](./lute/index.md) - Core runtime libraries (`@lute/*`) +- [Standard Libraries](./std/index.md) - Standard library modules (`@std/*`) +]] +fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) + +-- Helper to generate index with table of children +local function generateLibraryIndex(title: string, alias: string, modulePath: path.pathlike): string + local entries = fs.listdirectory(modulePath) + local modules = {} + + for _, entry in entries do + local name = entry.name + if entry.type == "dir" then + -- Check if it has an init.luau (is a module) + local initPath = path.join(modulePath, name, "init.luau") + if fs.exists(initPath) then + table.insert(modules, { name = name, isDir = true }) + end + elseif entry.type == "file" and string.match(name, "%.luau$") then + local modName = string.gsub(name, "%.luau$", "") + if modName ~= "init" then + table.insert(modules, { name = modName, isDir = false }) + end + end + end + + table.sort(modules, function(a, b) + return a.name < b.name + end) + + local lines = { + `# {title}`, + "", + "| Module | Require |", + "| ------ | ------- |", + } + + for _, mod in modules do + local link = if mod.isDir then `./{mod.name}/` else `./{mod.name}` + table.insert(lines, `| [{mod.name}]({link}) | \`require("@{alias}/{mod.name}")\` |`) end + + table.insert(lines, "") + return table.concat(lines, "\n") end +local definitionsPath = path.join(process.cwd(), "..", "definitions") +local referencePath = path.join(referenceBasePath, "lute") +processDirectory(definitionsPath, referencePath, definitionsPath, "lute") + +-- Generate lute subfolder index with table of modules +local luteIndex = generateLibraryIndex("lute", "lute", definitionsPath) +fs.writestringtofile(path.join(referencePath, "index.md"), luteIndex) + +local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") +local stdlibReferencePath = path.join(referenceBasePath, "std") +processDirectory(stdlibPath, stdlibReferencePath, stdlibPath, "std") + +-- Generate std subfolder index with table of modules +local stdIndex = generateLibraryIndex("std", "std", stdlibPath) +fs.writestringtofile(path.join(stdlibReferencePath, "index.md"), stdIndex) + +-- Mock module in test/ for testing purposes + +-- local testPath = path.join(process.cwd(), "test") +-- local testReferencePath = path.join(process.cwd(), "test") +-- processDirectory(testPath, testReferencePath, testPath, "test") + print("Reference documentation generation complete!") diff --git a/docs/test/test_module.luau b/docs/test/test_module.luau new file mode 100644 index 000000000..3d35c1b10 --- /dev/null +++ b/docs/test/test_module.luau @@ -0,0 +1,40 @@ +-- This file creates a mock module called test_module with random mock functions and different types of comments to test the reference doc generator reference.luau +-- See output documentation file generated in this folder `test_module.md` + +local test_module = {} + +--- Property declaration test: declare process.env as a map from string to string +test_module.property = test_module.property or {} :: { [string]: string } +test_module.property["EXAMPLE_VAR"] = "value" + +--- Simple function with triple dashes comment that returns hello +function test_module.triple_dash(name: string): string + return `Hello, {name}!` +end + +--- Multiple triple-dash comments (---) above function declaration +--- Should capture both of these lines + +--- Even if there are blank lines between them +function test_module.multiple_triple_dashes(y: number): number + return y + 1 +end + +-- this comment should not be captured because it uses double dashes + +--[[ + This block comment is also not captured because there are no = signs. +]] + +--- this should also not be captured because there is non function or property code +local ignore = "value" + +--[=[ + Block comment with equals above function declaration + This tests nested bracket delimiters being recognized properly. +]=] +function test_module.block_with_equals(name: string): string + return "This is a nested bracket block comment: " .. name +end + +return table.freeze(test_module) diff --git a/docs/test/test_module.md b/docs/test/test_module.md new file mode 100755 index 000000000..5d688c460 --- /dev/null +++ b/docs/test/test_module.md @@ -0,0 +1,43 @@ +# test_module + +```luau +local test_module = require("@test/test_module") +``` + +## test_module.block_with_equals + +Block comment with equals above function declaration + +This tests nested bracket delimiters being recognized properly. + +```luau +(name: string) -> string +``` + +## test_module.multiple_triple_dashes + +Multiple triple-dash comments (---) above function declaration + +Should capture both of these lines + +Even if there are blank lines between them + +```luau +(y: number) -> number +``` + +## test_module.property + +Property declaration test: declare process.env as a map from string to string + +```luau +{ [string]: string } +``` + +## test_module.triple_dash + +Simple function with triple dashes comment that returns hello + +```luau +(name: string) -> string +``` diff --git a/examples/async_read.luau b/examples/async_read.luau deleted file mode 100644 index ba0abc2df..000000000 --- a/examples/async_read.luau +++ /dev/null @@ -1,16 +0,0 @@ -local fs = require("@lute/fs") -local task = require("@std/task") - --- blocking -local f = fs.open("temp", "w+") -local x = "This is a string I am writing to a file" -fs.write(f, x) -fs.close(f) - -local t = task.create(function() - return fs.readasync("temp") -end) - -local t2 = task.await(t) -print(t2) -print(t2 == x) diff --git a/examples/badisnan.luau b/examples/badisnan.luau index 6bdeba554..afa20dd27 100644 --- a/examples/badisnan.luau +++ b/examples/badisnan.luau @@ -1,3 +1,4 @@ return function(n) + --lute-lint-ignore(divide_by_zero) return n == 0 / 0 end diff --git a/examples/compile.luau b/examples/compile.luau index 51872e794..2f7757459 100644 --- a/examples/compile.luau +++ b/examples/compile.luau @@ -1,4 +1,4 @@ -local luau = require("@lute/luau") +local luau = require("@std/luau") local bytecode_container = luau.compile('return "Hello, world!"') diff --git a/examples/create_directory.luau b/examples/create_directory.luau new file mode 100644 index 000000000..e647f3a79 --- /dev/null +++ b/examples/create_directory.luau @@ -0,0 +1,22 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") + +local tmpdir = system.tmpdir() +local directory = path.join(tmpdir, "example_dir") + +fs.createdirectory(directory, { makeparents = true }) + +if fs.exists(directory) and fs.type(directory) == "dir" then + print("Directory successfully created") +else + print("Failed to create directory") +end + +fs.removedirectory(directory) + +if not fs.exists(directory) then + print("Directory successfully removed") +else + print("Failed to remove directory") +end diff --git a/examples/difftext.luau b/examples/difftext.luau new file mode 100644 index 000000000..ac51b6a4f --- /dev/null +++ b/examples/difftext.luau @@ -0,0 +1,43 @@ +local difftext = require("@batteries/difftext") +local richterm = require("@batteries/richterm") + +local src = [[ +local originalVariable = 1 +local same = 1 + +function doSomething() -- comment + -- removed + print(originalVariable * 2) +end + +print() +]] + +local destination = [[ +local newVariable = 2 +local same = 1 + +function doSomething() + print(newVariable * 2) +end + +doSomething() +]] + +print(richterm.bold("Diff")) +print(difftext.prettydiff(src, destination)) +print(richterm.bold("Diff (w/ line numbers)")) +print(difftext.prettydiff(src, destination, { + includeLineNumbers = true, +})) +print() +print(richterm.bold("Diff (Detailed)")) +print(difftext.prettydiff(src, destination, { + detailed = true, +})) +print() +print(richterm.bold("Diff (Detailed w/ Line Numbers)")) +print(difftext.prettydiff(src, destination, { + detailed = true, + includeLineNumbers = true, +})) diff --git a/examples/directories.luau b/examples/directories.luau index 7f968b948..34c73bb27 100644 --- a/examples/directories.luau +++ b/examples/directories.luau @@ -1,4 +1,4 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") for _, file in fs.listdir("./examples") do print(`Example {file.name} is a {file.type}`) diff --git a/examples/homedir_cwd.luau b/examples/homedir_cwd.luau index 7ed6a2669..1d757baf0 100644 --- a/examples/homedir_cwd.luau +++ b/examples/homedir_cwd.luau @@ -1,3 +1,3 @@ -local process = require("@lute/process") +local process = require("@std/process") print(process.cwd(), process.homedir()) diff --git a/examples/json.luau b/examples/json.luau index 8d5e71d2d..1ff086df3 100644 --- a/examples/json.luau +++ b/examples/json.luau @@ -1,4 +1,4 @@ -local json = require("@batteries/json") +local json = require("@std/json") local serialize = json.serialize({ hello = "world", diff --git a/examples/linter.luau b/examples/linter.luau deleted file mode 100644 index f3cc1dd92..000000000 --- a/examples/linter.luau +++ /dev/null @@ -1,94 +0,0 @@ -local fs = require("@lute/fs") -local luau = require("@lute/luau") - -local pp = require("@batteries/pp") - -local function select(node, predicate: ({ [string]: any }) -> T?): { T } - local nodes = {} - - local function helper(n) - if typeof(n) ~= "table" then - return - end - - local result = predicate(n) - if result ~= nil then - table.insert(nodes, result) - end - - for key, value in n :: { unknown } do - if key == "tag" or key == "location" then - continue - end - - helper(value) - end - end - - helper(node) - return nodes -end - -local path = tostring(...) -local source = fs.readfiletostring(path) - -local prog = luau.parse(source) - -local function lintForDivZeroByZero(prog) - local function unwrapParens(n) - if n.tag == "group" then - return unwrapParens(n.expression) - end - return n - end - - local function isZero(n) - return n.tag == "number" and n.value == 0 - end - - local function isZeroDivZero(n) - return n.tag == "binary" - and n.operator == "/" - and isZero(unwrapParens(n.lhsoperand)) - and isZero(unwrapParens(n.rhsoperand)) - end - - local function isComparisonToZeroZero(n) - if not (n.operator == "~=" or n.operator == "==") then - return nil - end - - local lhs = unwrapParens(n.lhsoperand) - local rhs = unwrapParens(n.rhsoperand) - - if isZeroDivZero(lhs) then - return rhs - end - if isZeroDivZero(rhs) then - return lhs - end - - return nil - end - - local allBinaryOperators = select(prog, function(n) - return if n.tag == "binary" then n else nil - end) - - local violations = {} - for _, node in allBinaryOperators do - local expr = isComparisonToZeroZero(node) - if expr ~= nil then - local op_as_string = node.operator - table.insert(violations, { - severity = "err", - node = node, - message = `Don't compare things to 0/0, try: expr {op_as_string} expr`, - }) - end - end - - return violations -end - -print(pp(lintForDivZeroByZero(prog))) diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau new file mode 100644 index 000000000..4cce1e31e --- /dev/null +++ b/examples/lints/almost_swapped.luau @@ -0,0 +1,107 @@ +-- Check out lute/cli/commands/lint/rules for the default rules used by lute lint! + +local lintTypes = require("@commands/lint/types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "almost_swapped" +local message = "This looks like a failed attempt to swap." + +local compFuncs = {} + +function compFuncs.exprLocalsSame(a: syntax.AstExprLocal, b: syntax.AstExprLocal): boolean + return a["local"] == b["local"] +end + +function compFuncs.exprGlobalsSame(a: syntax.AstExprGlobal, b: syntax.AstExprGlobal): boolean + return a.name.text == b.name.text +end + +function compFuncs.exprIndexNamesSame(a: syntax.AstExprIndexName, b: syntax.AstExprIndexName): boolean + if a.index.text ~= b.index.text then + return false + end + + return compFuncs.refExprsSame(a.expression, b.expression) +end + +function compFuncs.exprIndexExprsSame(a: syntax.AstExprIndexExpr, b: syntax.AstExprIndexExpr): boolean + if a.index.tag == "string" and b.index.tag == "string" then + if a.index.text ~= b.index.text then + return false + else + return compFuncs.refExprsSame(a.expression, b.expression) + end + else + return compFuncs.refExprsSame(a.expression, b.expression) and compFuncs.refExprsSame(a.index, b.index) + end +end + +function compFuncs.refExprsSame(a: syntax.AstExpr, b: syntax.AstExpr): boolean + if a.tag ~= b.tag then + return false + end + + if a.tag == "local" then + return compFuncs.exprLocalsSame(a, b :: syntax.AstExprLocal) + elseif a.tag == "global" then + return compFuncs.exprGlobalsSame(a, b :: syntax.AstExprGlobal) + elseif a.tag == "indexname" then + return compFuncs.exprIndexNamesSame(a, b :: syntax.AstExprIndexName) + elseif a.tag == "index" then + return compFuncs.exprIndexExprsSame(a, b :: syntax.AstExprIndexExpr) + else + return false + end +end + +-- Report instances of attempted swaps like: +-- a = b; b = a +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } + local violations = {} + + local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes + + for _, block in nodes do + for i = 1, #block.statements - 1 do + local currStat = block.statements[i] + if currStat.tag ~= "assign" or #currStat.values ~= 1 or #currStat.variables ~= 1 then + continue + end + + local nextStat = block.statements[i + 1] + if nextStat.tag ~= "assign" or #nextStat.values ~= 1 or #nextStat.variables ~= 1 then + continue + end + + local currVar, currVal = currStat.variables[1].node, currStat.values[1].node + local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node + + if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then + table.insert(violations, { -- LUAUFIX: severity isn't inferred as a singleton, so table.insert is mad + lintname = name, + location = syntax.span.create({ + beginline = currStat.location.beginline, + begincolumn = currStat.location.begincolumn, + endline = nextStat.location.endline, + endcolumn = nextStat.location.endcolumn, + }), + message = message, + severity = "warning", + sourcepath = sourcepath, + }) + end + end + end + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau new file mode 100644 index 000000000..d4e6068fa --- /dev/null +++ b/examples/lints/divide_by_zero.luau @@ -0,0 +1,41 @@ +-- Check out lute/cli/commands/lint/rules for the default rules used by lute lint! + +local lintTypes = require("@commands/lint/types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "divide_by_zero" +local message = "Division by zero detected." + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } + return query + .findallfromroot(ast, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" + end) + :filter(function(bin) + return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 + end) + :maptoarray( + function( + n: syntax.AstExprBinary + ): lintTypes.LintViolation -- LUAUFIX: Bidiretional inference of generics should let us not need this annotation + return { + lintname = name, + location = n.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/examples/module_return_type/get_module_return_type.luau b/examples/module_return_type/get_module_return_type.luau new file mode 100644 index 000000000..3b30613ff --- /dev/null +++ b/examples/module_return_type/get_module_return_type.luau @@ -0,0 +1,18 @@ +local luau = require("@std/luau") + +local moduleTypeString = luau.typeofmodule("examples/module_return_type/mainmodule.luau") + +print("Module return type\n", moduleTypeString) + +-- Output: +-- Module return type +-- { +-- _metadata: { +-- id: string, +-- key: number +-- }, +-- func1: (a: number) -> (s: string) -> boolean, +-- strings: { +-- [string]: string +-- } +-- } diff --git a/examples/module_return_type/mainmodule.luau b/examples/module_return_type/mainmodule.luau new file mode 100644 index 000000000..01a7f51a5 --- /dev/null +++ b/examples/module_return_type/mainmodule.luau @@ -0,0 +1,24 @@ +local tableext = require("@std/tableext") + +local testFilter: { [string]: number } = { + ["a"] = 1, +} + +local module = {} + +function module.func1(a: number) + return function(s: string): boolean + return s == tostring(a) + end +end + +module.strings = tableext.map(testFilter, function(value) + return tostring(value) +end) + +module._metadata = { + key = -1, + id = "nice", +} + +return module diff --git a/examples/parsing.luau b/examples/parsing.luau index 55c37f6a9..fa6fc6e3a 100644 --- a/examples/parsing.luau +++ b/examples/parsing.luau @@ -1,6 +1,6 @@ -local luau = require("@lute/luau") +local syntax = require("@std/syntax") local pretty = require("@batteries/pp") -local foo = luau.parseexpr("5") +local foo = syntax.parseexpr("5") print(pretty(foo)) diff --git a/examples/process.luau b/examples/process.luau index abc801c0c..c1b5dd4f4 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -1,6 +1,8 @@ -local process = require("@lute/process") +local process = require("@std/process") local task = require("@std/task") +process.system("echo hello", { cwd = {} }) + local result = process.run({ "echo", "Hello, lute!" }) print(result.exitcode) @@ -16,17 +18,19 @@ print(r1.stdout) print(r2.stdout) print(r3.stdout) -local r4 = process.run("echo Hello, lute!", { shell = true }) +local r4 = process.system("echo Hello, lute!") print(r4.stdout) -local r5 = process.run({ "echo", "$HOME" }, { env = { HOME = "/home/lute" }, shell = true }) +local r5 = process.system("echo $HOME", { env = { HOME = "/home/lute" } }) print(r5.stdout) local r6 = process.run({ "pwd" }, { cwd = "/" }) print(r6.stdout) -local r7 = process.run("echo $0", { shell = true }) +local r7 = process.system("echo $0") print(r7.stdout) -local r8 = process.run("echo $0", { shell = "/bin/sh" }) +local r8 = process.system("echo $0", { system = "/bin/sh" }) print(r8.stdout) + +process.exit(0) diff --git a/examples/process_env.luau b/examples/process_env.luau index a1d8aa673..edb2c680d 100644 --- a/examples/process_env.luau +++ b/examples/process_env.luau @@ -1,4 +1,4 @@ -local process = require("@lute/process") +local process = require("@std/process") print(process.env.HOME) print(process.env.LUTE_HOME) diff --git a/examples/query.luau b/examples/query.luau new file mode 100644 index 000000000..68360f8a6 --- /dev/null +++ b/examples/query.luau @@ -0,0 +1,25 @@ +local syntax = require("@std/syntax") +local query = require("@std/syntax/query") +local utils = require("@std/syntax/utils") + +local ast = syntax.parseexpr("require(OldComponent)") + +local p = query + .findallfromroot(ast, utils.isExprCall) + :filter(function(call) + local calledFunc = call.func + return calledFunc.tag == "global" and calledFunc.name.text == "require" + end) + :filter(function(call) + local firstArg = call.arguments[1].node + return firstArg.tag == "indexname" and firstArg.index.text == "OldComponent" + end) + +-- Can we improve the type solver here so we don't need to add this annotation? +local argIndexNames = query.map(p, function(call: syntax.AstExprCall) + return call.arguments[1].node :: syntax.AstExprIndexName +end) + +local replacements = argIndexNames:replace(function(node) + return "NewComponent" +end) diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau new file mode 100644 index 000000000..41f918e03 --- /dev/null +++ b/examples/query_transformer.luau @@ -0,0 +1,19 @@ +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntax_utils = require("@std/syntax/utils") + +local function transform(ctx) + return query + .findallfromroot(ctx.parseresult.root, syntax_utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "~=" + and bin.lhsoperand.tag == "local" + and bin.rhsoperand.tag == "local" + and bin.lhsoperand.token.text == bin.rhsoperand.token.text + end) + :replace(function(bin) + return `math.isnan({(bin.lhsoperand :: syntax.AstExprLocal).token.text})` + end) +end + +return transform diff --git a/examples/secretbox.luau b/examples/secretbox.luau new file mode 100644 index 000000000..50332fde7 --- /dev/null +++ b/examples/secretbox.luau @@ -0,0 +1,20 @@ +local crypto = require("@lute/crypto") + +local message = "Hello, world!" +local box = crypto.secretbox.seal(message) + +function hexify(digest) + local hex = {} + for i = 1, buffer.len(digest) do + hex[i] = string.format("%02x", buffer.readu8(digest, i - 1)) + end + return table.concat(hex) +end + +print(hexify(box.ciphertext)) + +local opened = crypto.secretbox.open(box) +local output = buffer.readstring(opened, 0, buffer.len(opened)) + +print(output) +assert(output == message) diff --git a/examples/serve_html.luau b/examples/serve_html.luau index cecccc9da..94d67837c 100644 --- a/examples/serve_html.luau +++ b/examples/serve_html.luau @@ -2,7 +2,7 @@ local net = require("@lute/net") local server = net.serve({ port = 8080, - handler = function(req) + handler = function(req: net.ReceivedRequest): net.ServerResponse local headers = "" for key, value in req.headers do headers ..= `\n {key}: {value}` diff --git a/examples/system_environment_check.luau b/examples/system_environment_check.luau new file mode 100644 index 000000000..c061986b8 --- /dev/null +++ b/examples/system_environment_check.luau @@ -0,0 +1,13 @@ +local system = require("@std/system") + +if system.win32 then + print("Running on Windows") +elseif system.linux then + print("Running on Linux") +elseif system.macos then + print("Running on macOS") +elseif system.unix then + print("Running on Unix") +else + print("Unknown operating system") +end diff --git a/examples/system_lib.luau b/examples/system_lib.luau index 83296ee79..96957006b 100644 --- a/examples/system_lib.luau +++ b/examples/system_lib.luau @@ -1,4 +1,4 @@ -local system = require("@lute/system") +local system = require("@std/system") print( string.format( @@ -7,7 +7,7 @@ print( system.os, system.arch, system.threadcount(), - system.uptime(), + tostring(system.uptime()), system.freememory(), system.totalmemory() ) diff --git a/examples/task-delay.luau b/examples/task-delay.luau new file mode 100644 index 000000000..baed15a1a --- /dev/null +++ b/examples/task-delay.luau @@ -0,0 +1,5 @@ +local task = require("@lute/task") + +task.delay(1, coroutine.create(print), vector.one) + +task.delay(1, print, vector.one) diff --git a/examples/task-wait.luau b/examples/task-wait.luau index 7ee372303..4a91afeda 100644 --- a/examples/task-wait.luau +++ b/examples/task-wait.luau @@ -1,5 +1,5 @@ local task = require("@lute/task") -local time = require("@lute/time") +local time = require("@std/time") print(task.wait(1)) print(task.wait(time.duration.seconds(1))) diff --git a/examples/testing.luau b/examples/testing.luau new file mode 100644 index 000000000..66e658cc2 --- /dev/null +++ b/examples/testing.luau @@ -0,0 +1,70 @@ +local test = require("@std/test") + +test.case("foo", function(assert) + assert.eq(3, 3) + assert.eq(6, 1) +end) + +test.case("bar", function(assert) + assert.eq("a", "b") +end) + +test.case("baz", function(assert) + assert.neq("a", "b") +end) + +test.suite("MySuite", function(suite) + -- beforeall runs once before all tests in the suite + suite:beforeall(function() + print("Setting up MySuite - runs once before all tests") + end) + + -- beforeeach runs before each individual test + suite:beforeeach(function() + print("Running before each test") + end) + + -- aftereach runs after each individual test + suite:aftereach(function() + print("Cleaning up after each test") + end) + + -- afterall runs once after all tests in the suite + suite:afterall(function() + print("Tearing down MySuite - runs once after all tests") + end) + + suite:case("subsuite", function(assert) + assert.neq(1, 2) + assert.eq(1, 2) + end) + + suite:case("another_test", function(assert) + assert.eq(1, 1) + end) + + -- Example of table_eq assertion - demonstrates error reporting + suite:case("table_comparison", function(assert) + local table1 = { a = 1, b = { c = 2, d = 3 } } + local table2 = { a = 1, b = { c = 2, d = 3 } } + assert.tableeq(table1, table2) + assert.tableeq(table1, { a = 1 }) + end) + + suite:case("expect_error", function(assert) + local function foo(v: boolean) + if v then + error("error") + end + end + + assert.errors(function() -- demonstrates success (callback throws err) + foo(true) + end) + assert.errors(function() -- demonstrates failure (callback doesn't throw) + foo(false) + end) + end) +end) + +test.run() diff --git a/examples/transformee.luau b/examples/transformee.luau new file mode 100644 index 000000000..e99c87e57 --- /dev/null +++ b/examples/transformee.luau @@ -0,0 +1,3 @@ +local x = math.sqrt(-1) +-- x ~= x should become math.isnan(x) +local b = x ~= x diff --git a/examples/transformer.luau b/examples/transformer.luau new file mode 100644 index 000000000..b70c33aab --- /dev/null +++ b/examples/transformer.luau @@ -0,0 +1,95 @@ +local printer = require("@std/syntax/printer") +local visitor = require("@std/syntax/visitor") +local syntax = require("@std/syntax") + +local function transformation(ctx) + local v = visitor.create() + + v.visitExprBinary = function(node: syntax.AstExprBinary) + if node.operator.text ~= "~=" then + return true + end + + if node.lhsoperand.tag ~= "local" or node.rhsoperand.tag ~= "local" then + return true + end + + if node.lhsoperand.token.text ~= node.rhsoperand.token.text then + return true + end + + local operand: syntax.AstExprLocal = node.lhsoperand + operand.token.leadingtrivia = {} + operand.token.trailingtrivia = {} + local location: syntax.span = operand.token.location; + + -- transform node into an AstExprCall + (node :: any).operator = nil + (node :: any).lhsoperand = nil + (node :: any).rhsoperand = nil + + ((node :: any) :: syntax.AstExprCall).tag = "call" + + local func: syntax.AstExprGlobal = { + kind = "expr", + tag = "global", + name = { + istoken = true, + leadingtrivia = {}, + location = location, + text = "math.isnan", + trailingtrivia = {}, + }, + } + ((node :: any) :: syntax.AstExprCall).func = func + + local openparens: syntax.Token<"("> = { + istoken = true, + leadingtrivia = {}, + location = syntax.span.create({ + beginline = location.beginline, + begincolumn = location.begincolumn + #"math.isnan", + endline = location.endline, + endcolumn = location.endcolumn + #"math.isnan" + 1, + }), + text = "(", + trailingtrivia = {}, + } + ((node :: any) :: syntax.AstExprCall).openparens = openparens + + local arguments: syntax.Punctuated = { { node = operand } } + ((node :: any) :: syntax.AstExprCall).arguments = arguments + + local closeparens: syntax.Token<")"> = { + istoken = true, + leadingtrivia = {}, + location = syntax.span.create({ + beginline = location.beginline, + begincolumn = location.begincolumn + #"math.isnan(" + #operand.token.text, + endline = location.endline, + endcolumn = location.endcolumn + #"math.isnan(" + #operand.token.text + 1, + }), + text = ")", + trailingtrivia = {}, + } + ((node :: any) :: syntax.AstExprCall).closeparens = closeparens; + + ((node :: any) :: syntax.AstExprCall).self = false + + local argLocation: syntax.span = syntax.span.create({ + beginline = location.beginline, + begincolumn = location.begincolumn + #"math.isnan(", + endline = location.endline, + endcolumn = location.begincolumn + #"math.isnan(" + #operand.token.text, + }); + ((node :: any) :: syntax.AstExprCall).argLocation = argLocation + + return false + end + + visitor.visitblock(ctx.parseresult.root, v) + + return printer.printfile(ctx.parseresult) +end + +return transformation diff --git a/examples/user_input_no_prompt.luau b/examples/user_input_no_prompt.luau new file mode 100644 index 000000000..daf54def3 --- /dev/null +++ b/examples/user_input_no_prompt.luau @@ -0,0 +1,10 @@ +-- User input can be provided in multiple ways: +-- 1. lute user_input.luau +-- 2. echo "Hello!" | lute user_input_no_prompt.luau +-- 3. lute user_input_no_prompt.luau < input_text.txt + +local io = require("@std/io") + +-- Get user input +local input = io.input() +print(input) diff --git a/examples/user_input_with_prompt.luau b/examples/user_input_with_prompt.luau new file mode 100644 index 000000000..4ed801af8 --- /dev/null +++ b/examples/user_input_with_prompt.luau @@ -0,0 +1,10 @@ +-- User input can be provided in multiple ways: +-- 1. lute user_input.luau +-- 2. echo "Hello!" | lute user_input_with_prompt.luau +-- 3. lute user_input_with_prompt.luau < input_text.txt + +local io = require("@std/io") + +-- Get user input with prompt +local name = io.input("Please enter your name: ") +print(name) diff --git a/examples/walk_directory.luau b/examples/walk_directory.luau new file mode 100644 index 000000000..fc0638ed3 --- /dev/null +++ b/examples/walk_directory.luau @@ -0,0 +1,44 @@ +local fslib = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") + +local tmpdir = system.tmpdir() +local baseDir = path.join(tmpdir, "walk_directory_example") + +-- Setup: create a directory structure +if not fslib.exists(baseDir) then + fslib.createdirectory(baseDir, { makeparents = true }) +end + +local subdirs = { + "subdir1", + "subdir2/nested1", +} + +for _, subdir in subdirs do + local fullPath = path.join(baseDir, subdir) + fslib.createdirectory(fullPath, { makeparents = true }) +end + +local files = { + "file1.txt", + "subdir1/file2.txt", + "subdir2/nested1/file3.txt", +} + +for _, file in files do + local fullPath = path.join(baseDir, file) + fslib.writestringtofile(fullPath, "hello lute") +end + +-- Walk the directory recursively +print("Walking directory:", baseDir) +local it = fslib.walk(baseDir, { recursive = true }) +local walker = it() +while walker do + print("Found:", walker) + walker = it() +end + +-- Cleanup: remove the created directory structure +fslib.removedirectory(baseDir, { recursive = true }) diff --git a/examples/watch_directory.luau b/examples/watch_directory.luau new file mode 100644 index 000000000..7db44daea --- /dev/null +++ b/examples/watch_directory.luau @@ -0,0 +1,41 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") +local task = require("@lute/task") + +-- Setup, get a temporary directory to watch +local tmpdir = system.tmpdir() + +local watchedFileName = "watched.txt" +local watched = path.join(tmpdir, watchedFileName) +if fs.exists(watched) then + fs.remove(watched) +end + +local watcher = fs.watch(tmpdir) + +-- Trigger a file change event +fs.writestringtofile(watched, "x") + +-- Poll the iterator with a timeout of 2 seconds +local event +local start = os.clock() + +repeat + event = watcher:next() + if not event then + task.wait(0.01) + end +until event or (os.clock() - start > 2) + +if event then + print("Event detected:", event.change and "change" or "rename") +else + print("No event detected within the timeout period.") +end + +-- Cleanup, close the watcher and remove the watched file +watcher:close() +if fs.exists(watched) then + fs.remove(watched) +end diff --git a/examples/writeFile.luau b/examples/writeFile.luau index c02b49918..d01485781 100644 --- a/examples/writeFile.luau +++ b/examples/writeFile.luau @@ -1,4 +1,4 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") -- Open a file if it doesn't exist, truncate it, local file = fs.open("dest", "w+") diff --git a/extern/luau.tune b/extern/luau.tune index 7073c6944..66f11596f 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.675" -revision = "59658182835dc39f598a4fc16ba0b177b8e6f55b" +branch = "0.704" +revision = "dc955d68e70d7bff0735cfc3927f30be2277a2fc" diff --git a/foreman.toml b/foreman.toml index 822ead819..92e6e9cfa 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] -stylua = { github = "JohnnyMorganz/StyLua", version = "2.0.2" } -lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20250606" } +stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260109" } diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index b0b8e88e3..30fc7a1e5 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -12,26 +12,54 @@ target_compile_features(Lute.CLI.Commands PUBLIC cxx_std_17) target_include_directories(Lute.CLI.Commands PUBLIC include generated) target_compile_options(Lute.CLI.Commands PRIVATE ${LUTE_OPTIONS}) +set(CLI_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${CLI_GENERATED_INCLUDE_DIR}/lute") +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/include/lute/version.h.in + ${CLI_GENERATED_INCLUDE_DIR}/lute/version.h + @ONLY +) + add_library(Lute.CLI.lib STATIC) target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h + include/lute/coverage.h + include/lute/fileutils.h + include/lute/luauflags.h + include/lute/packagerun.h + include/lute/profiler.h + include/lute/reporter.h + include/lute/requiresetup.h + include/lute/staticrequires.h include/lute/tc.h + include/lute/uvstate.h src/climain.cpp src/compile.cpp + src/coverage.cpp + src/fileutils.cpp + src/luauflags.cpp + src/packagerun.cpp + src/profiler.cpp + src/requiresetup.cpp + src/staticrequires.cpp src/tc.cpp + src/uvstate.cpp ) target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) -target_include_directories(Lute.CLI.lib PUBLIC include) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Require Lute.Runtime Luau.CLI.lib) +target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Common Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Luau Lute.Process Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) target_sources(Lute.CLI PRIVATE + include/lute/clireporter.h + + src/clireporter.cpp src/main.cpp ) diff --git a/lute/cli/commands/lib/cli.luau b/lute/cli/commands/lib/cli.luau new file mode 100644 index 000000000..178637f82 --- /dev/null +++ b/lute/cli/commands/lib/cli.luau @@ -0,0 +1,175 @@ +local cli = {} +cli.__index = cli + +type ArgKind = "positional" | "flag" | "option" +type ArgOptions = { + help: string?, + aliases: { string }?, + default: string?, + required: boolean?, +} + +type ArgData = { + name: string, + kind: ArgKind, + options: ArgOptions, +} + +type ParseResult = { + values: { [string]: string }, + flags: { [string]: boolean }, + fwdArgs: { string }, +} + +type ParserData = { + arguments: { [number]: ArgData }, + positional: { [number]: ArgData }, + parsed: ParseResult, +} + +type ParserInterface = typeof(cli) +export type Parser = setmetatable + +function cli.parser(): Parser + local self = { + arguments = {}, + positional = {}, + parsed = { values = {}, flags = {}, fwdArgs = {} }, + } + + return setmetatable(self, cli) +end + +function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): () + local argument = { + name = name, + kind = kind, + options = options or { aliases = {}, required = false }, + } + + table.insert(self.arguments, argument) + if kind == "positional" then + table.insert(self.positional, argument) + end +end + +function cli.parse(self: Parser, args: { string }): () + local i = 0 + local pos_index = 1 + + while i < #args do + i += 1 + + local arg = args[i] + + -- if the argument is exactly "--", we pass everything along + if arg == "--" then + table.move(args, i + 1, #args, 1, self.parsed.fwdArgs) + break + end + + -- if the argument starts with two dashes, we're parsing either a flag or an option + if string.sub(arg, 1, 2) == "--" then + local name = string.sub(arg, 3) + local found = false + + for _, argument in self.arguments do + local aliases = argument.options.aliases or {} + + if argument.name == name or table.find(aliases, name) then + found = true + + if argument.kind == "option" then + -- advance past the argument + i += 1 + + assert(i <= #args, "Missing value for argument: " .. argument.name) + self.parsed.values[argument.name] = args[i] + + break + end + + self.parsed.flags[argument.name] = true + break + end + end + + assert(found, "Unknown argument: " .. name) + continue + end + + -- if the argument starts with a single dash, we're parsing a flag + if string.sub(arg, 1, 1) == "-" then + local flags = string.sub(arg, 2) + + for j = 1, #flags do + local name = string.sub(flags, j, j) + local found = false + for _, argument in self.arguments do + local aliases = argument.options.aliases or {} + if argument.name == name or table.find(aliases, name) then + found = true + if argument.kind == "option" then + i += 1 + assert(i <= #args, "Missing value for argument: " .. argument.name) + self.parsed.values[argument.name] = args[i] + else + self.parsed.flags[argument.name] = true + end + break + end + end + + assert(found, "Unknown argument: " .. name) + end + + continue + end + + -- if we have positional arguments left, we can take this argument as one + if pos_index <= #self.positional then + self.parsed.values[self.positional[pos_index].name] = arg + pos_index += 1 + continue + end + + -- otherwise, the argument is forwarded on + table.insert(self.parsed.fwdArgs, arg) + end + + -- check that all required arguments are present, and set any default values as needed + for _, argument in self.arguments do + assert(argument) + + if argument.options.required and self.parsed.values[argument.name] == nil then + assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name) + end + + if self.parsed.values[argument.name] == nil and argument.options.default then + self.parsed.values[argument.name] = argument.options.default + end + end +end + +function cli.get(self: Parser, name: string): string? + return self.parsed.values[name] +end + +function cli.has(self: Parser, name: string): boolean + return self.parsed.flags[name] ~= nil +end + +function cli.forwarded(self: Parser): { string }? + return self.parsed.fwdArgs +end + +function cli.help(self: Parser): () + print("Usage:") + for _, argument in self.arguments do + local aliasStr = table.concat(argument.options.aliases or {}, ", ") + local reqStr = if argument.options.required then "(required) " else "" + print(string.format(" --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or "")) + end +end + +return table.freeze(cli) diff --git a/lute/cli/commands/lib/files.luau b/lute/cli/commands/lib/files.luau new file mode 100644 index 000000000..7000e6bf0 --- /dev/null +++ b/lute/cli/commands/lib/files.luau @@ -0,0 +1,75 @@ +local fs = require("@lute/fs") +local process = require("@lute/process") + +local ignore = require("./ignore") +local path = require("@std/path") +local stringext = require("@std/stringext") +local tableext = require("@std/tableext") + +local function traverseDirectoryRecursive( + directory: path.path, + ignoreResolver: ignore.IgnoreResolver, + verbose: boolean? +): { path.path } + local results = {} + + for _, entry in fs.listdir(path.format(directory)) do + local fullPath = path.join(directory, entry.name) + + if entry.type == "dir" then + if ignoreResolver:isIgnoredDirectory(fullPath) then + if verbose then + print("Skipping", fullPath) + end + continue + end + + tableext.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver, verbose)) + else + -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? + if stringext.hassuffix(entry.name, ".luau") or stringext.hassuffix(entry.name, ".lua") then + if ignoreResolver:isIgnoredFile(fullPath) then + if verbose then + print("Skipping", fullPath) + end + continue + end + table.insert(results, fullPath) + elseif verbose then + print(`Skipping non-Luau file '{fullPath}'`) + end + end + end + + return results +end + +local function getSourceFiles(paths: { string }, verbose: boolean?): { path.path } + local files = {} + + local ignoreResolver = ignore.new() + + for _, filepath in paths do + local pathObj = path.parse(filepath) + + if not path.isabsolute(pathObj) then + pathObj = path.resolve(process.cwd(), pathObj) + end + + filepath = path.format(pathObj) + if verbose then + print(filepath) + end + if fs.type(filepath) == "dir" then + tableext.extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver, verbose)) + else + table.insert(files, pathObj) + end + end + + return files +end + +return { + getSourceFiles = getSourceFiles, +} diff --git a/lute/cli/commands/lib/ignore.luau b/lute/cli/commands/lib/ignore.luau new file mode 100644 index 000000000..6bba90665 --- /dev/null +++ b/lute/cli/commands/lib/ignore.luau @@ -0,0 +1,260 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local system = require("@std/system") + +local separator = if system.win32 then "\\" else "/" + +--- Performs ignore resolution on a set of +local IgnoreResolver = {} +IgnoreResolver.__index = IgnoreResolver + +type Glob = { pattern: string, onlyMatchDirectories: boolean, matchAnywhere: boolean } + +type GitignoreData = { + location: string, + ignores: { Glob }, + whitelists: { Glob }, + next: GitignoreData?, +} + +type IgnoreResolverData = { + _ignoreCache: { [string]: GitignoreData }, +} + +export type IgnoreResolver = setmetatable + +function IgnoreResolver.new(): IgnoreResolver + local self = {} + + self._ignoreCache = {} + + return setmetatable(self, IgnoreResolver) +end + +--- Converts a glob pattern into a Luau string pattern for efficient matching +--- The output pattern should be matched against a filepath that is relative to the .gitignore location +local function parseGlob(glob: string): Glob + local onlyMatchDirectories = false + + if glob:sub(-1) == "/" then + onlyMatchDirectories = true + glob = glob:sub(1, -2) + end + + local lua_parts = {} + local matchAnywhere = false + local idx = 1 + local len = #glob + + -- If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to + -- the directory level of the particular .gitignore file itself. + -- i.e., anchor the pattern to the start + if glob:find("/") then + table.insert(lua_parts, "^") + if glob:sub(1, 2) == "./" then + idx = 3 + elseif glob:sub(1, 1) == "/" or glob:sub(1, 1) == "." then + idx = 2 + end + else + -- If no leading slash, it can match anywhere, including after a directory. + -- This means it can start at the beginning of the string, or after a slash. + -- We can't match for two distinct patterns bc Lua lacks an OR pattern operator + -- Instead add unique marker we check for in matchesGlob, indicating we match + -- against the two distinct patterns: anchored at beginning, or after a slash + matchAnywhere = true + end + + while idx <= len do + local char = glob:sub(idx, idx) + if char == "*" then + if glob:sub(idx, idx + 1) == "**" then + if idx == 1 and glob:sub(idx + 2, idx + 2) == "/" then + -- Leading '**/' at start of the file. If we've already determined matchAnywhere, + -- we don't need to add an extra character match + if matchAnywhere then + idx = idx + 2 + continue + end + end + if glob:sub(idx + 2, idx + 2) == "/" then + -- ** between slashes (e.g., a/**/b) + -- Zero or more directory segments - match everything, and don't include the following `/` slash + table.insert(lua_parts, ".*") + idx = idx + 3 + else + -- Trailing ** (e.g., foo/**) or just `**` + table.insert(lua_parts, ".*") -- Matches anything, including slashes + idx = idx + 2 + end + else + -- Single asterisk `*` (matches anything except directory separator) + table.insert(lua_parts, `[^{separator}]*`) + idx = idx + 1 + end + elseif char == "?" then + -- Match any single character, except for directory separator + table.insert(lua_parts, `[^{separator}]`) + idx = idx + 1 + elseif char == "-" or char == "." then + table.insert(lua_parts, `%{char}`) + idx = idx + 1 + elseif char == "/" or char == "\\" then + table.insert(lua_parts, separator) + idx = idx + 1 + else + table.insert(lua_parts, char) + idx = idx + 1 + end + end + + -- Add the ending anchor if not already an implicit `.*` from `**` + if glob:sub(-2) ~= "**" then + table.insert(lua_parts, "$") + end + + return { + pattern = table.concat(lua_parts), + onlyMatchDirectories = onlyMatchDirectories, + matchAnywhere = matchAnywhere, + } +end + +local function parseGitignoreContents(location: string, contents: string): GitignoreData + local lines = contents:split("\n") + + local ignoreData: GitignoreData = { + location = location, + ignores = {}, + whitelists = {}, + } + + for _, line in lines do + line = stringext.trim(line) + + if line == "" or stringext.hasprefix(line, "#") then + continue + end + + if stringext.hasprefix(line, "!") then + local globPattern = stringext.removeprefix(line, "!") + local glob = parseGlob(globPattern) + table.insert(ignoreData.whitelists, glob) + else + local glob = parseGlob(line) + table.insert(ignoreData.ignores, glob) + end + end + + return ignoreData +end + +local function parseGitignore(filepath: path.path): GitignoreData + local contents = fs.readfiletostring(path.format(filepath)) + local parentDirectory = if #filepath.parts > 0 then path.dirname(filepath) else nil + assert(parentDirectory) + return parseGitignoreContents(parentDirectory, contents) +end + +local function matchesGlob(glob: Glob, filepath: string, isDirectory: boolean): boolean + if glob.onlyMatchDirectories and not isDirectory then + return false + end + -- If the pattern is marked with matchAnywhere, we need to test + -- both: at start of path OR after any directory separator + if glob.matchAnywhere then + local matchAtStart = filepath:match("^" .. glob.pattern) ~= nil + local matchAfterDir = filepath:match(separator .. glob.pattern) ~= nil + + return matchAtStart or matchAfterDir + else + return filepath:match(glob.pattern) ~= nil + end +end + +local function isIgnored(ignoreData: GitignoreData, filepath: string, isDirectory: boolean) + local matched, ignored = false, false + + local relativePath = path.format(path.relative(ignoreData.location, filepath)) + + for _, ignore in ignoreData.ignores do + if matchesGlob(ignore, relativePath, isDirectory) then + matched = true + ignored = true + break + end + end + + if ignored then + for _, whitelist in ignoreData.whitelists do + if matchesGlob(whitelist, filepath, isDirectory) then + matched = true + ignored = false + break + end + end + end + + if not matched and ignoreData.next then + return isIgnored(ignoreData.next, filepath, isDirectory) + end + + return ignored +end + +--- Checks whether the given file matches an ignore +function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.path) + local directory = if #filepath.parts > 0 then path.dirname(filepath) else nil + assert(directory ~= nil, "filepath has no directory!") + + local ignoreData = self:_readIgnoreRecursive(directory) + return isIgnored(ignoreData, path.format(filepath), false) +end + +function IgnoreResolver.isIgnoredDirectory(self: IgnoreResolver, directorypath: path.path) + -- Hardcode: skip the .git directory + local basename = path.basename(directorypath) + if basename == ".git" then + return true + end + + local parentDirectory = if basename then path.dirname(directorypath) else nil + if parentDirectory then + local ignoreData = self:_readIgnoreRecursive(parentDirectory) + return isIgnored(ignoreData, path.format(directorypath), true) + else + return false + end +end + +function IgnoreResolver._readIgnoreRecursive(self: IgnoreResolver, directory: string) + if self._ignoreCache[directory] ~= nil then + return self._ignoreCache[directory] + end + + local parentPath = if path.basename(directory) then path.dirname(directory) else nil + local baseIgnores = if parentPath then self:_readIgnoreRecursive(parentPath) else nil + local gitignorePath = path.join(directory, ".gitignore") + + local exists, fileType = pcall(fs.type, gitignorePath) + + if exists and fileType == "file" then + local myIgnoreData = parseGitignore(gitignorePath) + + if #myIgnoreData.ignores > 0 or #myIgnoreData.whitelists > 0 then + myIgnoreData.next = baseIgnores + baseIgnores = myIgnoreData + end + end + + if not baseIgnores then + baseIgnores = { location = directory, ignores = {}, whitelists = {}, next = nil } :: GitignoreData + end + assert(baseIgnores, "Luau") + + self._ignoreCache[directory] = baseIgnores + return baseIgnores +end + +return IgnoreResolver diff --git a/lute/cli/commands/lint/README.MD b/lute/cli/commands/lint/README.MD new file mode 100644 index 000000000..c661396b9 --- /dev/null +++ b/lute/cli/commands/lint/README.MD @@ -0,0 +1,17 @@ +# Lute Lint + +`lute lint` is a programmable linter for Luau code, shipped as part of Lute. +As a linter, it works to statically analyze the user's code to warn them about common pitfalls they may be falling into, or to nudge them away from discouraged coding practices. +It is _programmable_ meaning that you can write a new lint rule for your Luau code _in Luau_. +It's also built on top of the official Luau language stack, allowing it to leverage the same parser used by Luau and Roblox, unlike third-party linters that rely on separate, custom parser implementations. + +## Usage + +`lute lint` can be invoked by calling `lute lint ` or `lute lint ` (use `lute lint --help` for more uses). +`lute lint` comes with a set of default lint rules which will run automatically when it is called. +This collection of builtin default lint rules is under active development. +We're also working on [Mandolin](https://github.com/luau-lang/mandolin), a VSCode extension to surface lint violations in the editor, rather than just on the command line. + +## Contact + +Both `lute lint` and Mandolin are under active development. If you run into any problems or have any questions, feel free to reach out to skanosue@roblox.com or the rest of the Luau team! diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau new file mode 100644 index 000000000..d0be255b4 --- /dev/null +++ b/lute/cli/commands/lint/init.luau @@ -0,0 +1,473 @@ +local cli = require("./lib/cli") +local files = require("./lib/files") +local fs = require("@std/fs") +local json = require("@std/json") +local lsp = require("@self/lsp") +local luau = require("@std/luau") +local pathLib = require("@std/path") +local printer = require("@self/printer") +local process = require("@std/process") +local syntax = require("@std/syntax") +local tableext = require("@std/tableext") +local triviaUtils = require("@std/syntax/utils/trivia") +local types = require("@self/types") +local visitor = require("@std/syntax/visitor") + +local DEFAULT_RULES = { "almost_swapped", "constant_table_comparison", "divide_by_zero", "parenthesized_conditions" } +local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" +local VERBOSE = false + +local function printHelp() + print(USAGE) + print([[ +Lint the specified Luau file using the specified lint rule(s) or using the default rules. + +OPTIONS: + -h, --help Show this help message + -v, --verbose Enable verbose output + -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder + is provided, any subfolders containing init.luau files will be treated as + modules exporting lint rules, while all other .luau files will be treated + as individual lint rules. If unspecified, the default lint rules are used. + -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. + -s, --string-input Lint the provided string input instead of reading from files. + --auto-fix Automatically apply fixes for lint violations that provide a suggested fix. + Assumes that suggested fixes do not overlap. + +PATHS: + Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. + +EXAMPLES: + lute lint -r examples/lints/almost_swapped.luau bad_swap.luau + lute lint -r examples/lints/ lintee.luau src_code/ +]]) +end + +local function assertIsLintRule(rule: unknown, path: string) + assert(rule, `{path} failed to require`) + assert(typeof(rule) == "table", `{path} must return a table`) + assert(typeof(rule.name) == "string", `{path} must return a table with a 'name' string property`) + assert( + typeof(rule.lint) == "function", -- LUAUFIX: type error on loaded.lint because loaded has been refined to { read name: string } + `{path} must return a table with a 'lint' function property` + ) +end + +local function loadLintRule(path: string): types.LintRule + local loaded = luau.loadbypath(path) + assertIsLintRule(loaded, path) + + return loaded -- Typecast isn't needed because loaded is refined to any bc of type error in assertIsLintRule +end + +local function loadLintRules(path: string): { types.LintRule } + assert(fs.exists(path), `Lint rule at path '{path}' does not exist`) + + local rules = {} + + local queue: { string } = { path } + while #queue > 0 do + local currentPath = table.remove(queue, 1) + assert(currentPath, "We should never pop from an empty queue") + + if fs.metadata(currentPath).type == "dir" then + local currentPathObj = pathLib.parse(currentPath) + local initPath = pathLib.format(pathLib.join(currentPathObj, "init.luau")) + if fs.exists(initPath) then + table.insert(rules, loadLintRule(initPath)) + else + local entries = fs.listdirectory(currentPath) + local childPaths = tableext.map(entries, function(entry) + return pathLib.format(pathLib.join(currentPathObj, entry.name)) + end) + tableext.extend(queue, childPaths) + end + elseif pathLib.extname(currentPath) == ".luau" then + table.insert(rules, loadLintRule(currentPath)) + end + end + + return rules +end + +local function loadDefaultRules(): { types.LintRule } + local rules = {} + for _, ruleName in DEFAULT_RULES do + local path = `@self/rules/{ruleName}` + local rule: types.LintRule = require(path) :: any -- Anycast to silence error from dynamic require + assertIsLintRule(rule, path) + table.insert(rules, rule) + end + return rules +end + +local function parseDirectives(node: syntax.AstNode): { [string]: boolean } + local directives: { [string]: boolean } = {} + + local leadingTrivia = triviaUtils.leftmosttrivia(node) + for _, trivia in leadingTrivia do + if trivia.tag == "blockcomment" or trivia.tag == "comment" then + for rule in trivia.text:gmatch("lute%-lint%-ignore%((.+)%)") do + directives[rule] = true -- True means ignore + end + for rule in trivia.text:gmatch("lute%-lint%-report%((.+)%)") do + directives[rule] = false -- False means report + end + end + end + + return directives +end + +local function maybeParseDirectives( + node: syntax.AstNode, + cache: { [syntax.AstNode]: { [string]: boolean } } +): { [string]: boolean } + local directives = cache[node] + if directives == nil then + directives = parseDirectives(node) + + if node.kind == "stat" and node.tag == "block" then + -- The logic here is a little different, since we only want suppression to apply to the first statement + -- Store a sentinel value at this node to avoid reparsing suppressions + cache[node] = {} + -- Parse the rules that this node suppresses and attach them to the first statement of the block + cache[node.statements[1]] = directives + end + + cache[node] = directives + end + return directives +end + +local function violationIsSuppressed( + violation: types.LintViolation, + ast: syntax.AstStatBlock, + directiveCache: { [syntax.AstNode]: { [string]: boolean } }, + globalIgnores: { [string]: true } +): boolean + local violationSuppressed = globalIgnores[violation.lintname] == true + + -- We want to find the lint directive with the tightest scope that applies to this violation + local function nodeIsSuppressed(node: syntax.AstNode): boolean + if syntax.span.subsumes(violation.location, node.location) then + -- The first node which is fully contained within a violation has the tightest relevant directive + local directives = maybeParseDirectives(node, directiveCache) + violationSuppressed = if directives[violation.lintname] ~= nil + then directives[violation.lintname] + else violationSuppressed + -- No other fully contained nodes can affect whether we suppress, so we can stop recursing + return false + end + + if not syntax.span.subsumes(node.location, violation.location) then + -- Violation doesn't come from this node, so don't recurse deeper + return false + end + + local directives = maybeParseDirectives(node, directiveCache) + + -- AstStatBlock suppressions should only apply to the first statement, not the block itself + if node.kind == "stat" and node.tag == "block" then + return true + end + + -- Use any directives found on this node to update suppression status + violationSuppressed = if directives[violation.lintname] ~= nil + then directives[violation.lintname] + else violationSuppressed + + -- A deeper node might affect whether we suppress, so continue recursing + return true + end + + local suppressionVisitor = visitor.create(nodeIsSuppressed) + + visitor.visitblock(ast, suppressionVisitor) + + return violationSuppressed +end + +local function parseGlobalIgnores(trivia: { syntax.Trivia }): { [string]: true } + local globalIgnores: { [string]: true } = {} + + for _, triv in trivia do + if triv.tag == "blockcomment" or triv.tag == "comment" then + for rule in triv.text:gmatch("lute%-lint%-global%-ignore%((.+)%)") do + globalIgnores[rule] = true + end + end + end + + return globalIgnores +end + +local function lintString( + source: string, + lintRules: { types.LintRule }, + inputFilePath: pathLib.path? +): { types.LintViolation } + if VERBOSE then + print(`Parsing input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) + end + local ast = syntax.parseblock(source) + + if VERBOSE then + print(`Parsing global lint ignores in input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) + end + local globalIgnores = parseGlobalIgnores(triviaUtils.leftmosttrivia(ast)) + + if VERBOSE then + print("Applying lint rules") + end + local violations: { types.LintViolation } = {} + local directiveCache = {} + for _, rule in lintRules do + local success, err = pcall(rule.lint, ast, inputFilePath) + if success then + -- On success, err contains the returned violations + for _, violation in err do + if not violationIsSuppressed(violation, ast, directiveCache, globalIgnores) then + table.insert(violations, violation) + end + end + else + print(`Error applying lint rule '{rule.name}': {err}`) + end + end + + table.sort( + violations, + function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirecitonal inference should mean that annotations on a and b aren't needed + return a.location < b.location + end + ) + + return violations +end + +type fix = { + location: syntax.span, + replacement: string, +} + +local function applySuggestedFixes(source: string, fixes: { fix }): string + local lines = source:split("\n") + + -- Sort fixes by starting location descending to avoid messing up locations of earlier fixes + table.sort(fixes, function(a: fix, b: fix) + return not (a.location < b.location) + end) + + for _, fix in fixes do + local beginLine = fix.location.beginline + local endLine = fix.location.endline + local beginColumn = fix.location.begincolumn + local endColumn = fix.location.endcolumn + + if beginLine == endLine then + -- Single-line fix + local line = lines[beginLine] + lines[beginLine] = line:sub(1, beginColumn - 1) .. fix.replacement .. line:sub(endColumn) + else + -- Multi-line fix + local firstLine = lines[beginLine] + local lastLine = lines[endLine] + + lines[beginLine] = firstLine:sub(1, beginColumn - 1) .. fix.replacement .. lastLine:sub(endColumn) + + -- Remove the lines that were replaced + for i = beginLine + 1, endLine do + lines[i] = nil -- this is cheaper than doing table.remove, but we need to handle the resulting holes later + end + end + end + + local newLines: { string } = {} + for _, line in lines do -- we rely on the assumption that we'll see the entries in the same order they were created in the array + table.insert(newLines, line) + end + + return table.concat(newLines, "\n") +end + +local function lintFile( + inputFilePath: pathLib.path, + lintRules: { types.LintRule }, + autofixEnabled: boolean +): { types.LintViolation } + if VERBOSE then + print(`Reading input file '{inputFilePath}'`) + end + local fileContent = fs.readfiletostring(inputFilePath) + + local violations = lintString(fileContent, lintRules, inputFilePath) + + if not autofixEnabled then + return violations + end + + local hasSuggestedFixes = tableext.any(violations, function(violation) + return violation.suggestedfix ~= nil + end) + + if not hasSuggestedFixes then + return violations + end + + if VERBOSE then + print(`Applying suggested fixes to file '{inputFilePath}'`) + end + + local fixes: { fix } = {} + for _, violation in violations do + if violation.suggestedfix ~= nil then + local fixLocation = violation.suggestedfix.location or violation.location + table.insert(fixes, { location = fixLocation, replacement = violation.suggestedfix.fix }) + end + end + + local newContent = applySuggestedFixes(fileContent, fixes) + + fs.writestringtofile(inputFilePath, newContent) + + return lintString(newContent, lintRules, inputFilePath) +end + +local function lintPaths( + inputFilePaths: { string }, + lintRules: { types.LintRule }, + autofixEnabled: boolean +): { [pathLib.path]: { types.LintViolation } } + if VERBOSE then + print("Filtering input files by gitignore") + end + local inputFiles = files.getSourceFiles(inputFilePaths, VERBOSE) + + local allViolations: { [pathLib.path]: { types.LintViolation } } = {} + + for _, inputPath in inputFiles do + local walker = fs.walk(inputPath, { recursive = true }) + + local curr = walker() + while curr ~= nil do + local inputFilePath = pathLib.parse(curr) + + local success, err = pcall(lintFile, inputFilePath, lintRules, autofixEnabled) + if success then + -- Violations are returned as the second return value from pcall + allViolations[inputFilePath] = err + else + print(`Error linting file '{inputFilePath}': {err}`) + end + + curr = walker() + end + end + + return allViolations +end + +local function main(...: string) + local args = cli.parser() + + args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) + args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) + args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) + args:add("json", "flag", { help = "Output lint violations in JSON format", aliases = { "j" } }) + args:add( + "string-input", + "option", + { help = "Lint the provided string input instead of reading from files", aliases = { "s" } } + ) + args:add( + "auto-fix", + "flag", + { help = "Automatically apply fixes for lint violations that provide a suggested fix" } + ) + + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + VERBOSE = args:has("verbose") + + local inputFiles = args:forwarded() + local stringInput = args:get("string-input") + if (inputFiles == nil or #inputFiles == 0) and not stringInput then + print("Error: No input files or string input specified.\n\n" .. USAGE .. "\nUse --help for more information.") + process.exit(1) + end + + local lintRules: { types.LintRule } + local rulePath = args:get("rules") + if rulePath == nil then + if VERBOSE then + print("Using default lint rules.") + end + lintRules = loadDefaultRules() + else + if VERBOSE then + print(`Loading lint rule(s) from '{rulePath}'`) + end + lintRules = loadLintRules(rulePath) + end + + if stringInput ~= nil then + if args:has("auto-fix") then + print("Auto-fix has no effect on string input.") + end + + local violations = lintString(stringInput, lintRules) + + if args:has("json") then + local diagnostics = tableext.map(violations, lsp.diagnostic) + print(json.serialize(diagnostics :: json.array, true)) + elseif #violations > 0 then + if VERBOSE then + print(`Printing violations from string input\n`) + end + + printer.printLintsWithSource(violations, stringInput) + + process.exit(1) + else + if VERBOSE then + print("No lint violations found.") + end + + process.exit(0) + end + else + local autofixEnabled = args:has("auto-fix") + if autofixEnabled and VERBOSE then + print("Auto-fix is enabled.") + end + + local allViolations = lintPaths(inputFiles :: { string }, lintRules, autofixEnabled) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed + + if args:has("json") then + print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) + else + if VERBOSE then + print(`Printing violations from {#allViolations} files\n`) + end + + local violationsFound = false + + for sourcePath, violations in allViolations do + if #violations > 0 then + violationsFound = true + printer.printLintsWithPath(violations, sourcePath) + end + end + + process.exit(if violationsFound then 1 else 0) + end + end +end + +main(...) diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau new file mode 100644 index 000000000..5af844410 --- /dev/null +++ b/lute/cli/commands/lint/lsp/init.luau @@ -0,0 +1,73 @@ +local pathLib = require("@std/path") +local lintTypes = require("./types") +local lspTypes = require("@self/types") +local tableext = require("@std/tableext") + +local lsp = {} + +local function severity(sev: lintTypes.severity): number + if sev == "error" then + return 1 + elseif sev == "warning" then + return 2 + elseif sev == "info" then + return 3 + else + return 4 + end +end + +local function tag(t: lintTypes.tag): number + if t == "unnecessary" then + return 1 + else + return 2 + end +end + +function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic + return table.freeze({ + range = table.freeze({ + start = table.freeze({ + line = lint.location.beginline - 1, + character = lint.location.begincolumn - 1, + }), + ["end"] = table.freeze({ + line = lint.location.endline - 1, + character = lint.location.endcolumn - 1, + }), + }), + severity = severity(lint.severity), + code = lint.lintname, + source = "lute lint" :: "lute lint", -- LUAUFIX: Annotation needed, otherwise this errors because string isn't inferred as a singleton + message = lint.message, + tags = if lint.tags then table.freeze(tableext.map(lint.tags, tag)) else nil, + }) +end + +local function workspaceDocumentDiagnosticReport( + path: pathLib.path, + violations: { lintTypes.LintViolation } +): lspTypes.WorkspaceDocumentDiagnosticReport + local report = { items = {}, uri = pathLib.format(path) } + for _, violation in violations do + table.insert(report.items, lsp.diagnostic(violation)) + end + return table.freeze(report) +end + +function lsp.workspaceDiagnosticReport( + violations: { [pathLib.path]: { lintTypes.LintViolation } } +): lspTypes.WorkspaceDiagnosticReport + local report = { + items = {}, + } + + for path, pathViolations in violations do + table.insert(report.items, workspaceDocumentDiagnosticReport(path, pathViolations)) + end + + return table.freeze(report) +end + +return table.freeze(lsp) diff --git a/lute/cli/commands/lint/lsp/types.luau b/lute/cli/commands/lint/lsp/types.luau new file mode 100644 index 000000000..c9fbe176c --- /dev/null +++ b/lute/cli/commands/lint/lsp/types.luau @@ -0,0 +1,22 @@ +export type Diagnostic = { + read range: { + read start: { read line: number, read character: number }, + read ["end"]: { read line: number, read character: number }, + }, -- zero-based + read severity: number, -- 1: Error, 2: Warning, 3: Information, 4: Hint + read code: string, -- lint name + read source: "lute lint", + read message: string, + read tags: { number }?, -- 1: Unnecessary, 2: Deprecated +} + +export type WorkspaceDocumentDiagnosticReport = { + read items: { Diagnostic }, + read uri: string, -- Note: Any lsp consumers will need to ensure this is a proper file URI. For now, we just use the file path. +} + +export type WorkspaceDiagnosticReport = { + read items: { WorkspaceDocumentDiagnosticReport }, +} + +return table.freeze({}) diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau new file mode 100644 index 000000000..831cdf423 --- /dev/null +++ b/lute/cli/commands/lint/printer.luau @@ -0,0 +1,96 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local types = require("./types") + +local printerLib = {} + +local TAB_SIZE = 4 + +local function printLint(lint: types.LintViolation, sourceLines: { string }): () + print(`{lint.severity}[{lint.lintname}]: {lint.message}\n`) + + local location = lint.location + local filepath = if lint.sourcepath then path.format(lint.sourcepath) else "input" + + if location.beginline == location.endline then + local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number + local blankGutter = string.rep(" ", gutterSize) + + -- Compute the number of tabs before the violation and within the violation + local prelintTabs = stringext.count(sourceLines[location.beginline], "\t", 1, location.begincolumn - 1) + local lintTabs = + stringext.count(sourceLines[location.beginline], "\t", location.begincolumn, location.endcolumn) + -- Convert tabs in the source line to spaces + local sourceLine = + ` {location.beginline} │ {sourceLines[location.beginline]:gsub("\t", string.rep(" ", TAB_SIZE))}` + + local filepathLine = + `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endcolumn} ──` + if #filepathLine < #sourceLine then + filepathLine ..= string.rep("─", #sourceLine - #filepathLine) + end + + print(filepathLine) + print(`{blankGutter}│`) + print(sourceLine) + print( + `{blankGutter}│ {string.rep(" ", location.begincolumn - 1 + (prelintTabs * (TAB_SIZE - 1)))}{string.rep( + "^", + location.endcolumn - location.begincolumn + (lintTabs * (TAB_SIZE - 1)) + )}` + ) + print(`{blankGutter}│`) + print() + else + local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number + local blankGutter = string.rep(" ", gutterSize) + + local subbedSourceLine = sourceLines[location.beginline]:gsub("\t", string.rep(" ", TAB_SIZE)) + local renderedSourceLines = { + ` {string.format(`%-{gutterSize - 2}d`, location.beginline)} │ ╭ {subbedSourceLine}`, + } + local maxSourceLineLength = #renderedSourceLines[1] + for lineNum = location.beginline + 1, location.endline do + subbedSourceLine = sourceLines[lineNum]:gsub("\t", string.rep(" ", TAB_SIZE)) + table.insert( + renderedSourceLines, + ` {string.format(`%-{gutterSize - 2}d`, lineNum)} │ │ {subbedSourceLine}` + ) + maxSourceLineLength = math.max(maxSourceLineLength, #renderedSourceLines[#renderedSourceLines]) + end + + local filepathLine = + `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endline}:{location.endcolumn} ──` + if #filepathLine < maxSourceLineLength then + filepathLine ..= string.rep("─", maxSourceLineLength - #filepathLine) + end + + print(filepathLine) + print(`{blankGutter}│`) + for _, line in renderedSourceLines do + print(line) + end + print(`{blankGutter}│ ╰{string.rep("─", maxSourceLineLength - gutterSize - 8)}^`) + print(`{blankGutter}│`) + print() + end + + if lint.suggestedfix then + print(`Suggested fix: {lint.suggestedfix.fix}\n`) + end +end + +function printerLib.printLintsWithSource(lints: { types.LintViolation }, sourceContent: string): () + local sourceLines = sourceContent:split("\n") + + for _, lint in lints do + printLint(lint, sourceLines) + end +end + +function printerLib.printLintsWithPath(lints: { types.LintViolation }, sourcePath: path.path): () + printerLib.printLintsWithSource(lints, fs.readfiletostring(sourcePath)) +end + +return table.freeze(printerLib) diff --git a/lute/cli/commands/lint/rules/README.md b/lute/cli/commands/lint/rules/README.md new file mode 100644 index 000000000..cd3b3a505 --- /dev/null +++ b/lute/cli/commands/lint/rules/README.md @@ -0,0 +1 @@ +To add a new default lint rule to `lute lint`, create a module or luau file defining the rule in this folder. Then, add the name of the rule to the DEFAULT_RULES constant in `lint/init.luau`, and add a test for the rule in `tests/cli/lint.test.luau`. The expected type of a lint rule is specified in `lint/types.luau`. \ No newline at end of file diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau new file mode 100644 index 000000000..5e2ffc60b --- /dev/null +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -0,0 +1,114 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local stringext = require("@std/stringext") +local syntax = require("@std/syntax") +local printer = require("@std/syntax/printer") +local utils = require("@std/syntax/utils") + +local name = "almost_swapped" +local message = "This looks like a failed attempt to swap." + +local compFuncs = {} + +function compFuncs.exprLocalsSame(a: syntax.AstExprLocal, b: syntax.AstExprLocal): boolean + return a["local"] == b["local"] +end + +function compFuncs.exprGlobalsSame(a: syntax.AstExprGlobal, b: syntax.AstExprGlobal): boolean + return a.name.text == b.name.text +end + +function compFuncs.exprIndexNamesSame(a: syntax.AstExprIndexName, b: syntax.AstExprIndexName): boolean + if a.index.text ~= b.index.text then + return false + end + + return compFuncs.refExprsSame(a.expression, b.expression) +end + +function compFuncs.exprIndexExprsSame(a: syntax.AstExprIndexExpr, b: syntax.AstExprIndexExpr): boolean + if a.index.tag == "string" and b.index.tag == "string" then + if a.index.text ~= b.index.text then + return false + else + return compFuncs.refExprsSame(a.expression, b.expression) + end + else + return compFuncs.refExprsSame(a.expression, b.expression) and compFuncs.refExprsSame(a.index, b.index) + end +end + +function compFuncs.refExprsSame(a: syntax.AstExpr, b: syntax.AstExpr): boolean + if a.tag ~= b.tag then + return false + end + + if a.tag == "local" then + return compFuncs.exprLocalsSame(a, b :: syntax.AstExprLocal) + elseif a.tag == "global" then + return compFuncs.exprGlobalsSame(a, b :: syntax.AstExprGlobal) + elseif a.tag == "indexname" then + return compFuncs.exprIndexNamesSame(a, b :: syntax.AstExprIndexName) + elseif a.tag == "index" then + return compFuncs.exprIndexExprsSame(a, b :: syntax.AstExprIndexExpr) + else + return false + end +end + +-- Report instances of attempted swaps like: +-- a = b; b = a +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + local violations = {} + + local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes + + for _, block in nodes do + for i = 1, #block.statements - 1 do + local currStat = block.statements[i] + if currStat.tag ~= "assign" or #currStat.values ~= 1 or #currStat.variables ~= 1 then + continue + end + + local nextStat = block.statements[i + 1] + if nextStat.tag ~= "assign" or #nextStat.values ~= 1 or #nextStat.variables ~= 1 then + continue + end + + local currVar, currVal = currStat.variables[1].node, currStat.values[1].node + local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node + + if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then + local currVarStr = stringext.trim(printer.printnode(currVar)) + local currValStr = stringext.trim(printer.printnode(currVal)) + local suggestedFix = `{currVarStr}, {currValStr} = {currValStr}, {currVarStr}` + + table.insert(violations, { + lintname = name, + location = syntax.span.create({ + beginline = currStat.location.beginline, + begincolumn = currStat.location.begincolumn, + endline = nextStat.location.endline, + endcolumn = nextStat.location.endcolumn, + }), + message = message, + severity = "warning" :: "warning", -- LUAUFIX: cast needed because severity isn't inferred as a singleton + sourcepath = sourcepath, + suggestedfix = { + fix = suggestedFix, + }, + }) + end + end + end + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau new file mode 100644 index 000000000..c7bf74b5b --- /dev/null +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -0,0 +1,75 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local stringext = require("@std/stringext") +local syntax = require("@std/syntax") +local syntaxPrinter = require("@std/syntax/printer") +local utils = require("@std/syntax/utils") + +local name = "constant_table_comparison" +local message = + "Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false." + +local function isTableLiteral(expr: syntax.AstExpr): boolean + return expr.tag == "table" + or (expr.tag == "group" and isTableLiteral(expr.expression)) + or (expr.tag == "cast" and isTableLiteral(expr.operand)) +end + +local function isEmptyTableLiteral(expr: syntax.AstExpr): boolean + if expr.tag == "table" then + return #expr.entries == 0 + elseif expr.tag == "group" then + return isEmptyTableLiteral(expr.expression) + elseif expr.tag == "cast" then + return isEmptyTableLiteral(expr.operand) + else + return false + end +end + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + return query + .findallfromroot(ast, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "==" or bin.operator.text == "~=" + end) + :filter(function(bin) + return isTableLiteral(bin.rhsoperand) or isTableLiteral(bin.lhsoperand) + end) + :maptoarray( + function( + bin: syntax.AstExprBinary + ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation + local suggestedFix: string? = nil + + if isEmptyTableLiteral(bin.lhsoperand) then + suggestedFix = + `next({stringext.trim(syntaxPrinter.printnode(bin.rhsoperand))}) {bin.operator.text} nil` + elseif isEmptyTableLiteral(bin.rhsoperand) then + suggestedFix = + `next({stringext.trim(syntaxPrinter.printnode(bin.lhsoperand))}) {bin.operator.text} nil` + end + + return { + lintname = name, + location = bin.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + suggestedfix = if suggestedFix ~= nil + then { + fix = suggestedFix, + } + else nil, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau new file mode 100644 index 000000000..a3a34f853 --- /dev/null +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -0,0 +1,39 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "divide_by_zero" +local message = "Division by zero detected." + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + return query + .findallfromroot(ast, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" + end) + :filter(function(bin) + return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 + end) + :maptoarray( + function( + n: syntax.AstExprBinary + ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation + return { + lintname = name, + location = n.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau new file mode 100644 index 000000000..22b916b69 --- /dev/null +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -0,0 +1,81 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntaxPrinter = require("@std/syntax/printer") + +local name = "parenthesized_conditions" +local message = "Luau doesn't require parentheses around conditions. You can remove them for readability." + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + local violations: { lintTypes.LintViolation } = {} + + query + .findallfromroot(ast, function(node: syntax.AstNode): (syntax.AstExprIfElse | syntax.AstStatIf)? + if (node.kind == "expr" or node.kind == "stat") and node.tag == "conditional" then + return node + end + return nil + end) + :foreach(function(ifStat) + if ifStat.condition.tag == "group" then + table.insert(violations, { + lintname = name, + location = ifStat.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printnode(ifStat.condition.expression), + }, + }) + end + + for _, elseIfStat in ifStat.elseifs do + if elseIfStat.condition.tag == "group" then + table.insert(violations, { + lintname = name, + location = elseIfStat.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printnode(elseIfStat.condition.expression), + }, + }) + end + end + end) + + query + .findallfromroot(ast, function(node: syntax.AstNode): (syntax.AstStatWhile | syntax.AstStatRepeat)? + if node.kind == "stat" and (node.tag == "while" or node.tag == "repeat") then + return node + end + return nil + end) + :filter(function(n) + return n.condition.tag == "group" + end) + :foreach(function(n) + table.insert(violations, { + lintname = name, + location = n.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printnode((n.condition :: syntax.AstExprGroup).expression), + }, + }) + end) + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau new file mode 100644 index 000000000..0fb1872c9 --- /dev/null +++ b/lute/cli/commands/lint/types.luau @@ -0,0 +1,25 @@ +local syntax = require("@std/syntax") +local path = require("@std/path") + +export type severity = "error" | "warning" | "info" | "hint" +export type tag = "unnecessary" | "deprecated" + +export type LintViolation = { + read lintname: string, + read location: syntax.span, + read message: string, + read severity: severity, + read sourcepath: path.path?, + read suggestedfix: { + read location: syntax.span?, -- if nil, applies to whole violation location + read fix: string, + }?, + read tags: { tag }?, +} + +export type LintRule = { + read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?) -> { LintViolation }, + read name: string, +} + +return {} diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 3b4a0056f..a0bd745ae 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -1,20 +1,56 @@ local definitions = require("@self/generated/definitions") -local fs = require("@lute/fs") -local process = require("@lute/process") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local json = require("@std/json") -local homedir = process.homedir() +local TYPEDEFS_PATH = path.parse(".lute/typedefs/0.1.0") +local BASE_PATH = path.join(process.homedir(), TYPEDEFS_PATH) +local BASE_PATH_PRETTY = `~/{TYPEDEFS_PATH}` +local TEMPLATE_RC_FILE = { + aliases = { + lint = `{BASE_PATH_PRETTY}/lint`, + lute = `{BASE_PATH_PRETTY}/lute`, + std = `{BASE_PATH_PRETTY}/std`, + }, +} + +local args = { ... } --- Already exists -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. -print("Found existing /.lute/ directory! Overwriting type definitions.") -if not fs.exists(homedir .. "/.lute") then - fs.mkdir(homedir .. "/.lute") - fs.mkdir(homedir .. "/.lute/typedefs") - fs.mkdir(homedir .. "/.lute/typedefs/0.1.0") +print(`Writing definitions at {BASE_PATH_PRETTY}`) +if fs.exists(BASE_PATH) then + fs.removedirectory(BASE_PATH, { recursive = true }) end +fs.createdirectory(BASE_PATH, { makeparents = true }) for key, value in definitions :: { [string]: string } do - fs.writestringtofile(homedir .. "/.lute/typedefs/0.1.0/" .. key, value) + local filePath = path.join(BASE_PATH, key) + fs.createdirectory(path.dirname(filePath), { makeparents = true }) + fs.writestringtofile(filePath, value) +end + +print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) + +if not table.find(args, "--with-luaurc") then + return +end + +local luauRcPath = path.join(process.cwd(), ".luaurc") +local rcFile = nil +print(`Writing luaurc file at {luauRcPath}`) + +if fs.exists(luauRcPath) then + local contents = fs.readfiletostring(luauRcPath) + rcFile = json.deserialize(contents) :: any + + rcFile.aliases = rcFile.aliases or {} + for key, value in TEMPLATE_RC_FILE.aliases :: { [string]: string } do + rcFile.aliases[key] = value + end +else + rcFile = TEMPLATE_RC_FILE end +fs.writestringtofile(luauRcPath, json.serialize(rcFile, true)) -print("Successfully wrote type definition files to `~/.lute/typedefs/0.1.0/`") +print(`Successfully wrote luaurc file to {luauRcPath}`) diff --git a/lute/cli/commands/test/filter.luau b/lute/cli/commands/test/filter.luau new file mode 100644 index 000000000..7d045ba9c --- /dev/null +++ b/lute/cli/commands/test/filter.luau @@ -0,0 +1,71 @@ +local testtypes = require("@std/test/types") + +local function filtertests( + env: testtypes.testenvironment, + suiteName: string?, + caseName: string? +): testtypes.testenvironment + local filtered: testtypes.testenvironment = { + anonymous = {}, + suites = {}, + suiteindex = {}, + caseindex = {}, + } + + -- No filters: return all tests + if not suiteName and not caseName then + return env + end + + -- Filter by both suite and case name + if suiteName and caseName then + local caseEntry = env.caseindex[caseName] + if caseEntry and caseEntry.suites[suiteName] then + local suite = env.suiteindex[suiteName] + if suite then + local filteredSuite = { + name = suite.name, + cases = caseEntry.suites[suiteName], + _beforeeach = suite._beforeeach, + _beforeall = suite._beforeall, + _aftereach = suite._aftereach, + _afterall = suite._afterall, + } + table.insert(filtered.suites, filteredSuite) + end + end + + -- Filter by suite name only + elseif suiteName then + local suite = env.suiteindex[suiteName] + if suite then + table.insert(filtered.suites, suite) + end + elseif caseName then + local caseEntry = env.caseindex[caseName] + if caseEntry then + -- Add all anonymous tests with this case name + filtered.anonymous = caseEntry.anonymous + + -- Add all suites that have tests with this case name + for sName, cases in caseEntry.suites do + local suite = env.suiteindex[sName] + if suite then + local filteredSuite = { + name = suite.name, + cases = cases, + _beforeeach = suite._beforeeach, + _beforeall = suite._beforeall, + _aftereach = suite._aftereach, + _afterall = suite._afterall, + } + table.insert(filtered.suites, filteredSuite) + end + end + end + end + + return filtered +end + +return table.freeze({ filtertests = filtertests }) diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau new file mode 100644 index 000000000..318b8ec11 --- /dev/null +++ b/lute/cli/commands/test/finder.luau @@ -0,0 +1,32 @@ +local ps = require("@std/process") +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local tableext = require("@std/tableext") + +local function istestfile(filename: string): boolean + return stringext.hassuffix(filename, ".test.luau") or stringext.hassuffix(filename, ".spec.luau") +end + +local function findtestfiles(paths: { string }): { path.path } + local files = {} + local function findfiles(p: path.pathlike) + local it = fs.walk(path.normalize(p), { recursive = true }) + local file = it() + while file do + if fs.type(file) ~= "dir" and istestfile(path.format(file)) then + table.insert(files, path.normalize(file)) + end + file = it() + end + end + + local cwd = ps.cwd() + for _, p in paths do + findfiles(path.join(cwd, p)) + end + + return files +end + +return table.freeze({ findtestfiles = findtestfiles }) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau new file mode 100644 index 000000000..ea3c01d9d --- /dev/null +++ b/lute/cli/commands/test/init.luau @@ -0,0 +1,131 @@ +local finder = require("@self/finder") +local filter = require("@self/filter") +local luau = require("@std/luau") +local path = require("@std/path") +local ps = require("@std/process") +local testtypes = require("@std/test/types") +local runner = require("@std/test/runner") +local reporter = require("@std/test/reporter") +local test = require("@std/test") + +local function printHelp() + print([[ +Usage: lute test [OPTIONS] [PATHS...] + +Run tests discovered in .test.luau and .spec.luau files. + +OPTIONS: + -h, --help Show this help message + --list List all discovered test cases without running them + -s, --suite=SUITE Run only tests in the specified suite + -c, --case=CASE Run only test cases matching the specified name + +PATHS: + Directories or files to search for tests (default: ./tests) + +EXAMPLES: + lute test Run all tests in ./tests + lute test --list List all test cases + lute test -s MyTestSuite Run all tests in MyTestSuite + lute test --suite MyTestSuite --case mytest Run specific test in suite + lute test --case "some case" Run all test cases named "some case" +]]) +end + +local function loadtests(testfiles: { path.path }) + -- Load all test files first + for _, p in testfiles do + local success, err = pcall(luau.loadbypath, p, nil) + + if not success then + print(`Error loading {path.format(p)}: {err}`) + end + end +end + +local function listtests() + -- Gets all registered tests using the shared test instance + local registered = test._registered() + local totalTests = #registered.anonymous + + for _, suite in registered.suites do + totalTests += #suite.cases + end + + print("Anonymous:") + for _, tc in registered.anonymous do + print(`\t{tc.name}`) + end + + for _, suite in registered.suites do + print(`{suite.name}:`) + for _, tc in suite.cases do + print(`\t{tc.name}`) + end + end +end + +local function runtests(env: testtypes.testenvironment, runner: testtypes.testrunner, reporter: testtypes.testreporter) + local result = runner(env) + + reporter(result) + if result.failed ~= 0 then + ps.exit(1) + end + ps.exit(0) +end + +local function main(...: string) + local args = { ... } + local testPaths = {} + local isListMode = false + local suiteName: string? = nil + local caseName: string? = nil + + local i = 1 + while i <= #args do + local arg = args[i] + + if arg == "-h" or arg == "--help" then + printHelp() + return + elseif arg == "--list" then + isListMode = true + i += 1 + elseif arg == "-s" or arg == "--suite" then + i += 1 + if i <= #args then + suiteName = args[i] + i += 1 + else + print(`Error: {arg} requires a value`) + ps.exit(1) + end + elseif arg == "-c" or arg == "--case" then + i += 1 + if i <= #args then + caseName = args[i] + i += 1 + else + print(`Error: {arg} requires a value`) + ps.exit(1) + end + else + table.insert(testPaths, arg) + i += 1 + end + end + + local searchpath = if #testPaths > 0 then testPaths else { "./tests" } + loadtests(finder.findtestfiles(searchpath)) + + if isListMode then + listtests() + else + local env = test._registered() + local filteredEnv = filter.filtertests(env, suiteName, caseName) + runtests(filteredEnv, runner.run, reporter.simple) + end +end + +main(...) diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau new file mode 100644 index 000000000..5a9bb7863 --- /dev/null +++ b/lute/cli/commands/transform/init.luau @@ -0,0 +1,174 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local pathLib = require("@std/path") +local printer = require("@std/syntax/printer") +local syntax = require("@std/syntax") + +local arguments = require("@self/lib/arguments") +local files = require("./lib/files") +local types = require("@self/lib/types") + +local function exhaustiveMatch(value: never): never + error(`Unknown value in exhaustive match: {value}`) +end + +local function loadMigration(path: string): types.Migration + local success, loaded = pcall(luau.loadbypath, path) + assert(success, `{path} failed to require: {loaded}`) + assert(loaded, `{path} is missing a return`) + + if typeof(loaded) == "function" then + local migration = {} + migration.transform = loaded + return migration :: types.Migration -- FIXME: Luau + elseif typeof(loaded) == "table" then + -- FIXME(Luau): https://github.com/luau-lang/luau/issues/1803 for the type assertions + assert( + typeof((loaded :: any).transform) == "function", + `{path} must contain a 'transform' property of type function` + ) + if (loaded :: any).options then + assert(typeof(loaded.options) == "table", `Expected return of {path}.options to be a table`) + for _, option in loaded.options do + assert(typeof(option.name) == "string", `Expected option.name to be a string`) + assert( + option.kind == "string" or option.kind == "boolean", + `Expected option.kind to be either 'string' or 'boolean', got {option.kind}` + ) + end + end + if (loaded :: any).initialize then + assert(typeof(loaded.initialize) == "function", `Expected {path}.initialize to be a function`) + end + return loaded :: types.Migration + else + error(`Expected '{path}' to return a function or table, got {typeof(loaded)}`) + end +end + +local function processMigrationOptions(migration: types.Migration, options: { [string]: string }): { [string]: any } + if migration.options == nil then + return table.freeze({}) :: any -- FIXME(Luau): https://github.com/luau-lang/luau/issues/1802 + end + + local processedOptions: { [string]: string | boolean } = {} + + for _, option in migration.options do + local value = options[option.name] + if value then + if option.kind == "boolean" then + local lowered = value:lower() + assert( + lowered == "true" or lowered == "false", + `Option '--{option.name}' expects a boolean value 'true' or 'false'` + ) + processedOptions[option.name] = lowered == "true" + elseif option.kind == "string" then + processedOptions[option.name] = value + else + exhaustiveMatch(option.kind) + end + else + assert(option.default ~= nil, `Missing required option '--{option.name}'`) + processedOptions[option.name] = option.default + end + end + + return table.freeze(processedOptions) +end + +local function applyMigration( + migration: types.Migration, + paths: { pathLib.path }, + options: { [string]: string | boolean }, + dryRun: boolean, + outputPath: string? +) + local deletedPaths = {} + + if migration.initialize then + print("Initializing migration") + local ctx = { + options = options, + } + migration.initialize(ctx) + end + + local outputFilePath = if outputPath then pathLib.parse(outputPath) else nil + + for _, pathObj in paths do + local pathStr = pathLib.format(pathObj) + + print(`Applying to {pathStr}`) + + local source = fs.readfiletostring(pathStr) + + local parseresult = syntax.parse(source) + + local ctx = { + path = pathStr, + source = source, + parseresult = parseresult, + options = options, + } + + -- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace + local result = migration.transform(ctx) + + local toWrite = if typeof(result) == "string" then result else printer.printfile(parseresult, result) + + if toWrite == types.DELETION_MARKER then + if outputFilePath then + print(`Skipping writing {pathStr} as it was marked for deletion`) + else + print(`Marking {pathStr} for deletion`) + table.insert(deletedPaths, pathStr) + end + elseif not dryRun then + if outputFilePath then + assert(outputFilePath ~= nil) + fs.writestringtofile(outputFilePath, toWrite) + elseif toWrite ~= source then + fs.writestringtofile(pathStr, toWrite) + end + end + end + + if not dryRun then + for _, pathStr in deletedPaths do + print(`Deleting {pathStr}`) + fs.remove(pathStr) + end + end +end + +local function main(...: string) + local args = arguments.parse({ ... }) + + if args.dryRun then + print("Executing in dry run mode") + end + + if args.outputFile then + print(`Output path specified: '{args.outputFile}'`) + end + + print(`Loading migration '{args.migrationPath}'`) + local migration = loadMigration(args.migrationPath) + + print("Finding source files") -- todo: might type error + local files = files.getSourceFiles(args.filePaths) + if #files == 0 then + error("error: no source files provided") + end + + print("Processing migration options") + local migrationOptions = processMigrationOptions(migration, args.migrationOptions) + + print("Applying migration") + applyMigration(migration, files, migrationOptions, args.dryRun, args.outputFile) + + print(`Processed {#files} files!`) +end + +main(...) diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau new file mode 100644 index 000000000..833a4e6ce --- /dev/null +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -0,0 +1,80 @@ +local stringext = require("@std/stringext") + +type Config = { + --- Prevent making changes to files + dryRun: boolean, + --- Path to the migration module to execute + migrationPath: string, + --- Configuration flags to pass to the migration + migrationOptions: { [string]: string }, + --- List of file paths to process + --- Note: no directory traversal or filtering has been applied on this list + filePaths: { string }, + --- Path to output file (only works if a single input file is specified) + outputFile: string?, +} + +local function parse(arguments: { string }): Config + -- TODO: replace with proper CLI system + -- For now, first argument is the transformer script, whilst the rest are either flags or files to apply it on + + if #arguments < 2 then + error("Usage: lute transform [options...] ") + end + + local config = { + dryRun = false, + migrationPath = nil :: string?, + migrationOptions = {}, + filePaths = {}, + outputFile = nil, + } + + local i = 1 + while i <= #arguments do + local argument = arguments[i] + + if stringext.hasprefix(argument, "--") then + local name = stringext.removeprefix(argument, "--") + + if config.migrationPath == nil then + -- Options before the codemod file are parsed as options for the tool itself + if name == "dry-run" then + config.dryRun = true + elseif name == "output" then + i += 1 + assert(i <= #arguments, `Missing value for '--output'`) + config.outputFile = arguments[i] + else + error(`Unknown flag '--{name}'`) + end + else + i += 1 + assert(i <= #arguments, `Missing value for '{name}'`) + local value = arguments[i] + + config.migrationOptions[name] = value + end + else + if config.migrationPath == nil then + config.migrationPath = argument + else + table.insert(config.filePaths, argument) + end + end + + i += 1 + end + + assert(config.migrationPath, "ASSERTION FAILED: codemodPath ~= nil") + assert( + if config.outputFile ~= nil then #config.filePaths == 1 else true, + "ASSERTION FAILED: When specifying an output file, only one input file is allowed" + ) + + return config +end + +return { + parse = parse, +} diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/lib/types.luau new file mode 100644 index 000000000..ebb3f3942 --- /dev/null +++ b/lute/cli/commands/transform/lib/types.luau @@ -0,0 +1,37 @@ +local syntax = require("@std/syntax") +local syntaxTypes = require("@std/syntax/types") + +export type Context = { + path: string, + source: string, + parseresult: syntax.ParseResult, + options: Options, +} + +export type Transformer = (Context) -> string | syntaxTypes.replacements + +export type ConfigOption = + { + kind: "string", + name: string, + default: string?, + } + | { + kind: "boolean", + name: string, + default: boolean?, + } + +export type InitializationContext = { + options: Options, +} + +export type Migration = { + options: { ConfigOption }?, + initialize: ((InitializationContext) -> ())?, + transform: Transformer, +} + +return { + DELETION_MARKER = "$DELETION_MARKER", +} diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index d2bc33aae..30f219e14 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -1,7 +1,18 @@ #pragma once +#include +#include struct lua_State; struct Runtime; +class LuteReporter; -lua_State* setupCliState(Runtime& runtime); -int cliMain(int argc, char** argv); +int cliMain(int argc, char** argv, LuteReporter& reporter); +bool runBytecode( + Runtime& runtime, + const std::string& bytecode, + const std::string& chunkname, + lua_State* GL, + int program_argc, + char** program_argv, + LuteReporter& reporter +); diff --git a/lute/cli/include/lute/clireporter.h b/lute/cli/include/lute/clireporter.h new file mode 100644 index 000000000..fae6df254 --- /dev/null +++ b/lute/cli/include/lute/clireporter.h @@ -0,0 +1,11 @@ +#pragma once + +#include "lute/reporter.h" + +// Standard CLI reporter that writes to stderr and stdout +class CLIReporter : public LuteReporter +{ +public: + void reportError(const std::string& message) override; + void reportOutput(const std::string& message) override; +}; diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index 5a3e2ab2e..ab161f6c5 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -1,13 +1,96 @@ #pragma once +#include "lute/reporter.h" + +#include "Luau/DenseHash.h" #include "Luau/FileUtils.h" -struct AppendedBytecodeResult +#include + +struct LuteDecodeResult; + +struct LuteEncodeResult +{ + std::string payload; + size_t bytesWritten = 0; + size_t compressedPayloadSizeBytes = 0; + size_t uncompressedPayloadSizeBytes = 0; +}; + +/** + * Represents a bundle of compiled Luau files ready for injection. + * + * Uncompressed bundle format (before compression): + * [num_config_entries: uint32_t] + * For each config entry: + * [path_length: uint32_t] + * [path_val: char[path_length]] + * [luaurc_length: uint32_t] + * [luaurc_contents: char[luaurc_length]] + * For each file: + * [path_length: uint32_t] + * [path_string: char[path_length]] + * [bytecode_size: uint64_t] + * [bytecode_data: byte[bytecode_size]] + * + * Injectable payload format (after compression, what goes into executable): + * [compressed_bundle_data: byte[compressed_size]] + * [compressed_size: uint64_t] + * [uncompressed_size: uint64_t] + * [num_files: uint32_t] + * [entry_point_path_string: char[entry_point_path_length]] + * [entry_point_path_length: uint32_t] + + */ +struct LuteExePayload +{ + LuteExePayload(LuteReporter& reporter); + void add(const std::string& bundlePath, const std::string& sourcePath); + void setLuauConfig(const Luau::DenseHashMap& configs); + + std::optional encode(); + static std::optional decode(const std::string_view binary, LuteReporter& reporter); + + std::string entryPointPath; + Luau::DenseHashMap filePathToBytecode{""}; // path -> bytecode + Luau::DenseHashMap luauConfigFiles{""}; // path -> config + +private: + LuteReporter& reporter; + bool parseFromDecompressedBundle(std::string_view decompressedBundle); + std::vector filePaths; + Luau::DenseHashMap sourceToBundlePath{""}; +}; + +struct LuteDecodeResult { - bool found = false; - std::string BytecodeData; + LuteDecodeResult(LuteReporter& reporter); + LuteExePayload payload; + size_t bytesRead = 0; + size_t compressedPayloadSizeBytes = 0; + size_t uncompressedPayloadSizeBytes = 0; }; -AppendedBytecodeResult checkForAppendedBytecode(const std::string& executablePath); +/** + * Manages creating and reading Lute executables with embedded bytecode bundles. + * + * Binary format (from end of file, reading backward): + * [lute runtime executable's bytes] + * [compressed bundle data] + * [compressed_size: uint64_t] + * [uncompressed_size: uint64_t] + * [num_files: uint32_t] + * [entry_point_path_string: char[entry_point_path_length]] + * [entry_point_path_length: uint32_t] + * [MAGIC_FLAG: "LUTEBYTE" (8 bytes)] + */ +struct LuteExecutable +{ + LuteExecutable(const std::string& luteRuntimePath, LuteReporter& reporter); + + bool create(const std::string& outputPath, LuteExePayload& payload); + std::optional extract(); -int compileScript(const std::string& inputFilePath, const std::string& outputFilePath, const std::string& currentExecutablePath); + std::string executablePath; + LuteReporter& reporter; +}; diff --git a/lute/cli/include/lute/coverage.h b/lute/cli/include/lute/coverage.h new file mode 100644 index 000000000..2327570fd --- /dev/null +++ b/lute/cli/include/lute/coverage.h @@ -0,0 +1,9 @@ +#pragma once + +struct lua_State; + +void coverageInit(lua_State* L); +bool coverageActive(); + +void coverageTrack(lua_State* L, int funcindex); +void coverageDump(const char* path); diff --git a/lute/cli/include/lute/fileutils.h b/lute/cli/include/lute/fileutils.h new file mode 100644 index 000000000..15a150a07 --- /dev/null +++ b/lute/cli/include/lute/fileutils.h @@ -0,0 +1,28 @@ +// This file is part of the Lute programming language and is licensed under MIT License +#pragma once + +#include +#include + +namespace Lute +{ + +// Open a file with platform-specific handling (wfopen on Windows, fopen elsewhere) +FILE* openFile(const std::string& path, const char* mode); + +// Write string content to a file +bool writeFile(const std::string& path, const std::string& content); + +// Create a directory (non-recursive) +bool createDirectory(const std::string& path); + +// Create directories recursively (equivalent to mkdir -p) +bool createDirectories(const std::string& path); + +// Remove a file +bool removeFile(const std::string& path); + +// Remove a directory (must be empty) +bool removeDirectory(const std::string& path); + +} // namespace Lute diff --git a/lute/cli/include/lute/luauflags.h b/lute/cli/include/lute/luauflags.h new file mode 100644 index 000000000..00482a16c --- /dev/null +++ b/lute/cli/include/lute/luauflags.h @@ -0,0 +1,3 @@ +#pragma once + +void setLuauFlags(); diff --git a/lute/cli/include/lute/packagerun.h b/lute/cli/include/lute/packagerun.h new file mode 100644 index 000000000..ee6052b84 --- /dev/null +++ b/lute/cli/include/lute/packagerun.h @@ -0,0 +1,14 @@ +#pragma once + +#include "lute/userlandvfs.h" + +#include +#include +#include +#include + +std::optional getAbsolutePathToNearestLockfile(std::string entryFile); + +std::pair, std::vector>> getDependenciesFromLockfile( + const std::string& lockfilePath +); diff --git a/lute/cli/include/lute/profiler.h b/lute/cli/include/lute/profiler.h new file mode 100644 index 000000000..dadc95b96 --- /dev/null +++ b/lute/cli/include/lute/profiler.h @@ -0,0 +1,7 @@ +#pragma once + +struct lua_State; + +void profilerStart(lua_State* L, int frequency); +void profilerStop(); +void profilerDump(const char* path); diff --git a/lute/cli/include/lute/reporter.h b/lute/cli/include/lute/reporter.h new file mode 100644 index 000000000..a6ba8ce78 --- /dev/null +++ b/lute/cli/include/lute/reporter.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +// Interface for reporting output and errors from CLI operations +class LuteReporter +{ +public: + virtual ~LuteReporter() = default; + + // Report an error message (typically to stderr) + virtual void reportError(const std::string& message) = 0; + + // Report normal output (typically to stdout) + virtual void reportOutput(const std::string& message) = 0; + + // Variadic template version for formatted error messages using printf-style format specifiers + template + void formatError(const char* format, Args&&... args) + { + reportError(formatMessage(format, std::forward(args)...)); + } + + // Variadic template version for formatted output messages using printf-style format specifiers + template + void formatOutput(const char* format, Args&&... args) + { + reportOutput(formatMessage(format, std::forward(args)...)); + } + +private: + // Format implementation using snprintf + template + static std::string formatMessage(const char* format, Args&&... args) + { + // Calculate required buffer size + int size = std::snprintf(nullptr, 0, format, args...); + + if (size > 0) + { + // Allocate buffer and format the string + std::vector buffer(size + 1); + std::snprintf(buffer.data(), buffer.size(), format, args...); + + return std::string(buffer.data(), size); + } + + // Fallback if formatting fails + return std::string(format); + } +}; diff --git a/lute/cli/include/lute/requiresetup.h b/lute/cli/include/lute/requiresetup.h new file mode 100644 index 000000000..735398515 --- /dev/null +++ b/lute/cli/include/lute/requiresetup.h @@ -0,0 +1,25 @@ +#pragma once + +#include "lute/userlandvfs.h" + +#include "Luau/DenseHash.h" + +#include +#include + +struct lua_State; +struct Runtime; + +lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); + +lua_State* setupPkgCliState( + Runtime& runtime, + std::vector directDependencies, + std::vector> allDependencies +); + +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +); diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h new file mode 100644 index 000000000..d7653eac9 --- /dev/null +++ b/lute/cli/include/lute/staticrequires.h @@ -0,0 +1,56 @@ +#pragma once + +#include "lute/reporter.h" + +#include "Luau/DenseHash.h" + +#include +#include +#include + +class StaticRequireTracer +{ +public: + StaticRequireTracer(LuteReporter& reporter); + + // Trace dependencies starting from an entry point file + // entryPoint: absolute path to entry point file + void trace(const std::string& entryPoint); + + // Get discovered files as pairs of (bundlePath, absolutePath) + // bundlePath is the absolute path with the lowest common prefix stripped + std::vector> getStaticRequirePairs() const; + + // Check if an absolute path exists in the discovered files + bool containsAbsolute(const std::string& absolutePath) const; + + const std::string& getLowestCommonRoot() const + { + return lowestCommonRoot; + } + + // Get discovered .luaurc files as a map of (lcrPath -> content) + // lcrPath is the absolute path with the lowest common prefix stripped + const Luau::DenseHashMap& getLuaurcFiles() const + { + return luaurcFiles; + } + + void printRequireGraph() const; + // Find the lowest common root directory from a collection of absolute paths + static std::string findLowestCommonRoot(const std::vector& paths); + +private: + Luau::DenseHashSet visited{""}; + std::vector discovered; // Absolute paths + Luau::DenseHashMap> requireGraph{""}; // Absolute paths + Luau::DenseHashMap luaurcFiles{""}; // LCR-relative path -> content + std::string lowestCommonRoot; + + // Extract all require() paths from source code + std::vector extractRequires(const std::string& source); + + // Resolve a require path relative to the requiring file + std::optional resolveRequire(const std::string& requirer, const std::string& required); + LuteReporter& reporter; +}; diff --git a/lute/cli/include/lute/tc.h b/lute/cli/include/lute/tc.h index f88ce0231..99af61bdc 100644 --- a/lute/cli/include/lute/tc.h +++ b/lute/cli/include/lute/tc.h @@ -1,7 +1,8 @@ #pragma once -#include "Luau/FileResolver.h" -#include "Luau/Frontend.h" -#include "Luau/FileUtils.h" +#include "lute/reporter.h" -int typecheck(const std::vector& sourceFiles); +#include +#include + +int typecheck(const std::vector& sourceFiles, LuteReporter& reporter); diff --git a/lute/cli/include/lute/uvstate.h b/lute/cli/include/lute/uvstate.h new file mode 100644 index 000000000..7922f1546 --- /dev/null +++ b/lute/cli/include/lute/uvstate.h @@ -0,0 +1,12 @@ +#pragma once + +struct UvGlobalState +{ + UvGlobalState(const UvGlobalState&) = delete; + UvGlobalState& operator=(const UvGlobalState&) = delete; + UvGlobalState(UvGlobalState&&) = delete; + UvGlobalState& operator=(UvGlobalState&&) = delete; + + UvGlobalState(int argc, char** argv); + ~UvGlobalState(); +}; diff --git a/lute/cli/include/lute/version.h.in b/lute/cli/include/lute/version.h.in new file mode 100644 index 000000000..0334b964a --- /dev/null +++ b/lute/cli/include/lute/version.h.in @@ -0,0 +1,5 @@ +#pragma once + +#define LUTE_VERSION "@LUTE_VERSION@" +#define LUTE_VERSION_SUFFIX "@LUTE_VERSION_SUFFIX@" +#define LUTE_VERSION_FULL "@LUTE_VERSION_FULL@" diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index f190f69ed..073b5528c 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -1,75 +1,113 @@ -#include "Luau/Common.h" -#include "Luau/CodeGen.h" -#include "Luau/Compiler.h" -#include "Luau/FileUtils.h" -#include "Luau/Parser.h" -#include "Luau/Require.h" - -#include "lua.h" -#include "lualib.h" -#include "uv.h" +#include "lute/climain.h" #include "lute/clicommands.h" -#include "lute/clivfs.h" #include "lute/compile.h" +#include "lute/luauflags.h" +#include "lute/modulepath.h" #include "lute/options.h" +#include "lute/packagerun.h" +#include "lute/process.h" #include "lute/ref.h" -#include "lute/require.h" +#include "lute/reporter.h" +#include "lute/requiresetup.h" #include "lute/runtime.h" +#include "lute/staticrequires.h" #include "lute/tc.h" +#include "lute/version.h" + +#include "Luau/CodeGen.h" +#include "Luau/Common.h" +#include "Luau/Compiler.h" +#include "Luau/DenseHash.h" +#include "Luau/FileUtils.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include #ifdef _WIN32 #include #endif -#include -#include -#include -#include +static const char* HELP_STRING = R"(Usage: lute [options] [arguments...] -static int program_argc = 0; -static char** program_argv = nullptr; +Commands: + run (default) Run a Luau script. + check Type check Luau files. + compile Compile a Luau script into a standalone executable. + setup Generate type definition files for the language server. + transform Run a specified code transformation on specified Luau files. + lint Run linting rules on specified Luau files. -void* createCliRequireContext(lua_State* L) -{ - void* ctx = lua_newuserdatadtor( - L, - sizeof(RequireCtx), - [](void* ptr) - { - static_cast(ptr)->~RequireCtx(); - } - ); +Run Options (when using 'run' or no command): + lute [run] [args...] + Executes the script, passing [args...] to it. - if (!ctx) - luaL_error(L, "unable to allocate RequireCtx"); +Check Options: + lute check [file2.luau...] + Performs a type check on the specified files. - ctx = new (ctx) RequireCtx{CliVfs{}}; +Compile Options: + lute compile [--output ] + Compiles entry point and auto-discovered dependencies into a standalone executable. - // Store RequireCtx in the registry to keep it alive for the lifetime of - // this lua_State. Memory address is used as a key to avoid collisions. - lua_pushlightuserdata(L, ctx); - lua_insert(L, -2); - lua_settable(L, LUA_REGISTRYINDEX); +Setup Options: + lute setup + Generates type definition files for the language server. + --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file. - return ctx; -} +Transform Options: + lute transform [options...] + Runs the specified code transformation on the provided Luau files. + --dry-run Runs the transformation without actually overwriting or deleting any files. + --output Specifies an output file for a transformed file. Only valid when + transforming a single file. If not specified, files are overwritten in place. -lua_State* setupCliState(Runtime& runtime) -{ - return setupState( - runtime, - [](lua_State* L) - { - if (Luau::CodeGen::isSupported()) - Luau::CodeGen::create(L); +Lint Options: + lute lint [options...] + Runs linting rules on the specified Luau files. + --rules Path to a single lint rule or a directory containing multiple lint rules. + If not specified, default lint rules are used. - luaopen_require(L, requireConfigInit, createCliRequireContext(L)); - } - ); -} +General Options: + -h, --help Display this usage message. + --version Show the lute version. +)"; + +static const char* VERSION_STRING = LUTE_VERSION_FULL; + +static const char* RUN_HELP_STRING = R"(Usage: lute run [args...] + +Run Options: + -h, --help Display this usage message. +)"; + +static const char* PKGRUN_HELP_STRING = R"(Usage: lute pkgrun [args...] -bool setupArguments(lua_State* L, int argc, char** argv) +Run Options: + -h, --help Display this usage message. +)"; + +static const char* CHECK_HELP_STRING = R"(Usage: lute check [file2.luau...] + +Check Options: + -h, --help Display this usage message. +)"; + +static const char* COMPILE_HELP_STRING = R"(Usage: lute compile [options] + +Compile Options: + --output Name for the compiled executable. + Defaults to entry file's base name (with .exe on Windows). + --bundle-stats Display bundle size and compression statistics. + --show-require-graph Print the require dependency graph. + -h, --help Display this usage message. +)"; + +static bool setupArguments(lua_State* L, int argc, char** argv) { if (!lua_checkstack(L, argc)) return false; @@ -80,7 +118,15 @@ bool setupArguments(lua_State* L, int argc, char** argv) return true; } -static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL) +bool runBytecode( + Runtime& runtime, + const std::string& bytecode, + const std::string& chunkname, + lua_State* GL, + int program_argc, + char** program_argv, + LuteReporter& reporter +) { // module needs to run in a new thread, isolated from the rest lua_State* L = lua_newthread(GL); @@ -91,9 +137,9 @@ static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std if (luau_load(L, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) != 0) { if (const char* str = lua_tostring(L, -1)) - fprintf(stderr, "%s", str); + reporter.reportError(str); else - fprintf(stderr, "Failed to load bytecode"); + reporter.reportError("Failed to load bytecode"); lua_pop(GL, 1); return false; @@ -107,7 +153,7 @@ static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std if (!setupArguments(L, program_argc, program_argv)) { - fprintf(stderr, "Failed to pass arguments to Luau"); + reporter.reportError("Failed to pass arguments to Luau"); lua_pop(GL, 1); return false; } @@ -120,18 +166,18 @@ static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std return runtime.runToCompletion(); } -static bool runFile(Runtime& runtime, const char* name, lua_State* GL) +static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int program_argc, char** program_argv, LuteReporter& reporter) { if (isDirectory(name)) { - fprintf(stderr, "Error: %s is a directory\n", name); + reporter.formatError("Error: %s is a directory", name); return false; } std::optional source = readFile(name); if (!source) { - fprintf(stderr, "Error opening %s\n", name); + reporter.formatError("Error opening %s", name); return false; } @@ -139,58 +185,7 @@ static bool runFile(Runtime& runtime, const char* name, lua_State* GL) std::string bytecode = Luau::compile(*source, copts()); - return runBytecode(runtime, bytecode, chunkname, GL); -} - -static void displayHelp(const char* argv0) -{ - printf("Usage: lute [options] [arguments...]\n"); - printf("\n"); - printf("Commands:\n"); - printf(" run (default) Run a Luau script.\n"); - printf(" check Type check Luau files.\n"); - printf(" compile Compile a Luau script into the executable.\n"); - printf("\n"); - printf("Run Options (when using 'run' or no command):\n"); - printf(" lute [run] [args...]\n"); - printf(" Executes the script, passing [args...] to it.\n"); - printf("\n"); - printf("Check Options:\n"); - printf(" lute check [file2.luau...]\n"); - printf(" Performs a type check on the specified files.\n"); - printf("\n"); - printf("Compile Options:\n"); - printf(" lute compile [output_executable]\n"); - printf(" Compiles the script, embedding it into a new executable.\n"); - printf("\n"); - printf("General Options:\n"); - printf(" -h, --help Display this usage message.\n"); -} - -static void displayRunHelp(const char* argv0) -{ - printf("Usage: lute run [args...]\n"); - printf("\n"); - printf("Run Options:\n"); - printf(" -h, --help Display this usage message.\n"); -} - -static void displayCheckHelp(const char* argv0) -{ - printf("Usage: lute check [file2.luau...]\n"); - printf("\n"); - printf("Check Options:\n"); - printf(" -h, --help Display this usage message.\n"); -} - -static void displayCompileHelp(const char* argv0) -{ - printf("Usage: lute compile [output_executable]\n"); - printf("\n"); - printf("Compile Options:\n"); - printf(" output_executable Optional name for the compiled executable.\n"); - printf(" Defaults to '_compiled'.\n"); - printf(" -h, --help Display this usage message.\n"); + return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv, reporter); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -199,37 +194,82 @@ static int assertionHandler(const char* expr, const char* file, int line, const return 1; } -static bool checkValidPath(std::filesystem::path& filePath) +// Returns whether the filePath could be resolved to a valid file path, and a string containing either the valid path or an error message +static std::pair getWithRequireByStringSemantics(std::string filePath) { - if (std::filesystem::exists(filePath)) + std::string normalized = normalizePath(std::move(filePath)); + + std::string rootOfPath; + std::string restOfPath = normalized; + if (size_t firstSlash = normalized.find_first_of("\\/"); firstSlash != std::string::npos) { - return true; + rootOfPath = normalized.substr(0, firstSlash); + restOfPath = normalized.substr(firstSlash + 1); } - // if the file has an explicit extension, dont do a fallback - if (filePath.has_extension()) { - return false; - } + std::optional mp = ModulePath::create(std::move(rootOfPath), std::move(restOfPath), isFile, isDirectory); + if (!mp) + return {false, "Could not initialize ModulePath instance."}; - std::filesystem::path fallbackPath = ".lute" / filePath; + ResolvedRealPath resolved = mp->getRealPath(); - for (const auto& ext : {".luau", ".lua"}) + std::pair result; + switch (resolved.status) { - fallbackPath.replace_extension(ext); + case NavigationStatus::Success: + if (resolved.type == ResolvedRealPath::PathType::File) + result = {true, resolved.realPath}; + else + result = {false, "Path is a directory, not a file."}; + break; + case NavigationStatus::Ambiguous: + result = {false, "Unable to tell whether path is a file or directory. Is there a same-named file or directory?"}; + break; + case NavigationStatus::NotFound: + result = {false, "File or directory not found."}; + break; + } - if (std::filesystem::exists(fallbackPath)) + return result; +}; + +// Returns whether the filePath could be resolved to a valid file path, and a string containing either the valid path or an error message +static std::pair getValidPath(std::string filePath) +{ + auto [ok, res] = getWithRequireByStringSemantics(filePath); + if (ok) + return {true, res}; + + // Only fallback to checking .lute/* if the original path has no extension. + if (filePath.find('.') != std::string::npos) + return {false, res}; + + const std::array fallbackPaths = { + joinPaths(".lute", filePath + ".lua"), + joinPaths(".lute", filePath + ".luau"), + joinPaths(joinPaths(".lute", filePath), "init.lua"), + joinPaths(joinPaths(".lute", filePath), "init.luau"), + }; + + for (std::optional currentPath = getCurrentWorkingDirectory(); currentPath; currentPath = getParentPath(*currentPath)) + { + for (const std::string& fallbackPath : fallbackPaths) { - filePath = std::move(fallbackPath); - return true; + const std::string commandPath = joinPaths(*currentPath, fallbackPath); + if (isFile(commandPath)) + return {true, commandPath}; } } - return false; + return {false, res}; } -int handleRunCommand(int argc, char** argv, int argOffset) +int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness, LuteReporter& reporter) { - std::optional filePath; + std::string command = packageAwareness ? "pkgrun" : "run"; + std::string filePath; + int program_argc = 0; + char** program_argv = nullptr; for (int i = argOffset; i < argc; ++i) { @@ -237,45 +277,74 @@ int handleRunCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayRunHelp(argv[0]); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); return 0; } else if (currentArg[0] == '-') { - fprintf(stderr, "Error: Unrecognized option '%s' for 'run' command.\n\n", currentArg); - displayRunHelp(argv[0]); + reporter.formatError("Error: Unrecognized option '%s' for '%s' command.", currentArg, command.c_str()); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); return 1; } else { - filePath.emplace(currentArg); + filePath = currentArg; program_argc = argc - i; program_argv = &argv[i]; break; } } - if (!filePath) + if (filePath.empty()) + { + reporter.formatError("Error: No file specified for '%s' command.", command.c_str()); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); + return 1; + } + + auto [ok, validPath] = getValidPath(filePath); + if (!ok) { - fprintf(stderr, "Error: No file specified for 'run' command.\n\n"); - displayRunHelp(argv[0]); + reporter.formatError("Error while resolving filepath '%s': %s", filePath.c_str(), validPath.c_str()); return 1; } Runtime runtime; - lua_State* L = setupCliState(runtime); + lua_State* L; - if (!checkValidPath(filePath.value())) + if (packageAwareness) { - std::cerr << "Error: File '" << filePath->string() << "' does not exist.\n"; - return 1; + if (!isAbsolutePath(validPath)) + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + { + reporter.reportError("Error: Failed to get current working directory.\n"); + return 1; + } + validPath = normalizePath(joinPaths(*cwd, filePath)); + } + + std::optional lockfile = getAbsolutePathToNearestLockfile(validPath); + if (!lockfile) + { + reporter.formatError("Error: No loom.lock file found for '%s'", validPath.c_str()); + return 1; + } + + auto [directDependencies, allDependencies] = getDependenciesFromLockfile(*lockfile); + L = setupPkgCliState(runtime, std::move(directDependencies), std::move(allDependencies)); + } + else + { + L = setupCliState(runtime); } - bool success = runFile(runtime, filePath->string().c_str(), L); + bool success = runFile(runtime, validPath.c_str(), L, program_argc, program_argv, reporter); return success ? 0 : 1; } -int handleCheckCommand(int argc, char** argv, int argOffset) +int handleCheckCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) { std::vector files; @@ -285,13 +354,13 @@ int handleCheckCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayCheckHelp(argv[0]); + reporter.reportOutput(CHECK_HELP_STRING); return 0; } else if (currentArg[0] == '-') { - fprintf(stderr, "Error: Unrecognized option '%s' for 'check' command.\n\n", currentArg); - displayCheckHelp(argv[0]); + reporter.formatError("Error: Unrecognized option '%s' for 'check' command.", currentArg); + reporter.reportOutput(CHECK_HELP_STRING); return 1; } else @@ -302,105 +371,207 @@ int handleCheckCommand(int argc, char** argv, int argOffset) if (files.empty()) { - fprintf(stderr, "Error: No files specified for 'check' command.\n\n"); - displayCheckHelp(argv[0]); + reporter.reportError("Error: No files specified for 'check' command."); + reporter.reportOutput(CHECK_HELP_STRING); return 1; } - return typecheck(files); + return typecheck(files, reporter); } -int handleCompileCommand(int argc, char** argv, int argOffset) +int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) { - std::string inputFilePath; - std::string outputFilePath; + std::string filePath; + std::string outputPath; + bool bundleStats = false; + bool showRequireGraph = false; + // Parse arguments for (int i = argOffset; i < argc; ++i) { const char* currentArg = argv[i]; if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayCompileHelp(argv[0]); + reporter.reportOutput(COMPILE_HELP_STRING); return 0; } - else if (inputFilePath.empty()) + else if (strcmp(currentArg, "--output") == 0) + { + if (i + 1 >= argc) + { + reporter.reportError("Error: --output requires a path argument."); + reporter.reportOutput(COMPILE_HELP_STRING); + return 1; + } + outputPath = argv[++i]; + } + else if (strcmp(currentArg, "--bundle-stats") == 0) + bundleStats = true; + else if (strcmp(currentArg, "--show-require-graph") == 0) + showRequireGraph = true; + else if (currentArg[0] == '-') { - inputFilePath = currentArg; + reporter.formatError("Error: Unrecognized option '%s' for 'compile' command.", currentArg); + reporter.reportOutput(COMPILE_HELP_STRING); + return 1; } - else if (outputFilePath.empty()) + else if (filePath.empty()) { - outputFilePath = currentArg; + filePath = currentArg; } else { - fprintf(stderr, "Error: Too many arguments for 'compile' command.\n\n"); - displayCompileHelp(argv[0]); + reporter.reportError("Error: Too many arguments for 'compile' command."); + reporter.reportOutput(COMPILE_HELP_STRING); return 1; } } - if (inputFilePath.empty()) + if (filePath.empty()) { - fprintf(stderr, "Error: No input file specified for 'compile' command.\n\n"); - displayCompileHelp(argv[0]); + reporter.reportError("Error: No input file specified for 'compile' command."); + reporter.reportOutput(COMPILE_HELP_STRING); return 1; } - if (outputFilePath.empty()) + std::string absoluteEntryPoint; + if (isAbsolutePath(filePath)) { - std::string inputBase = inputFilePath; - size_t lastSlash = inputBase.find_last_of("/"); - if (lastSlash != std::string::npos) - { - inputBase = inputBase.substr(lastSlash + 1); - } -#ifdef _WIN32 - size_t lastBackslash = inputBase.find_last_of("\\"); - if (lastBackslash != std::string::npos) + absoluteEntryPoint = filePath; + } + else + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) { - inputBase = inputBase.substr(lastBackslash + 1); + reporter.reportError("Error: Failed to get current working directory.\n"); + return 1; } -#endif - size_t lastDot = inputBase.find_last_of('.'); + absoluteEntryPoint = normalizePath(joinPaths(*cwd, filePath)); + } + + // Set default output path if not specified + if (outputPath.empty()) + { + // Extract base name from input file (remove directory and extension) + std::string baseName = filePath; + + // Remove directory path + size_t lastSlash = baseName.find_last_of("/\\"); + if (lastSlash != std::string::npos) + baseName = baseName.substr(lastSlash + 1); + + // Remove extension + size_t lastDot = baseName.find_last_of('.'); if (lastDot != std::string::npos) - { - inputBase = inputBase.substr(0, lastDot); - } - outputFilePath = inputBase; + baseName = baseName.substr(0, lastDot); + + outputPath = baseName; #ifdef _WIN32 - outputFilePath += ".exe"; + outputPath += ".exe"; #endif } - return compileScript(inputFilePath, outputFilePath, argv[0]); + // Perform static require trace + StaticRequireTracer tracer{reporter}; + tracer.trace(absoluteEntryPoint); + + if (showRequireGraph) + tracer.printRequireGraph(); + + // Create payload and add all discovered files + LuteExePayload payload{reporter}; + auto staticRequirePairs = tracer.getStaticRequirePairs(); + // Add file with absolute path for reading and rooted path for bundle + // We don't want to leak your entire directory path in the bundle, so we + // try to pick the lowest common ancestor and keep that as the root. + for (const auto& [bundle, absolute] : staticRequirePairs) + { + payload.add(bundle, absolute); + } + + // Add the discovered luaurc configuration + payload.setLuauConfig(tracer.getLuaurcFiles()); + + + // Encode the payload + reporter.reportOutput("Compiling and bundling bytecode..."); + std::optional encodeResult = payload.encode(); + if (!encodeResult) + { + reporter.reportError("Error: Failed to encode bundle"); + return 1; + } + + // Show bundle stats if requested + if (bundleStats) + { + reporter.reportOutput("\nBundle Statistics:"); + reporter.formatOutput("\tFiles bundled: %zu", staticRequirePairs.size()); + reporter.formatOutput("\tUncompressed size: %zu bytes", encodeResult->uncompressedPayloadSizeBytes); + reporter.formatOutput("\tCompressed size: %zu bytes", encodeResult->compressedPayloadSizeBytes); + reporter.formatOutput( + "\tSpace saved: %.2f%%", + 100.0 * (1.0 - (double)encodeResult->compressedPayloadSizeBytes / (double)encodeResult->uncompressedPayloadSizeBytes) + ); + reporter.formatOutput( + "\tCompression ratio: %.2f:1", (double)encodeResult->uncompressedPayloadSizeBytes / (double)encodeResult->compressedPayloadSizeBytes + ); + reporter.formatOutput("\tTotal payload size: %zu bytes", encodeResult->bytesWritten); + reporter.reportOutput(""); + } + + // Get current executable path + std::string errorMsg; + std::optional exePath = process::getExecPath(&errorMsg); + if (!exePath) + { + reporter.formatError("Error: Failed to get executable path: %s", errorMsg.c_str()); + return 1; + } + + // Create the executable with embedded payload + LuteExecutable executable{*exePath, reporter}; + if (!executable.create(outputPath, payload)) + { + reporter.reportError("Error: Failed to create executable."); + return 1; + } + + reporter.formatOutput("Created executable '%s'", outputPath.c_str()); + return 0; } -int handleCliCommand(CliCommandResult result) +int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, LuteReporter& reporter) { Runtime runtime; lua_State* L = setupCliState(runtime); std::string bytecode = Luau::compile(std::string(result.contents), copts()); - return runBytecode(runtime, bytecode, "@" + result.path, L) ? 0 : 1; + return runBytecode(runtime, bytecode, "@" + result.path, L, program_argc, program_argv, reporter) ? 0 : 1; } -int cliMain(int argc, char** argv) +int cliMain(int argc, char** argv, LuteReporter& reporter) { Luau::assertHandler() = assertionHandler; + setLuauFlags(); + + std::string err = ""; - AppendedBytecodeResult embedded = checkForAppendedBytecode(argv[0]); - if (embedded.found) + LuteExecutable exe{argv[0], reporter}; + if (auto payload = exe.extract()) { Runtime runtime; - lua_State* GL = setupCliState(runtime); - - program_argc = argc; - program_argv = argv; - - bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL); - return success ? 0 : 1; + lua_State* GL = setupBundleState(runtime, payload->luauConfigFiles, payload->filePathToBytecode); + std::string entryPoint = payload->entryPointPath; + auto entryModule = payload->filePathToBytecode.find(entryPoint); + if (entryModule != nullptr) + { + bool success = runBytecode(runtime, *entryModule, "@@bundle/" + entryPoint, GL, argc, argv, reporter); + return success ? 0 : 1; + } } #ifdef _WIN32 @@ -410,7 +581,7 @@ int cliMain(int argc, char** argv) if (argc < 2) { // TODO: REPL? - displayHelp(argv[0]); + reporter.reportOutput(HELP_STRING); return 0; } @@ -419,29 +590,38 @@ int cliMain(int argc, char** argv) if (strcmp(command, "run") == 0) { - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ false, reporter); + } + else if (strcmp(command, "pkgrun") == 0) + { + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ true, reporter); } else if (strcmp(command, "check") == 0) { - return handleCheckCommand(argc, argv, argOffset); + return handleCheckCommand(argc, argv, argOffset, reporter); } else if (strcmp(command, "compile") == 0) { - return handleCompileCommand(argc, argv, argOffset); + return handleCompileCommand(argc, argv, argOffset, reporter); } else if (strcmp(command, "-h") == 0 || strcmp(command, "--help") == 0) { - displayHelp(argv[0]); + reporter.reportOutput(HELP_STRING); + return 0; + } + else if (strcmp(command, "--version") == 0) + { + reporter.reportOutput(VERSION_STRING); return 0; } else if (std::optional result = getCliCommand(command); result) { - return handleCliCommand(*result); + return handleCliCommand(*result, argc - argOffset, &argv[argOffset], reporter); } else { // Default to 'run' command argOffset = 1; - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ false, reporter); } } diff --git a/lute/cli/src/clireporter.cpp b/lute/cli/src/clireporter.cpp new file mode 100644 index 000000000..34d8d00d5 --- /dev/null +++ b/lute/cli/src/clireporter.cpp @@ -0,0 +1,13 @@ +#include "lute/clireporter.h" + +#include + +void CLIReporter::reportError(const std::string& message) +{ + fprintf(stderr, "%s\n", message.c_str()); +} + +void CLIReporter::reportOutput(const std::string& message) +{ + fprintf(stdout, "%s\n", message.c_str()); +} diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index a15bf433a..f957a5991 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -1,121 +1,520 @@ #include "lute/compile.h" #include "lute/options.h" +#include "lute/process.h" + #include "uv.h" +#include "zlib.h" +#include #include +#include +#include + +#ifndef _WIN32 +#include +#endif const char MAGIC_FLAG[] = "LUTEBYTE"; const size_t MAGIC_FLAG_SIZE = sizeof(MAGIC_FLAG) - 1; -const size_t BYTECODE_SIZE_FIELD_SIZE = sizeof(uint64_t); -AppendedBytecodeResult checkForAppendedBytecode(const std::string& executablePath) +LuteExePayload::LuteExePayload(LuteReporter& reporter) + : reporter(reporter) { - AppendedBytecodeResult result; - std::ifstream exeFile(executablePath, std::ios::binary | std::ios::ate); - if (!exeFile) +} + +void LuteExePayload::add(const std::string& bundlePath, const std::string& sourcePath) +{ + // First file added becomes the entry point + if (filePaths.empty()) + entryPointPath = bundlePath; + + // Store the source path for reading files + filePaths.push_back(sourcePath); + + // Map source path to bundle path + sourceToBundlePath[sourcePath] = bundlePath; +} + +void LuteExePayload::setLuauConfig(const Luau::DenseHashMap& configs) +{ + luauConfigFiles = configs; +} + +std::optional LuteExePayload::encode() +{ + // Encoding an empty payload is an error + if (filePaths.empty()) { - return result; + reporter.reportError("Encode failed: No files added to payload"); + return std::nullopt; } - std::streampos fileSize = exeFile.tellg(); - if (fileSize < static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE)) + LuteEncodeResult result; + // Step 1: Build uncompressed bundle + // Format: [num_config_entries][config entries...][file entries...] + std::string uncompressedBundle; + + // Step 1a: Write .luaurc config files first + uint32_t numConfigEntries = static_cast(luauConfigFiles.size()); + uncompressedBundle.append(reinterpret_cast(&numConfigEntries), sizeof(uint32_t)); + + for (const auto& [configPath, configContent] : luauConfigFiles) { - exeFile.close(); - return result; - } + // Append path_length field (uint32_t, 4 bytes) + uint32_t pathLength = static_cast(configPath.size()); + uncompressedBundle.append(reinterpret_cast(&pathLength), sizeof(uint32_t)); + + // Append path_val field (variable length) + uncompressedBundle.append(configPath); - std::vector flagBuffer(MAGIC_FLAG_SIZE); - exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE)); - exeFile.read(flagBuffer.data(), MAGIC_FLAG_SIZE); + // Append luaurc_length field (uint32_t, 4 bytes) + uint32_t luaurcLength = static_cast(configContent.size()); + uncompressedBundle.append(reinterpret_cast(&luaurcLength), sizeof(uint32_t)); + + // Append luaurc_contents field (variable length) + uncompressedBundle.append(configContent); + } - if (memcmp(flagBuffer.data(), MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) + // Step 1b: Write bytecode files + for (const auto& sourcePath : filePaths) { - exeFile.close(); - return result; + // Get the bundle path (rooted path for the bundle) + const std::string* bundlePathPtr = sourceToBundlePath.find(sourcePath); + const std::string& bundlePath = bundlePathPtr ? *bundlePathPtr : sourcePath; + + // Read source file from disk using absolute source path + std::optional source = readFile(sourcePath); + if (!source) + { + reporter.formatError("Encode failed: Could not read file '%s'\n", sourcePath.c_str()); + return std::nullopt; + } + + // Compile Luau source to bytecode + std::string bytecode = Luau::compile(*source, copts()); + if (bytecode.empty()) + { + reporter.formatError("Encode failed: Could not compile file '%s' to bytecode", sourcePath.c_str()); + return std::nullopt; + } + + // Store bytecode with bundle path (rooted path) + filePathToBytecode[bundlePath] = bytecode; + + // Append path_length field (uint32_t, 4 bytes) - use bundle path + uint32_t pathLength = static_cast(bundlePath.size()); + uncompressedBundle.append(reinterpret_cast(&pathLength), sizeof(uint32_t)); + + // Append path_string field (variable length) - use bundle path + uncompressedBundle.append(bundlePath); + + // Append bytecode_size field (uint64_t, 8 bytes) + uint64_t bytecodeSize = bytecode.size(); + uncompressedBundle.append(reinterpret_cast(&bytecodeSize), sizeof(uint64_t)); + + // Append bytecode_data field (variable length) + uncompressedBundle.append(bytecode); } - uint64_t BytecodeSize; - exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE)); - exeFile.read(reinterpret_cast(&BytecodeSize), BYTECODE_SIZE_FIELD_SIZE); + // Step 2: Compress the bundled bytecode using zlib + uLong uncompressedSize = uncompressedBundle.size(); - if (fileSize < static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE + BytecodeSize)) { - fprintf(stderr, "Warning: Found magic flag but file size inconsistent.\n"); - exeFile.close(); - return result; + // Calculate maximum possible compressed size + uLong compressedSize = compressBound(uncompressedSize); + std::vector compressedData(compressedSize); + + // Compress with maximum compression level + int compressResult = compress2( + compressedData.data(), // destination buffer + &compressedSize, // in/out: buffer size / actual compressed size + reinterpret_cast(uncompressedBundle.data()), // source data + uncompressedSize, // source size + Z_BEST_COMPRESSION // compression level (9) + ); + + if (compressResult != Z_OK) + { + reporter.formatError("Encode failed: Compression error (zlib error %d)", compressResult); + return std::nullopt; } + result.payload.clear(); + size_t totalBytes = compressedSize // Size of compressed data + + sizeof(uint64_t) // Length of compressed data (uint64_t) + + sizeof(uint64_t) // Lengths of uncompressed data (uint64_t) + + sizeof(uint32_t) // Number of modules(files) in the bundle (uint32_t) + + entryPointPath.length() // Module entry point path length + + sizeof(uint32_t) // Length of entry point field + + MAGIC_FLAG_SIZE; // LUTEBYTE - the magic flag that tells us to decode the bundled modules + result.payload.reserve(totalBytes); + // Step 3: Append the metadata needed + // Append compressed_data field (variable length, compressedSize bytes) + result.payload.append(reinterpret_cast(compressedData.data()), compressedSize); + + // Append compressed_size field (uint64_t, 8 bytes) + uint64_t compressedSizeField = compressedSize; + result.payload.append(reinterpret_cast(&compressedSizeField), sizeof(uint64_t)); + + // Append uncompressed_size field (uint64_t, 8 bytes) + uint64_t uncompressedSizeField = uncompressedSize; + result.payload.append(reinterpret_cast(&uncompressedSizeField), sizeof(uint64_t)); + + // Append num_files field (uint32_t, 4 bytes) + uint32_t numFiles = static_cast(filePaths.size()); + result.payload.append(reinterpret_cast(&numFiles), sizeof(uint32_t)); + + // Append entry_point_path_string field (variable length) + result.payload.append(entryPointPath); + + // Append entry_point_path_length field (uint32_t, 4 bytes) + uint32_t entryPointPathLength = static_cast(entryPointPath.size()); + result.payload.append(reinterpret_cast(&entryPointPathLength), sizeof(uint32_t)); - result.BytecodeData.resize(BytecodeSize); - exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE + BytecodeSize)); - exeFile.read(&result.BytecodeData[0], BytecodeSize); + // Step 4: Append the LUTE BYTE + result.payload.append(MAGIC_FLAG, MAGIC_FLAG_SIZE); + + result.bytesWritten = result.payload.size(); + result.compressedPayloadSizeBytes = compressedSize; + result.uncompressedPayloadSizeBytes = uncompressedSize; - exeFile.close(); - result.found = true; return result; } -int compileScript(const std::string& inputFilePath, const std::string& outputFilePath, const std::string& currentExecutablePath) +std::optional LuteExePayload::decode(const std::string_view binary, LuteReporter& reporter) { - std::optional source = readFile(inputFilePath); - if (!source) + LuteDecodeResult result{reporter}; + result.payload.filePathToBytecode.clear(); + + // Check minimum size for magic flag + if (binary.size() < MAGIC_FLAG_SIZE + sizeof(uint32_t)) { - fprintf(stderr, "Error opening input file %s\n", inputFilePath.c_str()); - return 1; + reporter.formatError("Decode failed: Binary too small (%zu bytes) to contain valid payload", binary.size()); + return std::nullopt; } - std::string bytecode = Luau::compile(*source, copts()); - if (bytecode.empty()) + // Check for LUTEBYTE magic flag at the end + size_t magicOffset = binary.size() - MAGIC_FLAG_SIZE; + if (memcmp(binary.data() + magicOffset, MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) { - fprintf(stderr, "Error compiling %s to bytecode.\n", inputFilePath.c_str()); - return 1; + reporter.reportError("Decode failed: LUTEBYTE magic flag not found"); + return std::nullopt; } - std::ifstream exeFile(currentExecutablePath, std::ios::binary | std::ios::ate); - if (!exeFile) + // Helper to read fixed-size values backwards + auto readValue = [&binary, &reporter](size_t& pos, const char* fieldName, auto& value) -> bool + { + // We need this because the auto& parameter acts like a generic and we would like to strip out the & here + using T = std::decay_t; + if (pos < sizeof(T)) + { + reporter.formatError("Decode failed: Incomplete %s field", fieldName); + return false; + } + pos -= sizeof(T); + memcpy(&value, binary.data() + pos, sizeof(T)); + return true; + }; + + // Helper to read variable-length bytes backwards + auto readBytes = [&binary, &reporter](size_t& pos, size_t length, const char* fieldName) -> std::optional + { + if (pos < length) + { + reporter.formatError("Decode failed: Incomplete %s field", fieldName); + return std::nullopt; + } + pos -= length; + return std::string(binary.data() + pos, length); + }; + + // Read metadata from LUTEBYTE back to entry_point_path_length: + // [entry_point_path_length][entry_point_path][num_files][uncompressed_size][compressed_size][compressed_data]...[LUTEBYTE] + size_t pos = binary.size() - MAGIC_FLAG_SIZE; + + // Read entry_point_path_length + uint32_t entryPointPathLength; + if (!readValue(pos, "entry_point_path_length", entryPointPathLength)) + return std::nullopt; + + // Read entry_point_path + auto entryPointPath = readBytes(pos, entryPointPathLength, "entry_point_path"); + if (!entryPointPath) + return std::nullopt; + result.payload.entryPointPath = *entryPointPath; + + // Read num_files + uint32_t numFiles; + if (!readValue(pos, "num_files", numFiles)) + return std::nullopt; + + // Read uncompressed_size + uint64_t uncompressedSize; + if (!readValue(pos, "uncompressed_size", uncompressedSize)) + return std::nullopt; + + // Read compressed_size + uint64_t compressedSize; + if (!readValue(pos, "compressed_size", compressedSize)) + return std::nullopt; + + // Read compressed data + if (pos < compressedSize) { - fprintf(stderr, "Error opening current executable %s\n", currentExecutablePath.c_str()); - return 1; + reporter.formatError("Decode failed: Incomplete compressed data (expected %llu bytes)", static_cast(compressedSize)); + return std::nullopt; } - std::streamsize exeSize = exeFile.tellg(); - exeFile.seekg(0, std::ios::beg); - std::vector exeBuffer(exeSize); - if (!exeFile.read(exeBuffer.data(), exeSize)) + pos -= compressedSize; + + // Decompress the bundle + std::vector uncompressedData(uncompressedSize); + uLongf actualUncompressedSize = uncompressedSize; + int zlibResult = + uncompress(uncompressedData.data(), &actualUncompressedSize, reinterpret_cast(binary.data() + pos), compressedSize); + + if (zlibResult != Z_OK) { - fprintf(stderr, "Error reading current executable %s\n", currentExecutablePath.c_str()); - exeFile.close(); - return 1; + reporter.formatError("Decode failed: Decompression error (zlib error %d)", zlibResult); + return std::nullopt; } - exeFile.close(); - std::ofstream outFile(outputFilePath, std::ios::binary | std::ios::trunc); - if (!outFile) { - fprintf(stderr, "Error creating output file %s\n", outputFilePath.c_str()); - return 1; + // Parse the decompressed bundle + std::string_view decompressedBundle(reinterpret_cast(uncompressedData.data()), actualUncompressedSize); + if (!result.payload.parseFromDecompressedBundle(decompressedBundle)) + { + reporter.reportError("Decode failed: Failed to parse decompressed bundle"); + return std::nullopt; } - outFile.write(exeBuffer.data(), exeSize); + // Validate that the number of parsed files matches the metadata + if (result.payload.filePathToBytecode.size() != numFiles) + { + reporter.formatError("Decode failed: Expected %u files but parsed %zu", numFiles, result.payload.filePathToBytecode.size()); + return std::nullopt; + } - uint64_t bytecodeSize = bytecode.size(); - outFile.write(bytecode.data(), bytecodeSize); + // Populate result metrics + result.bytesRead = binary.size(); + result.compressedPayloadSizeBytes = compressedSize; + result.uncompressedPayloadSizeBytes = actualUncompressedSize; + return result; +} - outFile.write(reinterpret_cast(&bytecodeSize), BYTECODE_SIZE_FIELD_SIZE); +bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBundle) +{ + size_t offset = 0; + filePathToBytecode.clear(); + luauConfigFiles.clear(); - outFile.write(MAGIC_FLAG, MAGIC_FLAG_SIZE); + // Step 1: Read num_config_entries + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete num_config_entries field"); + return false; + } + + uint32_t numConfigEntries; + memcpy(&numConfigEntries, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Step 2: Read each config entry + for (uint32_t i = 0; i < numConfigEntries; ++i) + { + // Read path length + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete config path length field"); + return false; + } + + uint32_t pathLength; + memcpy(&pathLength, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Read path string + if (offset + pathLength > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete config path string"); + return false; + } + + std::string configPath(decompressedBundle.data() + offset, pathLength); + offset += pathLength; + + // Read luaurc length + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete luaurc length field"); + return false; + } + + uint32_t luaurcLength; + memcpy(&luaurcLength, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Read luaurc content + if (offset + luaurcLength > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete luaurc content"); + return false; + } + + std::string luaurcContent(decompressedBundle.data() + offset, luaurcLength); + offset += luaurcLength; + + // Store in map + luauConfigFiles[configPath] = luaurcContent; + } + + // Step 3: Read bytecode compiled scripts + while (offset < decompressedBundle.size()) + { + // Read path length + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete path length field"); + return false; + } + + uint32_t pathLength; + memcpy(&pathLength, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Read path string + if (offset + pathLength > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete path string"); + return false; + } + + std::string filePath(decompressedBundle.data() + offset, pathLength); + offset += pathLength; + + // Read bytecode size + if (offset + sizeof(uint64_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete bytecode size field"); + return false; + } + + uint64_t bytecodeSize; + memcpy(&bytecodeSize, decompressedBundle.data() + offset, sizeof(uint64_t)); + offset += sizeof(uint64_t); + + // Read bytecode + if (offset + bytecodeSize > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete bytecode data"); + return false; + } + + std::string bytecode(decompressedBundle.data() + offset, bytecodeSize); + offset += bytecodeSize; + + // Store in map + filePathToBytecode[filePath] = bytecode; + } + + return true; +} + +LuteDecodeResult::LuteDecodeResult(LuteReporter& reporter) + : payload(reporter) +{ +} - if (!outFile.good()) +LuteExecutable::LuteExecutable(const std::string& luteRuntimePath, LuteReporter& reporter) + : executablePath(luteRuntimePath) + , reporter(reporter) +{ +} + +bool LuteExecutable::create(const std::string& outputPath, LuteExePayload& payload) +{ + // Read the current executable (lute runtime) + std::ifstream sourceExe(executablePath, std::ios::binary | std::ios::ate); + if (!sourceExe) { - fprintf(stderr, "Error writing to output file %s\n", outputFilePath.c_str()); - outFile.close(); - remove(outputFilePath.c_str()); - return 1; + reporter.formatError("Error: Failed to read executable '%s'", executablePath.c_str()); + return false; } - outFile.close(); + std::streampos exeSize = sourceExe.tellg(); + if (exeSize < 0) + return false; - printf("Successfully compiled %s to %s\n", inputFilePath.c_str(), outputFilePath.c_str()); + sourceExe.seekg(0, std::ios::beg); + std::vector exeData(exeSize); + if (!sourceExe.read(exeData.data(), exeSize)) + return false; + + // Encode the payload + std::optional encodeResult = payload.encode(); + + if (!encodeResult) + { + reporter.reportError("Error: Failed to encode payload"); + return false; + } + + // Write output file: executable + payload + std::ofstream outputFile(outputPath, std::ios::binary); + if (!outputFile) + { + reporter.formatError("Error: Failed to create output file '%s'", outputPath.c_str()); + return false; + } + + // Write original executable + outputFile.write(exeData.data(), exeData.size()); + + // Write encoded payload + outputFile.write(encodeResult->payload.data(), encodeResult->payload.size()); + + // Set executable permissions (using libuv's permission constants) #ifndef _WIN32 - chmod(outputFilePath.c_str(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); + chmod(outputPath.c_str(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); #endif - return 0; + return true; +} + +std::optional LuteExecutable::extract() +{ + if (isDirectory(executablePath)) + return std::nullopt; + + std::ifstream exeFile(executablePath, std::ios::binary | std::ios::ate); + + if (!exeFile) + return std::nullopt; + + // Get file size + std::streampos fileSize = exeFile.tellg(); + if (fileSize < 0) + return std::nullopt; + + // Early check: validate LUTEBYTE magic flag at end before reading entire file + if (fileSize < static_cast(MAGIC_FLAG_SIZE)) + return std::nullopt; + + exeFile.seekg(-static_cast(MAGIC_FLAG_SIZE), std::ios::end); + char magicBuffer[MAGIC_FLAG_SIZE]; + if (!exeFile.read(magicBuffer, MAGIC_FLAG_SIZE)) + return std::nullopt; + + if (memcmp(magicBuffer, MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) + return std::nullopt; + + // Magic flag found, now read entire file + exeFile.seekg(0, std::ios::beg); + std::vector fileData(fileSize); + if (!exeFile.read(fileData.data(), fileSize)) + return std::nullopt; + + // Decode the payload + std::optional decodedPayload = LuteExePayload::decode(std::string_view(fileData.data(), fileData.size()), reporter); + if (!decodedPayload) + return std::nullopt; + + return decodedPayload->payload; } diff --git a/lute/cli/src/coverage.cpp b/lute/cli/src/coverage.cpp new file mode 100644 index 000000000..af6d3b062 --- /dev/null +++ b/lute/cli/src/coverage.cpp @@ -0,0 +1,87 @@ +#include "lute/coverage.h" + +#include "lua.h" + +#include +#include + +struct Coverage +{ + lua_State* L = nullptr; + std::vector functions; +} gCoverage; + +void coverageInit(lua_State* L) +{ + gCoverage.L = lua_mainthread(L); +} + +bool coverageActive() +{ + return gCoverage.L != nullptr; +} + +void coverageTrack(lua_State* L, int funcindex) +{ + int ref = lua_ref(L, funcindex); + gCoverage.functions.push_back(ref); +} + +static void coverageCallback(void* context, const char* function, int linedefined, int depth, const int* hits, size_t size) +{ + FILE* f = static_cast(context); + + std::string name; + + if (depth == 0) + name = "
"; + else if (function) + name = std::string(function) + ":" + std::to_string(linedefined); + else + name = ":" + std::to_string(linedefined); + + fprintf(f, "FN:%d,%s\n", linedefined, name.c_str()); + + for (size_t i = 0; i < size; ++i) + if (hits[i] != -1) + { + fprintf(f, "FNDA:%d,%s\n", hits[i], name.c_str()); + break; + } + + for (size_t i = 0; i < size; ++i) + if (hits[i] != -1) + fprintf(f, "DA:%d,%d\n", int(i), hits[i]); +} + +void coverageDump(const char* path) +{ + lua_State* L = gCoverage.L; + + FILE* f = fopen(path, "w"); + if (!f) + { + fprintf(stderr, "Error opening coverage %s\n", path); + return; + } + + fprintf(f, "TN:\n"); + + for (int fref : gCoverage.functions) + { + lua_getref(L, fref); + + lua_Debug ar = {}; + lua_getinfo(L, -1, "s", &ar); + + fprintf(f, "SF:%s\n", ar.short_src); + lua_getcoverage(L, -1, f, coverageCallback); + fprintf(f, "end_of_record\n"); + + lua_pop(L, 1); + } + + fclose(f); + + printf("Coverage dump written to %s (%d functions)\n", path, int(gCoverage.functions.size())); +} diff --git a/lute/cli/src/fileutils.cpp b/lute/cli/src/fileutils.cpp new file mode 100644 index 000000000..4be9a4585 --- /dev/null +++ b/lute/cli/src/fileutils.cpp @@ -0,0 +1,147 @@ +// This file is part of the Lute programming language and is licensed under MIT License +#include "lute/fileutils.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include + +#include +#endif + +#include "Luau/FileUtils.h" + +#include + +namespace Lute +{ + +#ifdef _WIN32 +static std::wstring fromUtf8(const std::string& path) +{ + if (path.empty()) + return std::wstring(); + + size_t result = MultiByteToWideChar(CP_UTF8, 0, path.data(), int(path.size()), nullptr, 0); + if (result == 0) + return std::wstring(); + + std::wstring buf(result, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, path.data(), int(path.size()), &buf[0], int(buf.size())); + + return buf; +} +#endif + +FILE* openFile(const std::string& path, const char* mode) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + std::wstring wmode = fromUtf8(mode); + if (wpath.empty() || wmode.empty()) + return nullptr; + return _wfopen(wpath.c_str(), wmode.c_str()); +#else + return fopen(path.c_str(), mode); +#endif +} + +bool writeFile(const std::string& path, const std::string& content) +{ + FILE* file = openFile(path, "wb"); + if (!file) + return false; + + bool success = fwrite(content.data(), 1, content.size(), file) == content.size(); + fclose(file); + return success; +} + +bool createDirectory(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + return _wmkdir(wpath.c_str()) == 0 || errno == EEXIST; +#else + return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST; +#endif +} + +bool createDirectories(const std::string& path) +{ + if (path.empty()) + return false; + + std::vector components = splitPath(path); + std::string currentPath; + size_t offset = 0; + + // Handle absolute paths - extract the root/prefix + if (isAbsolutePath(path)) + { +#ifdef _WIN32 + // Check if path starts with drive letter (e.g., "C:") + if (path.size() >= 2 && path[1] == ':') + { + currentPath = path.substr(0, 2); + offset = 1; // Skip the drive letter component in the loop + } + else if (path.size() >= 1 && (path[0] == '/' || path[0] == '\\')) + { + currentPath = path.substr(0, 1); + } +#else + currentPath = "/"; +#endif + } + + for (size_t i = offset; i < components.size(); ++i) + { + const std::string_view component = components[i]; + currentPath = joinPaths(currentPath, component); + + if (!createDirectory(currentPath)) + { + // Check if the directory already exists + if (errno != EEXIST) + return false; + } + } + + return true; +} + +bool removeFile(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + return _wunlink(wpath.c_str()) == 0; +#else + return unlink(path.c_str()) == 0; +#endif +} + +bool removeDirectory(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + return _wrmdir(wpath.c_str()) == 0; +#else + return rmdir(path.c_str()) == 0; +#endif +} + +} // namespace Lute diff --git a/lute/cli/src/luauflags.cpp b/lute/cli/src/luauflags.cpp new file mode 100644 index 000000000..a054d3689 --- /dev/null +++ b/lute/cli/src/luauflags.cpp @@ -0,0 +1,38 @@ +#include "lute/luauflags.h" + +#include "Luau/Common.h" +#include "Luau/ExperimentalFlags.h" + +#include +#include + +static void enableAllLuauFlags() +{ + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + if (strncmp(flag->name, "Luau", 4) == 0 && !Luau::isAnalysisFlagExperimental(flag->name)) + flag->value = true; + } +} + +[[maybe_unused]] static void setLuauFlag(std::string_view name, bool state) +{ + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + if (name == flag->name) + { + flag->value = state; + return; + } + } + + throw std::runtime_error("Unrecognized Luau flag"); +} + +void setLuauFlags() +{ + enableAllLuauFlags(); + + // Individual flags can be overridden here as needed, e.g.: + // setLuauFlag("LuauSomeFlagThatCausedARegression", false); +} diff --git a/lute/cli/src/main.cpp b/lute/cli/src/main.cpp index 6453368a3..b9c0dfe71 100644 --- a/lute/cli/src/main.cpp +++ b/lute/cli/src/main.cpp @@ -1,6 +1,10 @@ #include "lute/climain.h" +#include "lute/clireporter.h" +#include "lute/uvstate.h" int main(int argc, char** argv) { - return cliMain(argc, argv); + UvGlobalState uvState(argc, argv); + CLIReporter reporter; + return cliMain(argc, argv, reporter); } diff --git a/lute/cli/src/packagerun.cpp b/lute/cli/src/packagerun.cpp new file mode 100644 index 000000000..f64614eee --- /dev/null +++ b/lute/cli/src/packagerun.cpp @@ -0,0 +1,158 @@ +#include "lute/packagerun.h" + +#include "lute/userlandvfs.h" + +#include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" + +#include +#include +#include +#include +#include + +std::optional getAbsolutePathToNearestLockfile(std::string entryFile) +{ + if (!isAbsolutePath(entryFile)) + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + return std::nullopt; + + entryFile = joinPaths(*cwd, entryFile); + } + entryFile = normalizePath(entryFile); + + std::optional currentPath = getParentPath(entryFile); + while (currentPath) + { + std::string lockfilePath = joinPaths(*currentPath, "loom.lock.luau"); + if (isFile(lockfilePath)) + return lockfilePath; + + currentPath = getParentPath(*currentPath); + } + + return std::nullopt; +} + +static std::string toLower(std::string_view str) +{ + std::string result(str); + std::transform( + result.begin(), + result.end(), + result.begin(), + [](unsigned char c) + { + return std::tolower(c); + } + ); + return result; +} + +static std::vector extractIdentifiers(std::string lockfileContents) +{ + // TODO: support timing out Luau-syntax configurations + std::optional configOpt = Luau::extractConfig(lockfileContents, {}); + if (!configOpt) + return {}; + + Luau::ConfigTable config = std::move(*configOpt); + if (!config.contains("package")) + return {}; + + Luau::ConfigTable* packageTable = config["package"].get_if(); + if (!packageTable) + return {}; + + std::vector packages; + packages.resize(packageTable->size()); + + for (const auto& [k, v] : *packageTable) + { + const double* key = k.get_if(); + if (!key) + return {}; + + const size_t index = static_cast(*key); + if (index < 1 || packageTable->size() < index) + return {}; + + const Luau::ConfigTable* package = v.get_if(); + if (!package) + return {}; + + if (!package->contains("name") || !package->contains("rev")) + return {}; + + const std::string* name = (*package).find("name")->get_if(); + const std::string* rev = (*package).find("rev")->get_if(); + + if (!name || !rev) + return {}; + + Package::Identifier entry{}; + entry.name = toLower(*name); + entry.version = *rev; + packages[index - 1] = std::move(entry); + } + + return packages; +} + +// TODO: lockfile must specify entry file location; for now, we try out a few +// likely candidates. +static std::string getEntryPoint(const std::string& packageRoot) +{ + const std::vector candidateEntryPoints = { + "src/init.luau", + "src/init.lua", + "init.luau", + "init.lua", + }; + + for (const std::string& candidate : candidateEntryPoints) + { + std::string candidatePath = joinPaths(packageRoot, candidate); + if (isFile(candidatePath)) + return candidatePath; + } + + // Fallback to a default even if it doesn't exist + return joinPaths(packageRoot, "src/init.luau"); +} + +std::pair, std::vector>> getDependenciesFromLockfile( + const std::string& lockfilePath +) +{ + LUAU_ASSERT(isFile(lockfilePath)); + + std::optional contents = readFile(lockfilePath); + if (!contents) + return {}; + + std::optional lockfileParentDir = getParentPath(lockfilePath); + if (!lockfileParentDir) + return {}; + + std::string packagesPath = joinPaths(std::move(*lockfileParentDir), "Packages"); + + std::vector directDependencies = extractIdentifiers(*contents); + std::vector> allDependencies; + for (const Package::Identifier& identifier : directDependencies) + { + Package::Info info; + info.rootDirectory = joinPaths(packagesPath, identifier.name); + info.entryFile = getEntryPoint(info.rootDirectory); + + // TODO: lockfile must specify transitive dependency structure; we + // currently make all dependencies available to each other. + info.dependencies = directDependencies; + + allDependencies.emplace_back(identifier, std::move(info)); + } + + return {std::move(directDependencies), std::move(allDependencies)}; +} diff --git a/lute/cli/src/profiler.cpp b/lute/cli/src/profiler.cpp new file mode 100644 index 000000000..028d9eb81 --- /dev/null +++ b/lute/cli/src/profiler.cpp @@ -0,0 +1,163 @@ +#include "lute/profiler.h" + +#include "lua.h" + +#include "Luau/DenseHash.h" + +#include +#include +#include + +struct Profiler +{ + // static state + lua_Callbacks* callbacks = nullptr; + int frequency = 1000; + std::thread thread; + + // variables for communication between loop and trigger + std::atomic exit = false; + std::atomic ticks = 0; + std::atomic samples = 0; + + // private state for trigger + uint64_t currentTicks = 0; + std::string stackScratch; + + // statistics, updated by trigger + Luau::DenseHashMap data{""}; + uint64_t gc[16] = {}; +} gProfiler; + +static void profilerTrigger(lua_State* L, int gc) +{ + uint64_t currentTicks = gProfiler.ticks.load(); + uint64_t elapsedTicks = currentTicks - gProfiler.currentTicks; + + if (elapsedTicks) + { + std::string& stack = gProfiler.stackScratch; + + stack.clear(); + + if (gc > 0) + stack += "GC,GC,"; + + lua_Debug ar; + for (int level = 0; lua_getinfo(L, level, "sn", &ar); ++level) + { + if (!stack.empty()) + stack += ';'; + + stack += ar.short_src; + stack += ','; + if (ar.name) + stack += ar.name; + stack += ','; + if (ar.linedefined > 0) + stack += std::to_string(ar.linedefined); + } + + if (!stack.empty()) + { + gProfiler.data[stack] += elapsedTicks; + } + + if (gc > 0) + { + gProfiler.gc[gc] += elapsedTicks; + } + } + + gProfiler.currentTicks = currentTicks; + gProfiler.callbacks->interrupt = nullptr; +} + +static void profilerLoop() +{ + double last = lua_clock(); + + while (!gProfiler.exit) + { + double now = lua_clock(); + + if (now - last >= 1.0 / double(gProfiler.frequency)) + { + int64_t ticks = int64_t((now - last) * 1e6); + + gProfiler.ticks += ticks; + gProfiler.samples++; + gProfiler.callbacks->interrupt = profilerTrigger; + + last += ticks * 1e-6; + } + else + { + std::this_thread::yield(); + } + } +} + +void profilerStart(lua_State* L, int frequency) +{ + gProfiler.frequency = frequency; + gProfiler.callbacks = lua_callbacks(L); + + gProfiler.exit = false; + gProfiler.thread = std::thread(profilerLoop); +} + +void profilerStop() +{ + gProfiler.exit = true; + gProfiler.thread.join(); +} + +void profilerDump(const char* path) +{ + FILE* f = fopen(path, "wb"); + if (!f) + { + fprintf(stderr, "Error opening profile %s\n", path); + return; + } + + uint64_t total = 0; + + for (auto& p : gProfiler.data) + { + fprintf(f, "%lld %s\n", static_cast(p.second), p.first.c_str()); + total += p.second; + } + + fclose(f); + + printf( + "Profiler dump written to %s (total runtime %.3f seconds, %lld samples, %lld stacks)\n", + path, + double(total) / 1e6, + static_cast(gProfiler.samples.load()), + static_cast(gProfiler.data.size()) + ); + + uint64_t totalgc = 0; + for (uint64_t p : gProfiler.gc) + totalgc += p; + + if (totalgc) + { + printf("GC: %.3f seconds (%.2f%%)", double(totalgc) / 1e6, double(totalgc) / double(total) * 100); + + for (size_t i = 0; i < std::size(gProfiler.gc); ++i) + { + extern const char* luaC_statename(int state); + + uint64_t p = gProfiler.gc[i]; + + if (p) + printf(", %s %.2f%%", luaC_statename(int(i)), double(p) / double(totalgc) * 100); + } + + printf("\n"); + } +} diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp new file mode 100644 index 000000000..54e59283f --- /dev/null +++ b/lute/cli/src/requiresetup.cpp @@ -0,0 +1,153 @@ +#include "lute/requiresetup.h" + +#include "lute/bundlevfs.h" +#include "lute/clivfs.h" +#include "lute/packagerequirevfs.h" +#include "lute/require.h" +#include "lute/requirevfs.h" +#include "lute/runtime.h" +#include "lute/userlandvfs.h" + +#include "Luau/CodeGen.h" +#include "Luau/Require.h" + +#include "lualib.h" + +#include +#include +#include + +static void* createCliRequireContext(lua_State* L) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + + ctx = new (ctx) RequireCtx{std::make_unique(CliVfs{})}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + +static void* createPkgRequireContext( + lua_State* L, + std::vector directDependencies, + std::vector> allDependencies +) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + + Package::UserlandVfs userlandVfs = Package::UserlandVfs::create(std::move(directDependencies), std::move(allDependencies)); + ctx = new (ctx) RequireCtx{std::make_unique(std::move(userlandVfs))}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + +static void* createBundleRequireContext( + lua_State* L, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + ctx = new (ctx) RequireCtx{std::make_unique(BundleVfs{std::move(luaurcFiles), std::move(bundleMap)})}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + +lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) +{ + return setupState( + runtime, + [preSandboxInit = std::move(preSandboxInit)](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createCliRequireContext(L)); + if (preSandboxInit) + preSandboxInit(L); + } + ); +} + +lua_State* setupPkgCliState( + Runtime& runtime, + std::vector directDependencies, + std::vector> allDependencies +) +{ + return setupState( + runtime, + [directDependencies = std::move(directDependencies), allDependencies = std::move(allDependencies)](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createPkgRequireContext(L, std::move(directDependencies), std::move(allDependencies))); + } + ); +} + +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +) +{ + return setupState( + runtime, + [luaurcFiles = std::move(luaurcFiles), bundleMap = std::move(bundleMap)](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(luaurcFiles), std::move(bundleMap))); + } + ); +} diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp new file mode 100644 index 000000000..0a43f4941 --- /dev/null +++ b/lute/cli/src/staticrequires.cpp @@ -0,0 +1,331 @@ +#include "lute/staticrequires.h" + +#include "lute/modulepath.h" +#include "lute/resolverequire.h" +#include "lute/staticrequires.h" + +#include "Luau/Ast.h" +#include "Luau/Config.h" +#include "Luau/FileUtils.h" +#include "Luau/Parser.h" +#include "Luau/VecDeque.h" + +#include +#include + +// AST visitor to extract require() calls +class RequireExtractor : public Luau::AstVisitor +{ +public: + std::vector requirePaths; + + bool visit(Luau::AstExprCall* call) override + { + if (auto global = call->func->as()) + { + if (global->name == "require" && call->args.size > 0) + { + if (auto str = call->args.data[0]->as()) + { + requirePaths.emplace_back(str->value.data, str->value.size); + } + } + } + return true; + } +}; + +StaticRequireTracer::StaticRequireTracer(LuteReporter& reporter) + : reporter(reporter) +{ +} + +void StaticRequireTracer::trace(const std::string& entryPoint) +{ + visited.clear(); + discovered.clear(); + requireGraph.clear(); + luaurcFiles.clear(); + + if (!isAbsolutePath(entryPoint)) + { + fprintf(stderr, "Error: %s isn't an absolute path\n", entryPoint.c_str()); + return; + } + + // Calculate the entry point directory - we should not look for .luaurc files beyond this directory + std::string entryPointDir = entryPoint; + size_t lastSlash = entryPointDir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + entryPointDir = entryPointDir.substr(0, lastSlash); + } + else + { + entryPointDir = ""; + } + + // Temporary set to collect absolute paths to .luaurc files + Luau::DenseHashSet luaurcAbsolutePaths{""}; + + Luau::VecDeque toProcess; + toProcess.push_back(entryPoint); + + while (!toProcess.empty()) + { + std::string filePath = toProcess.front(); + toProcess.pop_front(); + + // Skip if already visited (handles circular dependencies) + if (visited.contains(filePath)) + continue; + + visited.insert(filePath); + std::optional source = readFile(filePath); + if (!source) + { + reporter.formatError("Warning: Could not read file '%s'\n", filePath.c_str()); + continue; + } + + // Add to discovered list (use the relative path from rootDirectory) + discovered.push_back(filePath); + + // Discover .luaurc files in this file's directory tree + std::string dir = filePath; + size_t lastSlash = dir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + dir = dir.substr(0, lastSlash); + + // Walk up the directory tree looking for .luaurc files, but stop at the entry point directory + while (!dir.empty()) + { + std::string luaurcPath = dir + "/" + Luau::kConfigName; + if (isFile(luaurcPath)) + { + luaurcAbsolutePaths.insert(luaurcPath); + break; + } + + // Stop if we've reached the entry point directory + if (dir == entryPointDir) + break; + + // Move to parent directory + size_t parentSlash = dir.find_last_of("/\\"); + if (parentSlash == std::string::npos || parentSlash == 0) + break; + dir = dir.substr(0, parentSlash); + } + } + + std::vector requiresInFile = extractRequires(*source); + + std::vector resolvedDeps; + + for (const auto& req : requiresInFile) + { + // Skip warning for built-in libraries (@std and @lute) + // The new and improved requireResolver is really good - it'll even handle std/lute aliases that we've built in. + // For now, we can just explicitly skip these, since they are provided by the runtime + if (req.find("@std/") == 0 || req.find("@lute/") == 0) + continue; + std::string err = ""; + std::optional resolvedPath = ::resolveRequire(req, "@" + filePath, &err); + + if (resolvedPath) + { + toProcess.push_back(*resolvedPath); + resolvedDeps.push_back(*resolvedPath); + } + else + { + // Skip warning for built-in libraries (@std and @lute) + bool isBuiltinLibrary = req.rfind("@std/", 0) == 0 || req.rfind("@lute/", 0) == 0; + if (!isBuiltinLibrary) + reporter.formatError("Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); + if (!err.empty()) + reporter.formatError("Warning: Could not resolve require('%s') from '%s':\n\t%s\n", req.c_str(), filePath.c_str(), err.c_str()); + } + } + + // Store the resolved dependencies in the graph + requireGraph[filePath] = std::move(resolvedDeps); + } + + lowestCommonRoot = findLowestCommonRoot(discovered); + + // Convert absolute .luaurc paths to LCR-relative .luaurc paths and read their content + size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; // +1 for the trailing slash + + for (const auto& absolutePath : luaurcAbsolutePaths) + { + // Get the directory containing the .luaurc file and append .luaurc + std::string absoluteDir = absolutePath; + size_t lastSlash = absoluteDir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + absoluteDir = absoluteDir.substr(0, lastSlash); + } + + // Convert to relative path and append .luaurc + std::string relativeLuaurc = ".luaurc"; + if (commonRootLen > 0 && absoluteDir.length() > commonRootLen) + { + std::string relativeDir = absoluteDir.substr(commonRootLen); + relativeLuaurc = relativeDir + "/.luaurc"; + } + + // Read the .luaurc file content + std::optional content = readFile(absolutePath); + if (content) + { + // Store using the .luaurc file path (e.g., "dir/.luaurc" or just ".luaurc") + luaurcFiles[relativeLuaurc] = *content; + } + else + { + reporter.formatError("Warning: Could not read .luaurc file '%s'\n", absolutePath.c_str()); + } + } +} + +std::vector StaticRequireTracer::extractRequires(const std::string& source) +{ + Luau::Allocator allocator; + Luau::AstNameTable names(allocator); + + Luau::ParseOptions options; + Luau::ParseResult result = Luau::Parser::parse(source.c_str(), source.size(), names, allocator, options); + + if (result.errors.size() > 0) + { + // Even with parse errors, we can still try to extract requires + // Parse errors might be from type errors or other issues that don't prevent require extraction + // Should we do something here, or should it be best effort? + } + + RequireExtractor extractor; + result.root->visit(&extractor); + + return extractor.requirePaths; +} + +std::vector> StaticRequireTracer::getStaticRequirePairs() const +{ + std::vector> pairs; + pairs.reserve(discovered.size()); + + size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; // +1 for the trailing slash + + for (const auto& absolutePath : discovered) + { + std::string bundlePath; + if (commonRootLen > 0 && absolutePath.length() > commonRootLen) + bundlePath = absolutePath.substr(commonRootLen); + else + bundlePath = absolutePath; + + pairs.emplace_back(bundlePath, absolutePath); + } + + return pairs; +} + +bool StaticRequireTracer::containsAbsolute(const std::string& absolutePath) const +{ + return visited.contains(absolutePath); +} + +void StaticRequireTracer::printRequireGraph() const +{ + reporter.reportOutput("\nRequire dependency graph:"); + + size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; + + for (const auto& [file, deps] : requireGraph) + { + // Convert absolute path to bundle path for display + std::string displayFile; + if (commonRootLen > 0 && file.length() > commonRootLen) + displayFile = file.substr(commonRootLen); + else + displayFile = file; + + reporter.formatOutput("\t%s", displayFile.c_str()); + for (const auto& dep : deps) + { + // Convert absolute path to bundle path for display + std::string displayDep; + if (commonRootLen > 0 && dep.length() > commonRootLen) + displayDep = dep.substr(commonRootLen); + else + displayDep = dep; + + reporter.formatOutput("\t\t -> %s", displayDep.c_str()); + } + if (deps.empty()) + { + reporter.reportOutput("\t\t(no dependencies)"); + } + } + + // Print luaurc files found + if (!luaurcFiles.empty()) + { + reporter.reportOutput("\n.luaurc files found:"); + for (const auto& [configDir, content] : luaurcFiles) + { + reporter.formatOutput("\t%s", configDir.c_str()); + } + } + else + { + reporter.reportOutput("\nNo .luaurc files found"); + } + + reporter.reportOutput(""); +} + +std::string StaticRequireTracer::findLowestCommonRoot(const std::vector& paths) +{ + if (paths.empty()) + return ""; + + if (paths.size() == 1) + { + // For a single file, return its directory + size_t lastSlash = paths[0].find_last_of("/\\"); + if (lastSlash != std::string::npos) + return paths[0].substr(0, lastSlash); + return ""; + } + + // Sort the paths - the first and last will have the maximum difference + std::vector sortedPaths = paths; + std::sort(sortedPaths.begin(), sortedPaths.end()); + + const std::string& first = sortedPaths.front(); + const std::string& last = sortedPaths.back(); + + // Find common prefix between first and last + size_t i = 0; + while (i < first.length() && i < last.length() && first[i] == last[i]) + { + i++; + } + + // Back up to the last directory separator + std::string commonPrefix = first.substr(0, i); + size_t lastSlash = commonPrefix.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + // Special case: if the slash is at position 0, we're at the root directory + if (lastSlash == 0) + return "/"; + return commonPrefix.substr(0, lastSlash); + } + + return ""; +} diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 5d6c8b214..002282fba 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -1,36 +1,14 @@ #include "lute/tc.h" +#include "lute/configresolver.h" +#include "lute/moduleresolver.h" + #include "Luau/BuiltinDefinitions.h" #include "Luau/Error.h" -#include "Luau/Transpiler.h" -#include "Luau/TypeAttach.h" - -static const std::string kLuteDefinitions = R"LUTE_TYPES( --- Net api -declare net: { - get: (string) -> string, - getAsync: (string) -> string, -} --- fs api -declare class file end -declare fs: { - -- probably not the correct sig - open: (string, "r" | "w" | "a" | "r+" | "w+") -> file, - close: (file) -> (), - read: (file) -> string, - write: (file, string) -> (), - readfiletostring : (string) -> string, - writestringtofile : (string, string) -> (), - -- is this right? I feel like we want a promise type here - readasync : (string) -> string, -} - --- globals -declare function spawn(path: string): any +#include "Luau/FileUtils.h" +#include "Luau/Frontend.h" -)LUTE_TYPES"; - -struct LuteFileResolver : Luau::FileResolver +struct LuteFileResolver : Luau::LuteModuleResolver { std::optional readSource(const Luau::ModuleName& name) override { @@ -55,126 +33,68 @@ struct LuteFileResolver : Luau::FileResolver return Luau::SourceCode{*source, sourceType}; } - std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) override - { - // TODO: Need to handle requires - return std::nullopt; - } - std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override { if (name == "-") return "stdin"; return name; } - -private: - // TODO: add require resolver; -}; - -struct LuteConfigResolver : Luau::ConfigResolver -{ - Luau::Config defaultConfig; - - mutable std::unordered_map configCache; - mutable std::vector> configErrors; - - LuteConfigResolver(Luau::Mode mode) - { - defaultConfig.mode = mode; - } - - const Luau::Config& getConfig(const Luau::ModuleName& name) const override - { - std::optional path = getParentPath(name); - if (!path) - return defaultConfig; - - return readConfigRec(*path); - } - - const Luau::Config& readConfigRec(const std::string& path) const - { - auto it = configCache.find(path); - if (it != configCache.end()) - return it->second; - - std::optional parent = getParentPath(path); - Luau::Config result = parent ? readConfigRec(*parent) : defaultConfig; - - std::string configPath = joinPaths(path, Luau::kConfigName); - - if (std::optional contents = readFile(configPath)) - { - Luau::ConfigOptions::AliasOptions aliasOpts; - aliasOpts.configLocation = configPath; - aliasOpts.overwriteAliases = true; - - Luau::ConfigOptions opts; - opts.aliasOptions = std::move(aliasOpts); - - std::optional error = Luau::parseConfig(*contents, result, opts); - if (error) - configErrors.push_back({configPath, *error}); - } - - return configCache[path] = result; - } }; -static void report(const char* name, const Luau::Location& loc, const char* type, const char* message) +static void report(const char* name, const Luau::Location& loc, const char* type, const char* message, LuteReporter& reporter) { // fprintf(stderr, "%s(%d,%d): %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, type, message); int columnEnd = (loc.begin.line == loc.end.line) ? loc.end.column : 100; // Use stdout to match luacheck behavior - fprintf(stdout, "%s:%d:%d-%d: (W0) %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, columnEnd, type, message); + reporter.formatOutput("%s:%d:%d-%d: (W0) %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, columnEnd, type, message); } -static void reportError(const Luau::Frontend& frontend, const Luau::TypeError& error) +static void reportError(const Luau::Frontend& frontend, const Luau::TypeError& error, LuteReporter& reporter) { std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(error.moduleName); if (const Luau::SyntaxError* syntaxError = Luau::get_if(&error.data)) - report(humanReadableName.c_str(), error.location, "SyntaxError", syntaxError->message.c_str()); + report(humanReadableName.c_str(), error.location, "SyntaxError", syntaxError->message.c_str(), reporter); else report( humanReadableName.c_str(), error.location, "TypeError", - Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str() + Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str(), + reporter ); } -static void reportWarning(const char* name, const Luau::LintWarning& warning) +static void reportWarning(const char* name, const Luau::LintWarning& warning, LuteReporter& reporter) { - report(name, warning.location, Luau::LintWarning::getName(warning.code), warning.text.c_str()); + report(name, warning.location, Luau::LintWarning::getName(warning.code), warning.text.c_str(), reporter); } -static bool reportModuleResult(Luau::Frontend& frontend, const Luau::ModuleName& name, bool annotate) +static bool reportModuleResult(Luau::Frontend& frontend, const Luau::ModuleName& name, bool annotate, LuteReporter& reporter) { std::optional cr = frontend.getCheckResult(name, false); if (!cr) { - fprintf(stderr, "Failed to find result for %s\n", name.c_str()); + reporter.formatError("Failed to find result for %s\n", name.c_str()); return false; } if (!frontend.getSourceModule(name)) { - fprintf(stderr, "Error opening %s\n", name.c_str()); + reporter.formatError("Error opening %s\n", name.c_str()); return false; } for (auto& error : cr->errors) - reportError(frontend, error); + reportError(frontend, error, reporter); std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(name); for (auto& error : cr->lintResult.errors) - reportWarning(humanReadableName.c_str(), error); + reportWarning(humanReadableName.c_str(), error, reporter); for (auto& warning : cr->lintResult.warnings) - reportWarning(humanReadableName.c_str(), warning); + reportWarning(humanReadableName.c_str(), warning, reporter); return cr->errors.empty() && cr->lintResult.errors.empty(); } @@ -220,13 +140,13 @@ std::vector processSourceFiles(const std::vector& sour return files; } -int typecheck(const std::vector& sourceFilesInput) +int typecheck(const std::vector& sourceFilesInput, LuteReporter& reporter) { std::vector sourceFiles = processSourceFiles(sourceFilesInput); if (sourceFiles.empty()) { - fprintf(stderr, "Error: lute check expects a file to type check.\n\n"); + reporter.reportError("Error: lute check expects a file to type check.\n\n"); return 1; } @@ -239,13 +159,11 @@ int typecheck(const std::vector& sourceFilesInput) frontendOptions.runLintChecks = true; LuteFileResolver fileResolver; - LuteConfigResolver configResolver(mode); + Luau::LuteConfigResolver configResolver(mode); Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions); + frontend.setLuauSolverMode(Luau::SolverMode::New); Luau::registerBuiltinGlobals(frontend, frontend.globals); - Luau::LoadDefinitionFileResult loadResult = - frontend.loadDefinitionFile(frontend.globals, frontend.globals.globalScope, kLuteDefinitions, "@luau", false, false); - LUAU_ASSERT(loadResult.success); Luau::freeze(frontend.globals.globalTypes); for (const std::string& path : sourceFiles) @@ -268,7 +186,8 @@ int typecheck(const std::vector& sourceFilesInput) humanReadableName.c_str(), location, "InternalCompilerError", - Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str() + Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str(), + reporter ); return 1; } @@ -276,14 +195,14 @@ int typecheck(const std::vector& sourceFilesInput) int failed = 0; for (const Luau::ModuleName& name : checkedModules) - failed += !reportModuleResult(frontend, name, annotate); + failed += !reportModuleResult(frontend, name, annotate, reporter); if (!configResolver.configErrors.empty()) { failed += int(configResolver.configErrors.size()); for (const auto& pair : configResolver.configErrors) - fprintf(stderr, "%s: %s\n", pair.first.c_str(), pair.second.c_str()); + reporter.formatError("%s: %s\n", pair.first.c_str(), pair.second.c_str()); } return failed ? 1 : 0; diff --git a/lute/cli/src/uvstate.cpp b/lute/cli/src/uvstate.cpp new file mode 100644 index 000000000..8a7b97962 --- /dev/null +++ b/lute/cli/src/uvstate.cpp @@ -0,0 +1,13 @@ +#include "lute/uvstate.h" + +#include "uv.h" + +UvGlobalState::UvGlobalState(int argc, char** argv) +{ + uv_setup_args(argc, argv); +} + +UvGlobalState::~UvGlobalState() +{ + uv_library_shutdown(); +} diff --git a/lute/crypto/include/lute/crypto.h b/lute/crypto/include/lute/crypto.h index 5c4e64b5e..eda8835d1 100644 --- a/lute/crypto/include/lute/crypto.h +++ b/lute/crypto/include/lute/crypto.h @@ -14,24 +14,36 @@ namespace crypto { static const char kHashProperty[] = "hash"; +static const char kSecretboxProperty[] = "secretbox"; static const char kPasswordProperty[] = "password"; static const char kDigestName[] = "digest"; int lua_digest(lua_State* L); +static const char kCiphertextField[] = "ciphertext"; +static const char kKeyField[] = "key"; +static const char kNonceField[] = "nonce"; + +static const char kKeygenName[] = "keygen"; +int lua_secretbox_keygen(lua_State* L); + +static const char kSealName[] = "seal"; +int lua_secretbox_seal(lua_State* L); + +static const char kOpenName[] = "open"; +int lua_secretbox_open(lua_State* L); + static const char kPasswordHashName[] = "hash"; int lua_pwhash(lua_State* L); static const char kVerifyPasswordHashName[] = "verify"; int lua_pwhash_verify(lua_State* L); -static const luaL_Reg lib[] = { - {kDigestName, lua_digest}, - {nullptr, nullptr} -}; +static const luaL_Reg lib[] = {{kDigestName, lua_digest}, {nullptr, nullptr}}; static const std::string properties[] = { kHashProperty, + kSecretboxProperty, kPasswordProperty, }; diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index fc474e7c0..3ed774070 100644 --- a/lute/crypto/src/crypto.cpp +++ b/lute/crypto/src/crypto.cpp @@ -1,9 +1,13 @@ #include "lute/crypto.h" -#include "lua.h" +#include "lute/userdatas.h" +#include "lua.h" #include "lualib.h" + #include "openssl/digest.h" #include "sodium/crypto_pwhash.h" +#include "sodium/crypto_secretbox.h" +#include "sodium/randombytes.h" #include #include @@ -11,141 +15,256 @@ namespace crypto { - struct HashFunction +struct HashFunction +{ + std::string name; + const env_md_st* md; +}; + +static const HashFunction hashFunctions[] = { + {"md5", EVP_md5()}, + {"sha1", EVP_sha1()}, + {"sha256", EVP_sha256()}, + {"sha512", EVP_sha512()}, + {"blake2b256", EVP_blake2b256()}, +}; + +int makeHashFunctionMap(lua_State* L) +{ + lua_createtable(L, 0, std::size(hashFunctions)); + + for (auto& [name, md] : hashFunctions) { - std::string name; - const env_md_st* md; - }; + lua_pushlightuserdatatagged(L, (void*) md, kHashFunctionTag); + lua_setfield(L, -2, name.c_str()); + } + + return 1; +} - static const int kHashFunctionTag = 81; +const env_md_st* getHashFunction(lua_State* L, int idx) +{ + if (auto typ = static_cast(lua_tolightuserdatatagged(L, idx, kHashFunctionTag))) + return typ; - static const HashFunction hashFunctions[] = { - {"md5", EVP_md5()}, - {"sha1", EVP_sha1()}, - {"sha256", EVP_sha256()}, - {"sha512", EVP_sha512()}, - {"blake2b256", EVP_blake2b256()}, - }; + luaL_typeerrorL(L, idx, "hash function"); +} - int makeHashFunctionMap(lua_State* L) - { - lua_createtable(L, 0, std::size(hashFunctions)); +struct BinaryData +{ + const void* data; + size_t length; +}; + +BinaryData extractData(lua_State* L, int idx) +{ + if (!lua_isstring(L, idx) && !lua_isbuffer(L, idx)) + luaL_typeerrorL(L, idx, "string or buffer"); - for (auto& [name, md] : hashFunctions) - { - lua_pushlightuserdatatagged(L, (void*) md, kHashFunctionTag); - lua_setfield(L, -2, name.c_str()); - } + if (lua_isstring(L, idx)) + { + size_t length = 0; + const char* data = lua_tolstring(L, idx, &length); - return 1; + return BinaryData{data, length}; } - const env_md_st* getHashFunction(lua_State* L, int idx) + + if (lua_isbuffer(L, idx)) { - if (auto typ = static_cast(lua_tolightuserdatatagged(L, idx, kHashFunctionTag))) - return typ; + size_t length = 0; + void* data = lua_tobuffer(L, idx, &length); - luaL_typeerrorL(L, idx, "hash function"); + return BinaryData{data, length}; } - struct BinaryData + luaL_error(L, "failed to extract binary data from stack: %d", idx); +} + +// digest(hash: hash, message: string | buffer): buffer +int lua_digest(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 2) + luaL_error(L, "%s: expected 2 arguments, but got %d", kDigestName, argumentCount); + + const env_md_st* hashFunction = getHashFunction(L, 1); + BinaryData message = extractData(L, 2); + + uint8_t* buffer = static_cast(lua_newbuffer(L, EVP_MD_size(hashFunction))); + if (EVP_Digest(message.data, message.length, buffer, nullptr, hashFunction, nullptr) == 0) + luaL_error(L, "%s: failed to compute hash", kDigestName); + + return 1; +} + +// keygen(): buffer +int lua_secretbox_keygen(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 0) + luaL_error(L, "%s: expected no arguments, but got %d", kKeygenName, argumentCount); + + uint8_t* key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); + + return 1; +} + +// seal(message: string | buffer, key: buffer?): { ciphertext: buffer, key: buffer, nonce: buffer } +int lua_secretbox_seal(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1 && argumentCount != 2) + luaL_error(L, "%s: expected 1 or 2 arguments, but got %d", kSealName, argumentCount); + + BinaryData message = extractData(L, 1); + + lua_createtable(L, 0, 3); + + // ciphertext + uint8_t* buffer = static_cast(lua_newbuffer(L, crypto_secretbox_macbytes() + message.length)); + lua_setfield(L, -2, kCiphertextField); + + uint8_t* key; + // user provided a key + if (argumentCount == 2) { - const void* data; - size_t length; - }; + size_t keyLength = 0; + key = static_cast(luaL_checkbuffer(L, 2, &keyLength)); + if (keyLength != crypto_secretbox_keybytes()) + luaL_error(L, "%s: key buffer should be %d bytes", kOpenName, int(crypto_secretbox_keybytes())); - BinaryData extractData(lua_State* L, int idx) + lua_pushvalue(L, 2); + lua_setfield(L, -2, kKeyField); + } + else { - if (!lua_isstring(L, idx) && !lua_isbuffer(L, idx)) - luaL_typeerrorL(L, idx, "string or buffer"); + key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); + lua_setfield(L, -2, kKeyField); + } + + // nonce + uint8_t* nonce = static_cast(lua_newbuffer(L, crypto_secretbox_noncebytes())); + randombytes_buf(nonce, crypto_secretbox_noncebytes()); + lua_setfield(L, -2, kNonceField); - if (lua_isstring(L, idx)) - { - size_t length = 0; - const char* data = lua_tolstring(L, idx, &length); + // compute the ciphertext based on the message, nonce, and key + crypto_secretbox_easy(buffer, static_cast(message.data), message.length, nonce, key); - return BinaryData{data, length}; - } + // freeze the result + lua_setreadonly(L, -1, true); + return 1; +} - if (lua_isbuffer(L, idx)) - { - size_t length = 0; - void* data = lua_tobuffer(L, idx, &length); +// open(secretbox: { ciphertext: buffer, key: buffer, nonce: buffer }): buffer +int lua_secretbox_open(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 argument, but got %d", kOpenName, argumentCount); - return BinaryData{data, length}; - } + // read all three of the required fields out of the table + lua_getfield(L, 1, "ciphertext"); + lua_getfield(L, 1, "nonce"); + lua_getfield(L, 1, "key"); - luaL_error(L, "failed to extract binary data from stack: %d", idx); - } + size_t ciphertextLength = 0; + uint8_t* ciphertext = static_cast(luaL_checkbuffer(L, 2, &ciphertextLength)); - int lua_digest(lua_State* L) - { - int argumentCount = lua_gettop(L); - if (argumentCount != 2) - luaL_error(L, "%s: expected 2 arguments, but got %d", kDigestName, argumentCount); + size_t nonceLength = 0; + uint8_t* nonce = static_cast(luaL_checkbuffer(L, 3, &nonceLength)); + if (nonceLength != crypto_secretbox_noncebytes()) + luaL_error(L, "%s: nonce buffer should be %d bytes", kOpenName, int(crypto_secretbox_noncebytes())); - const env_md_st* hashFunction = getHashFunction(L, 1); - BinaryData message = extractData(L, 2); + size_t keyLength = 0; + uint8_t* key = static_cast(luaL_checkbuffer(L, 4, &keyLength)); + if (keyLength != crypto_secretbox_keybytes()) + luaL_error(L, "%s: key buffer should be %d bytes", kOpenName, int(crypto_secretbox_keybytes())); - void* buffer = lua_newbuffer(L, EVP_MD_size(hashFunction)); - if (EVP_Digest(message.data, message.length, (uint8_t*) buffer, nullptr, hashFunction, nullptr) == 0) - luaL_error(L, "%s: failed to compute hash", kDigestName); + uint8_t* buffer = static_cast(lua_newbuffer(L, ciphertextLength - crypto_secretbox_macbytes())); + if (crypto_secretbox_open_easy(buffer, ciphertext, ciphertextLength, nonce, key) != 0) + luaL_error(L, "%s: failed to verify the message", kOpenName); - return 1; - } + return 1; +} - // hash(password: string): buffer - int lua_pwhash(lua_State* L) - { - int argumentCount = lua_gettop(L); - if (argumentCount != 1) - luaL_error(L, "%s: expected 1 arguments, but got %d", kPasswordHashName, argumentCount); +int makeSecretboxLibrary(lua_State* L) +{ + lua_createtable(L, 0, 3); - size_t length = 0; - const char* password = luaL_checklstring(L, 1, &length); + // keygen + lua_pushcfunction(L, lua_secretbox_keygen, kKeygenName); + lua_setfield(L, -2, kKeygenName); - void* buffer = lua_newbuffer(L, crypto_pwhash_STRBYTES); - if (crypto_pwhash_str((char*) buffer, password, length, - crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE)) { - luaL_error(L, "%s: hit memory limit for password hashing", kPasswordHashName); - } + // seal + lua_pushcfunction(L, lua_secretbox_seal, kSealName); + lua_setfield(L, -2, kSealName); - return 1; - } + // open + lua_pushcfunction(L, lua_secretbox_open, kOpenName); + lua_setfield(L, -2, kOpenName); - // verify(hash: buffer, password: string) - int lua_pwhash_verify(lua_State* L) - { - int argumentCount = lua_gettop(L); - if (argumentCount != 2) - luaL_error(L, "%s: expected 2 arguments, but got %d", kPasswordHashName, argumentCount); + lua_setreadonly(L, -1, true); - size_t hashLength = crypto_pwhash_STRBYTES; - void* hashedPassword = luaL_checkbuffer(L, 1, &hashLength); + return 1; +} - size_t length = 0; - const char* password = luaL_checklstring(L, 2, &length); +// hash(password: string): buffer +int lua_pwhash(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 arguments, but got %d", kPasswordHashName, argumentCount); - lua_pushboolean(L, crypto_pwhash_str_verify((const char*) hashedPassword, password, length) == 0); + size_t length = 0; + const char* password = luaL_checklstring(L, 1, &length); - return 1; + void* buffer = lua_newbuffer(L, crypto_pwhash_STRBYTES); + if (crypto_pwhash_str((char*)buffer, password, length, crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE)) + { + luaL_error(L, "%s: hit memory limit for password hashing", kPasswordHashName); } - int makePasswordHashLibrary(lua_State* L) - { - lua_createtable(L, 0, 2); + return 1; +} + +// verify(hash: buffer, password: string) +int lua_pwhash_verify(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 2) + luaL_error(L, "%s: expected 2 arguments, but got %d", kPasswordHashName, argumentCount); - // hash - lua_pushcfunction(L, lua_pwhash, kPasswordHashName); - lua_setfield(L, -2, kPasswordHashName); + size_t hashLength = crypto_pwhash_STRBYTES; + void* hashedPassword = luaL_checkbuffer(L, 1, &hashLength); - // verify - lua_pushcfunction(L, lua_pwhash_verify, kVerifyPasswordHashName); - lua_setfield(L, -2, kVerifyPasswordHashName); + size_t length = 0; + const char* password = luaL_checklstring(L, 2, &length); - return 1; - } + lua_pushboolean(L, crypto_pwhash_str_verify((const char*)hashedPassword, password, length) == 0); + + return 1; +} + +int makePasswordHashLibrary(lua_State* L) +{ + lua_createtable(L, 0, 2); + + // hash + lua_pushcfunction(L, lua_pwhash, kPasswordHashName); + lua_setfield(L, -2, kPasswordHashName); + + // verify + lua_pushcfunction(L, lua_pwhash_verify, kVerifyPasswordHashName); + lua_setfield(L, -2, kVerifyPasswordHashName); + + lua_setreadonly(L, -1, true); + + return 1; +} } // namespace crypto @@ -173,10 +292,13 @@ int luteopen_crypto(lua_State* L) crypto::makeHashFunctionMap(L); lua_setfield(L, -2, crypto::kHashProperty); + crypto::makeSecretboxLibrary(L); + lua_setfield(L, -2, crypto::kSecretboxProperty); + crypto::makePasswordHashLibrary(L); lua_setfield(L, -2, crypto::kPasswordProperty); - lua_setreadonly(L, -1, 1); + lua_setreadonly(L, -1, true); return 1; } diff --git a/lute/fs/CMakeLists.txt b/lute/fs/CMakeLists.txt index a41db2abd..7d97b78fc 100644 --- a/lute/fs/CMakeLists.txt +++ b/lute/fs/CMakeLists.txt @@ -4,6 +4,8 @@ target_sources(Lute.Fs PRIVATE include/lute/fs.h src/fs.cpp + src/fs_impl.h + src/fs_impl.cpp ) target_compile_features(Lute.Fs PUBLIC cxx_std_17) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index 33b840c88..ae269fb65 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -35,10 +35,10 @@ int writestringtofile(lua_State* L); int readasync(lua_State* L); /* Removes a file */ -int fs_remove(lua_State* L); +int remove(lua_State* L); /* Creates a folder */ -int fs_mkdir(lua_State* L); +int mkdir(lua_State* L); /* Removes a directory */ int fs_rmdir(lua_State* L); @@ -74,7 +74,7 @@ static const luaL_Reg lib[] = { {"write", write}, {"close", close}, - {"remove", fs_remove}, + {"remove", remove}, {"stat", fs_stat}, {"exists", fs_exists}, @@ -85,13 +85,10 @@ static const luaL_Reg lib[] = { {"symlink", fs_symlink}, {"copy", fs_copy}, - {"mkdir", fs_mkdir}, + {"mkdir", mkdir}, {"listdir", listdir}, {"rmdir", fs_rmdir}, - {"readfiletostring", readfiletostring}, - {"writestringtofile", writestringtofile}, - {"readasync", readasync}, {NULL, NULL}, }; diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index c257fdbf2..cdf1f72c7 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -1,26 +1,32 @@ #include "lute/fs.h" -#include "lua.h" -#include "lualib.h" -#include "uv.h" - #include "lute/runtime.h" #include "lute/time.h" #include "lute/userdatas.h" +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + #include #include + +#include "fs_impl.h" #ifdef _WIN32 #include +#else +#include #endif #include #include #include #include #include -#include -#include #include +#include + +#include #if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) @@ -66,6 +72,22 @@ const char* UV_DIRENT_TYPES[] = { UV_TYPENAME_BLOCK, }; +static UVFile* getFileHandle(lua_State* L, int index) +{ + if (!lua_islightuserdata(L, index)) + { + luaL_errorL(L, "Error: expected file handle"); + } + + auto* handle = static_cast(lua_tolightuserdata(L, index)); + if (!handle) + { + luaL_errorL(L, "Error: invalid file handle"); + } + + return handle; +} + std::optional setFlags(const char* c, int* openFlags) { int modeFlags = 0x0000; @@ -156,218 +178,91 @@ static const char* fileModeToType(uint64_t mode) } } -struct FileHandle -{ - ssize_t fileDescriptor = -1; - int errcode = -1; -}; - -l_noret luaL_errorHandle(lua_State* L, FileHandle& handle) -{ -#ifdef _MSC_VER - luaL_errorL(L, "Error writing to file with descriptor %Iu\n", handle.fileDescriptor); -#else - luaL_errorL(L, "Error writing to file with descriptor %zu\n", handle.fileDescriptor); -#endif -} - -void setfield(lua_State* L, const char* index, int value) -{ - lua_pushstring(L, index); - lua_pushinteger(L, value); - lua_settable(L, -3); -} - -void createFileHandle(lua_State* L, const FileHandle& toCreate) -{ - lua_newtable(L); - setfield(L, "fd", toCreate.fileDescriptor); - setfield(L, "err", toCreate.errcode); -} - -FileHandle unpackFileHandle(lua_State* L) -{ - FileHandle result; - - luaL_checktype(L, 1, LUA_TTABLE); - lua_getfield(L, 1, "fd"); - lua_getfield(L, 1, "err"); - - ssize_t fd = luaL_checkinteger(L, -2); - int err = luaL_checknumber(L, -1); - result.fileDescriptor = fd; - result.errcode = err; - - lua_pop(L, 2); // we got the args by value, so we can clean up the stack here - - return result; -} - int close(lua_State* L) { - lua_settop(L, 1); - FileHandle file = unpackFileHandle(L); - - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, file.fileDescriptor, nullptr); - return 0; + auto* handle = getFileHandle(L, 1); + return close_impl(L, handle); } -static char readBuffer[1024]; int read(lua_State* L) { - memset(readBuffer, 0, sizeof(readBuffer)); - // discard any extra arguments passed in - lua_settop(L, 1); - FileHandle file = unpackFileHandle(L); - - int numBytesRead = 0; - uv_fs_t readReq; - uv_buf_t iov = uv_buf_init(readBuffer, sizeof(readBuffer)); - // Output data - std::vector resultData; - do - { - uv_fs_read(uv_default_loop(), &readReq, file.fileDescriptor, &iov, 1, -1, nullptr); - - numBytesRead = readReq.result; - - if (numBytesRead < 0) - { - luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); - memset(readBuffer, 0, sizeof(readBuffer)); - return 0; - } - - for (int i = 0; i < numBytesRead; i++) - resultData.push_back(readBuffer[i]); - - } while (numBytesRead > 0); - - lua_pushlstring(L, resultData.data(), resultData.size()); - - // Clean up the scratch space - memset(readBuffer, 0, sizeof(readBuffer)); - return 1; + auto* handle = getFileHandle(L, 1); + return read_impl(L, handle); } int write(lua_State* L) { - char writeBuffer[4096]; + auto* handle = getFileHandle(L, 1); - // Reset the write buffer - int wbSize = sizeof(writeBuffer); - memset(writeBuffer, 0, sizeof(writeBuffer)); - FileHandle file = unpackFileHandle(L); size_t len; - const char* stringToWrite = luaL_checklstring(L, 2, &len); + const char* data = luaL_checklstring(L, 2, &len); - // Set up the buffer to write - int numBytesLeftToWrite = len; - int offset = 0; - do - { - // copy stringToWrite[0], numBytesLeftToWrite into write buffer - - int sizeToWrite = std::min(wbSize, numBytesLeftToWrite); - memcpy(writeBuffer, stringToWrite + offset, sizeToWrite); - uv_buf_t iov = uv_buf_init(writeBuffer, sizeToWrite); - - uv_fs_t writeReq; - int bytesWritten = 0; - uv_fs_write(uv_default_loop(), &writeReq, file.fileDescriptor, &iov, 1, -1, nullptr); - bytesWritten = writeReq.result; - - if (bytesWritten < 0) - { - // Error case. - luaL_errorHandle(L, file); - memset(writeBuffer, 0, sizeof(writeBuffer)); - return 0; - } - - - offset += bytesWritten; - numBytesLeftToWrite -= bytesWritten; - } while (numBytesLeftToWrite > 0); - - return 0; -} -// Returns 0 on error, 1 otherwise -std::optional openHelper(lua_State* L, const char* path, const char* mode, int* openFlags) -{ - std::optional modeFlags = setFlags(mode, openFlags); - if (!modeFlags) - return std::nullopt; - - uv_fs_t openReq; - int errcode = uv_fs_open(uv_default_loop(), &openReq, path, *openFlags, *modeFlags, nullptr); - if (openReq.result < 0) - { - luaL_errorL(L, "Error opening file %s\n", path); - return std::nullopt; - } - - return FileHandle{openReq.result, errcode}; + return write_impl(L, handle, data, len); } int open(lua_State* L) { int nArgs = lua_gettop(L); - const char* path = luaL_checkstring(L, 1); - int openFlags = 0x0000; - // When the number of arguments is less 2 if (nArgs < 1) { luaL_errorL(L, "Error: no file supplied\n"); - return 0; } + const char* path = luaL_checkstring(L, 1); - if (nArgs < 2) + int openFlags = 0x0000; + const char* mode = "r"; + // Default to read mode if no mode is supplied (i.e., mode is nil in Luau) + if (nArgs < 2 || lua_isnil(L, 2)) { openFlags = O_RDONLY; } - - const char* mode = luaL_checkstring(L, 2); - if (std::optional result = openHelper(L, path, mode, &openFlags)) + else { - createFileHandle(L, *result); - return 1; + mode = luaL_checkstring(L, 2); } - return 0; -} + std::optional modeFlags = setFlags(mode, &openFlags); + if (!modeFlags) + { + luaL_errorL(L, "Error decoding mode: %s\n", mode); + } -void cleanup(char* buffer, int size, const FileHandle& handle) -{ - memset(buffer, 0, size); - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, handle.fileDescriptor, nullptr); + return open_impl(L, path, openFlags, *modeFlags); } -int fs_remove(lua_State* L) +int remove(lua_State* L) { - uv_fs_t unlink_req; - int err = uv_fs_unlink(uv_default_loop(), &unlink_req, luaL_checkstring(L, 1), nullptr); + int nArgs = lua_gettop(L); + if (nArgs < 1) + { + luaL_errorL(L, "Error: no file supplied\n"); + } - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); + if (nArgs > 1) + { + luaL_errorL(L, "Error: too many arguments supplied\n"); + } + const char* path = luaL_checkstring(L, 1); - return 0; + return remove_impl(L, path); } -int fs_mkdir(lua_State* L) +int mkdir(lua_State* L) { + int nArgs = lua_gettop(L); + if (nArgs < 1) + { + luaL_errorL(L, "Error: no path supplied\n"); + } + + if (nArgs > 2) + { + luaL_errorL(L, "Error: too many arguments supplied\n"); + } const char* path = luaL_checkstring(L, 1); int mode = luaL_optinteger(L, 2, 0777); - uv_fs_t req; - int err = uv_fs_mkdir(uv_default_loop(), &req, path, mode, nullptr); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - return 0; + return mkdir_impl(L, path, mode); } int fs_rmdir(lua_State* L) @@ -376,6 +271,7 @@ int fs_rmdir(lua_State* L) uv_fs_t rmdir_req; int err = uv_fs_rmdir(uv_default_loop(), &rmdir_req, path, nullptr); + uv_fs_req_cleanup(&rmdir_req); if (err) luaL_errorL(L, "%s", uv_strerror(err)); @@ -391,7 +287,10 @@ int fs_stat(lua_State* L) int err = uv_fs_stat(uv_default_loop(), &stat_req, path, nullptr); if (err) + { + uv_fs_req_cleanup(&stat_req); luaL_errorL(L, "%s", uv_strerror(err)); + } lua_createtable(L, 0, 6); @@ -406,13 +305,13 @@ int fs_stat(lua_State* L) lua_setfield(L, -2, "size"); createDurationFromTimespec32(L, stat.st_birthtim); - lua_setfield(L, -2, "created_at"); + lua_setfield(L, -2, "created"); createDurationFromTimespec32(L, stat.st_atim); - lua_setfield(L, -2, "accessed_at"); + lua_setfield(L, -2, "accessed"); createDurationFromTimespec32(L, stat.st_mtim); - lua_setfield(L, -2, "modified_at"); + lua_setfield(L, -2, "modified"); // permissions lua_createtable(L, 0, 2); @@ -424,28 +323,31 @@ int fs_stat(lua_State* L) lua_setfield(L, -2, "permissions"); + uv_fs_req_cleanup(&stat_req); + return 1; } static void defaultCallback(uv_fs_t* req) { auto* request_state = static_cast(req->data); + auto token = std::move(*request_state); + delete request_state; - if (req->result) + auto err = req->result; + + uv_fs_req_cleanup(req); + delete req; + + if (err) { - request_state->get()->fail(uv_strerror(req->result)); - uv_fs_req_cleanup(req); - delete req; + token->fail(uv_strerror(err)); return; } - request_state->get()->complete( - [req](lua_State* L) + token->complete( + [](lua_State* L) { - uv_fs_req_cleanup(req); - - delete req; - return 0; } ); @@ -463,6 +365,8 @@ int fs_copy(lua_State* L) if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -481,6 +385,8 @@ int fs_link(lua_State* L) if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -508,6 +414,8 @@ int fs_symlink(lua_State* L) if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -531,6 +439,8 @@ struct WatchHandle luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); } + uv_close((uv_handle_t*)&handle, nullptr); + isClosed = true; getRuntime(L)->releasePendingToken(); @@ -556,12 +466,6 @@ static int closeWatchHandle(lua_State* L) return 0; } - int err = uv_fs_event_stop(&handle->handle); - if (err) - { - luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); - } - handle->close(); return 0; @@ -605,32 +509,15 @@ int fs_watch(lua_State* L) eventHandle->callbackReference->push(L); // filename - lua_pushstring(L, filename.c_str()); + lua_pushlstring(L, filename.c_str(), filename.size()); // events lua_createtable(L, 0, 2); - if ((events & UV_RENAME) == UV_RENAME) - { - lua_pushboolean(L, true); - lua_setfield(L, -2, "rename"); - } - else - { - lua_pushboolean(L, false); - lua_setfield(L, -2, "rename"); - } - - if ((events & UV_CHANGE) == UV_CHANGE) - { - lua_pushboolean(L, true); - lua_setfield(L, -2, "change"); - } - else - { - lua_pushboolean(L, false); - lua_setfield(L, -2, "change"); - } + lua_pushboolean(L, (events & UV_RENAME) != 0); + lua_setfield(L, -2, "rename"); + lua_pushboolean(L, (events & UV_CHANGE) != 0); + lua_setfield(L, -2, "change"); return 2; } @@ -659,10 +546,11 @@ int fs_exists(lua_State* L) auto* req = new uv_fs_t(); req->data = new ResumeToken(getResumeToken(L)); - int err = uv_fs_stat( + int err = uv_fs_access( uv_default_loop(), req, path, + F_OK, [](uv_fs_t* req) { auto* request_state = static_cast(req->data); @@ -670,15 +558,7 @@ int fs_exists(lua_State* L) request_state->get()->complete( [req](lua_State* L) { - if (req->result == UV_ENOENT) - { - lua_pushboolean(L, false); // does not exist - } - else - { - lua_pushboolean(L, true); - } - + lua_pushboolean(L, req->result == 0); uv_fs_req_cleanup(req); delete req; @@ -686,11 +566,14 @@ int fs_exists(lua_State* L) return 1; } ); + delete request_state; } ); if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -706,7 +589,10 @@ int type(lua_State* L) int err = uv_fs_stat(uv_default_loop(), &req, path, nullptr); if (err) + { + uv_fs_req_cleanup(&req); luaL_errorL(L, "%s", uv_strerror(err)); + } auto type = fileModeToType(req.statbuf.st_mode); lua_pushstring(L, type); @@ -756,6 +642,7 @@ int listdir(lua_State* L) uv_fs_req_cleanup(req); + delete static_cast(req->data); delete req; if (err != UV_EOF) @@ -768,210 +655,12 @@ int listdir(lua_State* L) ); if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - return lua_yield(L, 0); -} - -int readfiletostring(lua_State* L) -{ - const char* path = luaL_checkstring(L, 1); - const char openMode[] = "r"; - int openFlags = 0x0000; - std::optional handle = openHelper(L, path, openMode, &openFlags); - if (!handle) - { - luaL_errorL(L, "Error opening file for reading at %s\n", path); - return 0; - } - - memset(readBuffer, 0, sizeof(readBuffer)); - // discard any extra arguments passed in - lua_settop(L, 1); - - int numBytesRead = 0; - uv_fs_t readReq; - uv_buf_t iov = uv_buf_init(readBuffer, sizeof(readBuffer)); - // Output data - std::vector resultData; - do - { - uv_fs_read(uv_default_loop(), &readReq, handle->fileDescriptor, &iov, 1, -1, nullptr); - - numBytesRead = readReq.result; - - if (numBytesRead < 0) - { - luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); - cleanup(readBuffer, sizeof(readBuffer), *handle); - return 0; - } - - for (int i = 0; i < numBytesRead; i++) - resultData.push_back(readBuffer[i]); - - } while (numBytesRead > 0); - - lua_pushlstring(L, resultData.data(), resultData.size()); - - // Clean up the scratch space - cleanup(readBuffer, sizeof(readBuffer), *handle); - return 1; -} - -int writestringtofile(lua_State* L) -{ - char writeBuffer[4096]; - - const char* path = luaL_checkstring(L, 1); - const char openMode[] = "w+"; - int openFlags = 0x0000; - std::optional handle = openHelper(L, path, openMode, &openFlags); - if (!handle) - { - luaL_errorL(L, "Error opening file for reading at %s\n", path); - return 0; - } - - int wbSize = sizeof(writeBuffer); - memset(writeBuffer, 0, sizeof(writeBuffer)); - size_t len; - const char* stringToWrite = luaL_checklstring(L, 2, &len); - - // Set up the buffer to write - int numBytesLeftToWrite = len; - int offset = 0; - uv_buf_t iov; - do - { - // copy stringToWrite[0], numBytesLeftToWrite into write buffer - - int sizeToWrite = std::min(wbSize, numBytesLeftToWrite); - memcpy(writeBuffer, stringToWrite + offset, sizeToWrite); - iov = uv_buf_init(writeBuffer, sizeToWrite); - - uv_fs_t writeReq; - int bytesWritten = 0; - uv_fs_write(uv_default_loop(), &writeReq, handle->fileDescriptor, &iov, 1, -1, nullptr); - bytesWritten = writeReq.result; - - if (bytesWritten < 0) - { - // Error case. - luaL_errorHandle(L, *handle); - cleanup(writeBuffer, sizeof(writeBuffer), *handle); - return 0; - } - - - offset += bytesWritten; - numBytesLeftToWrite -= bytesWritten; - } while (numBytesLeftToWrite > 0); - - cleanup(writeBuffer, sizeof(writeBuffer), *handle); - return 0; -} - -struct ResumeCaptureInformation -{ - explicit ResumeCaptureInformation(lua_State* L) - : token(getResumeToken(L)) { + delete static_cast(req->data); + delete req; + luaL_errorL(L, "%s", uv_strerror(err)); } - ResumeToken token = nullptr; -}; - -uv_fs_t* createRequest(lua_State* L) -{ - uv_fs_t* req = new uv_fs_t(); - req->data = new ResumeCaptureInformation(L); - return req; -} - -ResumeCaptureInformation* getResumeInformation(uv_fs_t* req) -{ - return reinterpret_cast(req->data); -} - -int readasync(lua_State* L) -{ - const char* path = luaL_checkstring(L, 1); - - uv_fs_t* openReq = createRequest(L); - uv_fs_open( - uv_default_loop(), - openReq, - path, - O_RDONLY, - 0, - [](uv_fs_t* req) - { - ResumeCaptureInformation* info = getResumeInformation(req); - int fd = req->result; - - if (fd < 0) - { - info->token->fail("Error opening file"); - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); - uv_fs_req_cleanup(req); - delete (ResumeCaptureInformation*)req->data; - delete req; - return; - } - - // Allocate the destination buffer for reading - char readBuffer[1024]; - memset(readBuffer, 0, sizeof(readBuffer)); - uv_buf_t iov = uv_buf_init(readBuffer, sizeof(readBuffer)); - // Read data - int numBytesRead = 0; - uv_fs_t readReq; - // Output data - std::vector resultData; - - do - { - uv_fs_read(uv_default_loop(), &readReq, fd, &iov, 1, -1, nullptr); - numBytesRead = readReq.result; - - if (numBytesRead < 0) - { - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); - // Schedule error; - // Also, we should free the original request. We don't have to do this for the read req since it's sycnrhonous - info->token->fail("Error reading file"); - uv_fs_req_cleanup(req); - delete (ResumeCaptureInformation*)req->data; - delete req; - return; - } - - for (int i = 0; i < numBytesRead; i++) - resultData.push_back(readBuffer[i]); - - } while (numBytesRead > 0); - - // Push the result buffer onto the stack - info->token->complete( - [data = std::move(resultData)](lua_State* L) - { - lua_pushlstring(L, data.data(), data.size()); - return 1; - } - ); - - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); - // free the read buffer as well as the resume information and the request - delete (ResumeCaptureInformation*)req->data; - delete req; - return; - } - ); - return lua_yield(L, 0); } @@ -1008,9 +697,7 @@ static void initalizeFS(lua_State* L) kWatchHandleTag, [](lua_State* L, void* ud) { - auto* handle = static_cast(ud); - - handle->~WatchHandle(); + std::destroy_at(static_cast(ud)); } ); diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp new file mode 100644 index 000000000..3d64b105e --- /dev/null +++ b/lute/fs/src/fs_impl.cpp @@ -0,0 +1,299 @@ +#include "fs_impl.h" + +#include "lute/UVRequest.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +constexpr size_t kChunkIOSize = 4096; + +namespace fs +{ + +using FSRequest = uvutils::UVRequest; + +struct FSRead : FSRequest +{ + FSRead(lua_State* L, UVFile* file) + : FSRequest(L) + , file(file) + { + chunk.resize(kChunkIOSize); + iov = uv_buf_init(chunk.data(), chunk.size()); + buffer.reserve(kChunkIOSize); + } + + static void readCallback(uv_fs_t* req); + + UVFile* file = nullptr; + std::vector buffer; + std::vector chunk; + uv_buf_t iov; +}; + +struct FSWrite : FSRequest +{ + FSWrite(lua_State* L, UVFile* file, const char* buf, size_t len) + : FSRequest(L) + , file(file) + , toWrite(buf, buf + len) + , offset(0) + { + chunk.resize(kChunkIOSize); + } + + static void writeCallback(uv_fs_t* req); + + UVFile* file = nullptr; + std::vector chunk; + uv_buf_t iov; + std::vector toWrite; + size_t offset = 0; +}; + +struct FSClose : FSRequest +{ + FSClose(lua_State* L, UVFile* file) + : FSRequest(L) + , file(file) + { + } + + ~FSClose() + { + delete file; + } + + UVFile* file = nullptr; +}; + +int open_impl(lua_State* L, const char* path, int flags, int mode) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_open( + uv_default_loop(), + &req->req, + path, + flags, + mode, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error opening file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [result](lua_State* L) + { + auto* file = new UVFile(); + file->fd = result; + lua_pushlightuserdata(L, file); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +void FSRead::readCallback(uv_fs_t* req) +{ + auto r = uvutils::retake(req); + auto bytesRead = req->result; + + if (bytesRead < 0) + { + r->fail("Error reading file: %s", uv_strerror(bytesRead)); + return; + } + + if (bytesRead == 0) + { + r->succeed( + [buffer = std::move(r->buffer)](lua_State* L) + { + lua_pushlstring(L, buffer.data(), buffer.size()); + return 1; + } + ); + return; + } + + // Append the read data to our buffer + r->buffer.insert(r->buffer.end(), r->chunk.begin(), r->chunk.begin() + bytesRead); + + // It's possible that the next read call will read fewer than chunk.size() bytes + // In this case, the chunk buffer might still retain some data from this read. Just to be safe, zero it out + std::fill(r->chunk.begin(), r->chunk.end(), 0); + + uvutils::ScopedUVRequest scopedReq{std::move(r)}; + uv_fs_read(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSRead::readCallback); +} + +void FSWrite::writeCallback(uv_fs_t* req) +{ + auto w = uvutils::retake(req); + auto bytesWritten = req->result; + if (bytesWritten < 0) + { + w->fail("Error writing file: %s", uv_strerror(bytesWritten)); + return; + } + + w->offset += bytesWritten; + if (w->offset == w->toWrite.size()) + { + w->succeed( + [](lua_State* L) + { + return 0; + } + ); + + return; + } + + // Copy next chunk to write + size_t remaining = w->toWrite.size() - w->offset; + size_t chunkSize = std::min(remaining, w->chunk.size()); + std::copy(w->toWrite.begin() + w->offset, w->toWrite.begin() + w->offset + chunkSize, w->chunk.begin()); + w->iov = uv_buf_init(w->chunk.data(), chunkSize); + + uvutils::ScopedUVRequest scopedReq{std::move(w)}; + uv_fs_write(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSWrite::writeCallback); +} + +int read_impl(lua_State* L, UVFile* handle) +{ + if (!handle->fd.has_value()) + { + luaL_errorL(L, "File handle is closed"); + } + + uvutils::ScopedUVRequest req{L, handle}; + uv_fs_read(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSRead::readCallback); + // Automatically releases when req goes out of scope + return lua_yield(L, 0); +} + +int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes) +{ + if (!handle->fd.has_value()) + { + luaL_errorL(L, "File handle is closed"); + } + + uvutils::ScopedUVRequest req{L, handle, toWrite, numBytes}; + + // Copy first chunk to write + size_t chunkSize = std::min(numBytes, req->chunk.size()); + std::copy(req->toWrite.begin(), req->toWrite.begin() + chunkSize, req->chunk.begin()); + req->iov = uv_buf_init(req->chunk.data(), chunkSize); + + uv_fs_write(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSWrite::writeCallback); + + return lua_yield(L, 0); +} + +int close_impl(lua_State* L, UVFile* handle) +{ + if (!handle->fd.has_value()) + { + luaL_errorL(L, "File handle is already closed"); + } + + uvutils::ScopedUVRequest req{L, handle}; + uv_fs_close( + uv_default_loop(), + &req->req, + handle->fd.value(), + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("Error closing file: %s", uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int remove_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_unlink( + uv_default_loop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error removing file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int mkdir_impl(lua_State* L, const char* path, int mode) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_mkdir( + uv_default_loop(), + &req->req, + path, + mode, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error creating directory %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + +} // namespace fs diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h new file mode 100644 index 000000000..c0360907a --- /dev/null +++ b/lute/fs/src/fs_impl.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +struct lua_State; + +namespace fs +{ + +struct UVFile +{ + std::optional fd = std::nullopt; +}; + +// New fs operations using UVRequest abstraction +int open_impl(lua_State* L, const char* path, int flags, int mode); +int read_impl(lua_State* L, UVFile* handle); +int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes); +int close_impl(lua_State* L, UVFile* handle); +int remove_impl(lua_State* L, const char* path); +int mkdir_impl(lua_State* L, const char* path, int mode); +} // namespace fs diff --git a/lute/io/CMakeLists.txt b/lute/io/CMakeLists.txt new file mode 100644 index 000000000..6d3c9c9f3 --- /dev/null +++ b/lute/io/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(Lute.IO STATIC) + +target_sources(Lute.IO PRIVATE + include/lute/io.h + + src/io.cpp +) + +target_compile_features(Lute.IO PUBLIC cxx_std_17) +target_include_directories(Lute.IO PUBLIC "include" ${LIBUV_INCLUDE_DIR}) +target_link_libraries(Lute.IO PRIVATE Lute.Runtime Luau.VM uv_a) +target_compile_options(Lute.IO PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/io/include/lute/io.h b/lute/io/include/lute/io.h new file mode 100644 index 000000000..7626d9565 --- /dev/null +++ b/lute/io/include/lute/io.h @@ -0,0 +1,22 @@ +#pragma once + +#include "lua.h" +#include "lualib.h" + +// open the library as a standard global luau library +int luaopen_io(lua_State* L); +// open the library as a table on top of the stack +int luteopen_io(lua_State* L); + +namespace io +{ + +int read(lua_State* L); + +static const luaL_Reg lib[] = { + {"read", read}, + + {nullptr, nullptr} +}; + +} // namespace io diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp new file mode 100644 index 000000000..926302da1 --- /dev/null +++ b/lute/io/src/io.cpp @@ -0,0 +1,140 @@ +#include "lute/io.h" + +#include "lute/runtime.h" + +#include "Luau/Variant.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +#include + + +namespace io +{ + +struct IOHandle +{ + Luau::Variant streamVariant; + uv_loop_t* loop = nullptr; + ResumeToken resumeToken; + std::shared_ptr self; + std::vector buffer; + + void closeHandles() + { + auto closeCb = [](uv_handle_t* handle) + { + IOHandle* ioh = static_cast(handle->data); + ioh->self.reset(); + }; + + uv_stream_t* stream = getStream(); + uv_read_stop(stream); + uv_close((uv_handle_t*)stream, closeCb); + } + + uv_stream_t* getStream() + { + return Luau::visit( + [](auto& stream) -> uv_stream_t* + { + return (uv_stream_t*)&stream; + }, + streamVariant + ); + } +}; + +static void allocBuffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) +{ + IOHandle* ioh = static_cast(handle->data); + ioh->buffer.resize(suggestedSize); + buf->base = ioh->buffer.data(); + buf->len = ioh->buffer.size(); +} + +// IOHandle is closed immediately after one read since we don't need a long running stream for this API. +static void onTtyRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) +{ + IOHandle* handle = static_cast(stream->data); + + if (nread > 0) + { + handle->resumeToken->complete( + [data = std::string(buf->base, nread)](lua_State* L) -> int + { + lua_pushlstring(L, data.c_str(), data.size()); + return 1; + } + ); + } + else if (nread < 0) + { + handle->resumeToken->fail(uv_strerror(nread)); + } + + handle->closeHandles(); +} + +int read(lua_State* L) +{ + auto handle = std::make_shared(); + handle->loop = uv_default_loop(); + handle->resumeToken = getResumeToken(L); + handle->self = handle; + + uv_handle_type ht = uv_guess_handle(fileno(stdin)); + if (ht == UV_TTY) + { + uv_tty_t& tty = handle->streamVariant.emplace(); + int status = uv_tty_init(handle->loop, &tty, fileno(stdin), 0); + if (status < 0) + luaL_error(L, "Failed to initialize TTY: %s", uv_strerror(status)); + } + else if (ht == UV_NAMED_PIPE || ht == UV_FILE) + { + uv_pipe_t& pipe = handle->streamVariant.emplace(); + int status = uv_pipe_init(handle->loop, static_cast(&pipe), 0); + if (status < 0) + luaL_error(L, "Failed to initialize pipe: %s", uv_strerror(status)); + uv_pipe_open(static_cast(&pipe), fileno(stdin)); + } + else + { + luaL_error(L, "Unsupported stdin type"); + } + + uv_stream_t* stream = handle->getStream(); + stream->data = handle.get(); + uv_read_start(stream, allocBuffer, onTtyRead); + return lua_yield(L, 0); +} + +} // namespace io + +int luaopen_io(lua_State* L) +{ + luaL_register(L, "io", io::lib); + return 1; +} + +int luteopen_io(lua_State* L) +{ + lua_createtable(L, 0, std::size(io::lib)); + + for (auto& [name, func] : io::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return 1; +} diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index edbac0404..efe530aa9 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -1,12 +1,18 @@ add_library(Lute.Luau STATIC) target_sources(Lute.Luau PRIVATE + include/lute/configresolver.h include/lute/luau.h + include/lute/moduleresolver.h + include/lute/resolverequire.h + src/configresolver.cpp src/luau.cpp + src/moduleresolver.cpp + src/resolverequire.cpp ) target_compile_features(Lute.Luau PUBLIC cxx_std_17) target_include_directories(Lute.Luau PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Luau PRIVATE Lute.Runtime Luau.VM uv_a Luau.Analysis Luau.Ast Luau.Compiler) +target_link_libraries(Lute.Luau PRIVATE Lute.Require Lute.Runtime uv_a Luau.Analysis Luau.Ast Luau.Compiler Luau.CLI.lib Luau.Require Luau.VM) target_compile_options(Lute.Luau PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/luau/include/lute/configresolver.h b/lute/luau/include/lute/configresolver.h new file mode 100644 index 000000000..4b6a9f1a2 --- /dev/null +++ b/lute/luau/include/lute/configresolver.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Luau/Config.h" +#include "Luau/ConfigResolver.h" + +namespace Luau +{ + +// Based on LuteConfigResolver in tc.cpp. +struct LuteConfigResolver : Luau::ConfigResolver +{ + Luau::Config defaultConfig; + mutable std::unordered_map configCache; + mutable std::vector> configErrors; + + LuteConfigResolver(Luau::Mode mode); + + const Luau::Config& getConfig(const Luau::ModuleName& name, const TypeCheckLimits& limits) const override; + + const Luau::Config& readConfigRec(const std::string& path) const; +}; + +} // namespace Luau diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index fe124c788..204f23c60 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/resolverequire.h" + #include "lua.h" #include "lualib.h" @@ -11,6 +13,10 @@ int luteopen_luau(lua_State* L); namespace luau { +static const char kSpanType[] = "span"; +static const char kCompileResultType[] = "CompileResult"; +static const char kSpanCreateName[] = "span.create"; + int luau_parse(lua_State* L); int luau_parseexpr(lua_State* L); @@ -19,12 +25,20 @@ int compile_luau(lua_State* L); int load_luau(lua_State* L); +int typeofmodule_luau(lua_State* L); + static const luaL_Reg lib[] = { {"parse", luau_parse}, {"parseexpr", luau_parseexpr}, {"compile", compile_luau}, {"load", load_luau}, + {"resolverequire", resolverequire_luau}, + {"typeofmodule", typeofmodule_luau}, {nullptr, nullptr}, }; +static const std::string properties[] = { + kSpanType, +}; + } // namespace luau diff --git a/lute/luau/include/lute/moduleresolver.h b/lute/luau/include/lute/moduleresolver.h new file mode 100644 index 000000000..c023346fd --- /dev/null +++ b/lute/luau/include/lute/moduleresolver.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Luau/FileResolver.h" + +namespace Luau +{ + +// Based on CliFileResolver in Analyze.cpp. +struct LuteModuleResolver : Luau::FileResolver +{ + LuteModuleResolver() = default; + + std::optional readSource(const Luau::ModuleName& name) override; + + // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. + std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) override; +}; + +} // namespace Luau diff --git a/lute/luau/include/lute/resolverequire.h b/lute/luau/include/lute/resolverequire.h new file mode 100644 index 000000000..62134700e --- /dev/null +++ b/lute/luau/include/lute/resolverequire.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +struct lua_State; + +std::optional resolveRequire(std::string requirePath, std::string requirerChunkname, std::string* error); +int resolverequire_luau(lua_State* L); diff --git a/lute/luau/src/configresolver.cpp b/lute/luau/src/configresolver.cpp new file mode 100644 index 000000000..c5305df20 --- /dev/null +++ b/lute/luau/src/configresolver.cpp @@ -0,0 +1,79 @@ +#include "lute/configresolver.h" + +#include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" +#include "Luau/StringUtils.h" + +namespace Luau +{ + +LuteConfigResolver::LuteConfigResolver(Luau::Mode mode) +{ + defaultConfig.mode = mode; +} + +const Luau::Config& LuteConfigResolver::getConfig(const Luau::ModuleName& name, const TypeCheckLimits& limits) const +{ + std::optional path = getParentPath(name); + if (!path) + return defaultConfig; + + return readConfigRec(*path); +} + +const Luau::Config& LuteConfigResolver::readConfigRec(const std::string& path) const +{ + auto it = configCache.find(path); + if (it != configCache.end()) + return it->second; + + std::optional parent = getParentPath(path); + Luau::Config result = parent ? readConfigRec(*parent) : defaultConfig; + + std::optional configPath = joinPaths(path, Luau::kConfigName); + if (!isFile(*configPath)) + configPath = std::nullopt; + + std::optional luauConfigPath = joinPaths(path, Luau::kLuauConfigName); + if (!isFile(*luauConfigPath)) + luauConfigPath = std::nullopt; + + if (configPath && luauConfigPath) + { + std::string ambiguousError = format("Both %s and %s files exist", Luau::kConfigName, Luau::kLuauConfigName); + configErrors.emplace_back(*configPath, std::move(ambiguousError)); + } + else if (configPath) + { + if (std::optional contents = readFile(*configPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = *configPath; + aliasOpts.overwriteAliases = true; + + Luau::ConfigOptions opts; + opts.aliasOptions = std::move(aliasOpts); + + std::optional error = Luau::parseConfig(*contents, result, opts); + if (error) + configErrors.emplace_back(*configPath, *error); + } + } + else if (luauConfigPath) + { + if (std::optional contents = readFile(*luauConfigPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = *luauConfigPath; + aliasOpts.overwriteAliases = true; + + std::optional error = Luau::extractLuauConfig(*contents, result, aliasOpts, Luau::InterruptCallbacks{}); + if (error) + configErrors.emplace_back(*luauConfigPath, *error); + } + } + + return configCache[path] = result; +} + +} // namespace Luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index d17ab6d1e..2d86f5068 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1,29 +1,28 @@ #include "lute/luau.h" +#include "lute/configresolver.h" +#include "lute/moduleresolver.h" +#include "lute/userdatas.h" + #include "Luau/Ast.h" +#include "Luau/BuiltinDefinitions.h" +#include "Luau/Compiler.h" +#include "Luau/Frontend.h" #include "Luau/Location.h" -#include "Luau/ParseResult.h" -#include "Luau/Parser.h" +#include "Luau/NotNull.h" #include "Luau/ParseOptions.h" +#include "Luau/Parser.h" +#include "Luau/ParseResult.h" #include "Luau/ToString.h" -#include "Luau/Compiler.h" -#include "Luau/NotNull.h" - -#include "lute/userdatas.h" - #include "lua.h" #include "lualib.h" + #include #include #include - -const char* COMPILE_RESULT_TYPE = "CompileResult"; - -LUAU_FASTFLAG(LuauStoreCSTData2) -LUAU_FASTFLAG(LuauStoreReturnTypesAsPackOnAst) -LUAU_FASTFLAG(LuauStoreLocalAnnotationColonPositions) -LUAU_FASTFLAG(LuauCSTForReturnTypeFunctionTail) +#include +#include namespace luau { @@ -38,12 +37,6 @@ struct StatResult static StatResult parse(std::string& source) { - // TODO: this is very bad, fix it! - FFlag::LuauStoreCSTData2.value = true; - FFlag::LuauStoreReturnTypesAsPackOnAst.value = true; - FFlag::LuauStoreLocalAnnotationColonPositions.value = true; - FFlag::LuauCSTForReturnTypeFunctionTail.value = true; - auto allocator = std::make_shared(); auto names = std::make_shared(*allocator); @@ -62,17 +55,11 @@ struct ExprResult std::shared_ptr allocator; std::shared_ptr names; - Luau::ParseExprResult parseResult; + Luau::ParseNodeResult parseResult; }; static ExprResult parseExpr(std::string& source) { - // TODO: this is very bad, fix it! - FFlag::LuauStoreCSTData2.value = true; - FFlag::LuauStoreReturnTypesAsPackOnAst.value = true; - FFlag::LuauStoreLocalAnnotationColonPositions.value = true; - FFlag::LuauCSTForReturnTypeFunctionTail.value = true; - auto allocator = std::make_shared(); auto names = std::make_shared(*allocator); @@ -132,6 +119,109 @@ struct Trivia std::string_view text; }; +// the userdata version of `Luau::Location` because exposing this as a table was, unfortunately, very impractical +// it happens too much all over the entire AST to do reasonably. +struct Span +{ + uint32_t beginLine; + uint32_t beginColumn; + uint32_t endLine; + uint32_t endColumn; +}; + +static int createSpan(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 argument, but got %d", kSpanCreateName, argumentCount); + + // read all three of the required fields out of the table + lua_getfield(L, 1, "beginline"); + lua_getfield(L, 1, "begincolumn"); + lua_getfield(L, 1, "endline"); + lua_getfield(L, 1, "endcolumn"); + + double beginline = luaL_checknumber(L, 2); + double begincolumn = luaL_checknumber(L, 3); + double endline = luaL_checknumber(L, 4); + double endcolumn = luaL_checknumber(L, 5); + + Span* span = static_cast(lua_newuserdatatagged(L, sizeof(Span), kSpanTag)); + + span->beginLine = static_cast(beginline); + span->beginColumn = static_cast(begincolumn); + span->endLine = static_cast(endline); + span->endColumn = static_cast(endcolumn); + + luaL_getmetatable(L, kSpanType); + lua_setmetatable(L, -2); + + return 1; +} + +static int makeSpanLibrary(lua_State* L) +{ + lua_createtable(L, 0, 1); + + lua_pushcfunction(L, luau::createSpan, "create"); + lua_setfield(L, -2, "create"); + + lua_setreadonly(L, -1, 1); + + return 1; +} + +static int indexSpan(lua_State* L) +{ + const Span* span = static_cast(luaL_checkudata(L, 1, kSpanType)); + + const char* fieldName = luaL_checkstring(L, 2); + + if (std::strcmp(fieldName, "beginline") == 0) + { + lua_pushnumber(L, span->beginLine); + return 1; + } + else if (std::strcmp(fieldName, "begincolumn") == 0) + { + lua_pushnumber(L, span->beginColumn); + return 1; + } + else if (std::strcmp(fieldName, "endline") == 0) + { + lua_pushnumber(L, span->endLine); + return 1; + } + else if (std::strcmp(fieldName, "endcolumn") == 0) + { + lua_pushnumber(L, span->endColumn); + return 1; + } + + return 0; +} + +static int ltSpan(lua_State* L) +{ + const Span* lhs = static_cast(luaL_checkudata(L, 1, kSpanType)); + const Span* rhs = static_cast(luaL_checkudata(L, 2, kSpanType)); + + // Compare beginnings, and if they're equal, compare ends + if (lhs->beginLine < rhs->beginLine || (lhs->beginLine == rhs->beginLine && lhs->beginColumn < rhs->beginColumn)) + lua_pushboolean(L, 1); + else if (lhs->beginLine == rhs->beginLine && lhs->beginColumn == rhs->beginColumn) + { + if (lhs->endLine < rhs->endLine || (lhs->endLine == rhs->endLine && lhs->endColumn < rhs->endColumn)) + lua_pushboolean(L, 1); + else + lua_pushboolean(L, 0); + } + else + lua_pushboolean(L, 0); + + return 1; +} + struct AstSerialize : public Luau::AstVisitor { lua_State* L; @@ -293,28 +383,19 @@ struct AstSerialize : public Luau::AstVisitor return {std::vector(trivia.begin(), middleIter), std::vector(middleIter, trivia.end())}; } - void serialize(Luau::Position position) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - - lua_pushnumber(L, position.line); - lua_setfield(L, -2, "line"); - - lua_pushnumber(L, position.column); - lua_setfield(L, -2, "column"); - } - void serialize(Luau::Location location) { lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - serialize(location.begin); - lua_setfield(L, -2, "begin"); + Span* span = static_cast(lua_newuserdatatagged(L, sizeof(Span), kSpanTag)); + + span->beginLine = location.begin.line + 1; + span->beginColumn = location.begin.column + 1; + span->endLine = location.end.line + 1; + span->endColumn = location.end.column + 1; - serialize(location.end); - lua_setfield(L, -2, "end"); + luaL_getmetatable(L, kSpanType); + lua_setmetatable(L, -2); } void serialize(Luau::AstName& name) @@ -333,7 +414,12 @@ struct AstSerialize : public Luau::AstVisitor if (lua_isnil(L, -1)) { lua_pop(L, 1); - lua_createtable(L, 0, 4); + lua_createtable(L, 0, 6); + + lua_pushlstring(L, "local", 5); + lua_setfield(L, -2, "kind"); + + withLocation(local->location); // set up reference for this local into the local table lua_pushlightuserdata(L, local); @@ -376,21 +462,32 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); + if (item.kind == Luau::AstExprTable::Item::List) { - lua_createtable(L, 0, 3); + lua_createtable(L, 0, 5); lua_pushstring(L, "list"); lua_setfield(L, -2, "kind"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istableitem"); + + withLocation(Luau::Location{item.value->location.begin, item.value->location.end}); + visit(item.value); lua_setfield(L, -2, "value"); } else if (item.kind == Luau::AstExprTable::Item::Record) { - lua_createtable(L, 0, 5); + lua_createtable(L, 0, 7); lua_pushstring(L, "record"); lua_setfield(L, -2, "kind"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istableitem"); + + withLocation(Luau::Location{item.key->location.begin, item.value->location.end}); + const auto& value = item.key->as()->value; serializeToken(item.key->location.begin, std::string(value.data, value.size).data()); lua_setfield(L, -2, "key"); @@ -404,20 +501,25 @@ struct AstSerialize : public Luau::AstVisitor } else if (item.kind == Luau::AstExprTable::Item::General) { - lua_createtable(L, 0, 7); + lua_createtable(L, 0, 9); lua_pushstring(L, "general"); lua_setfield(L, -2, "kind"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istableitem"); + LUAU_ASSERT(cstNode->indexerOpenPosition); + withLocation(Luau::Location{*cstNode->indexerOpenPosition, item.value->location.end}); + serializeToken(*cstNode->indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); + lua_setfield(L, -2, "indexeropen"); visit(item.key); lua_setfield(L, -2, "key"); LUAU_ASSERT(cstNode->indexerClosePosition); serializeToken(*cstNode->indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); + lua_setfield(L, -2, "indexerclose"); LUAU_ASSERT(cstNode->equalsPosition); serializeToken(*cstNode->equalsPosition, "="); @@ -449,14 +551,17 @@ struct AstSerialize : public Luau::AstVisitor } // preambleSize should encode the size of the fields we're setting up for _all_ nodes. - static const size_t preambleSize = 2; - void serializeNodePreamble(Luau::AstNode* node, const char* tag) + static const size_t preambleSize = 3; + void serializeNodePreamble(Luau::AstNode* node, const char* tag, const char* kind) { lua_rawcheckstack(L, 2); lua_pushstring(L, tag); lua_setfield(L, -2, "tag"); + lua_pushstring(L, kind); + lua_setfield(L, -2, "kind"); + withLocation(node->location); } @@ -497,7 +602,7 @@ struct AstSerialize : public Luau::AstVisitor void serializeToken(Luau::Position position, const char* text, int nrec = 0) { lua_rawcheckstack(L, 3); - lua_createtable(L, 0, nrec + 4); + lua_createtable(L, 0, nrec + 5); const auto trivia = extractTrivia(position); if (lastTokenRef != LUA_NOREF) @@ -508,7 +613,7 @@ struct AstSerialize : public Luau::AstVisitor LUAU_ASSERT(lua_istable(L, -1)); serializeTrivia(trailingTrivia); - lua_setfield(L, -2, "trailingTrivia"); + lua_setfield(L, -2, "trailingtrivia"); lua_pop(L, 1); lua_unref(L, lastTokenRef); lastTokenRef = LUA_NOREF; @@ -520,17 +625,23 @@ struct AstSerialize : public Luau::AstVisitor serializeTrivia(trivia); } LUAU_ASSERT(lua_istable(L, -2)); - lua_setfield(L, -2, "leadingTrivia"); + lua_setfield(L, -2, "leadingtrivia"); - serialize(position); - lua_setfield(L, -2, "position"); + size_t textLength = strlen(text); - lua_pushstring(L, text); + Luau::Position endPosition{position.line, position.column + static_cast(textLength)}; + serialize(Luau::Location{position, endPosition}); + lua_setfield(L, -2, "location"); + + lua_pushlstring(L, text, textLength); lua_setfield(L, -2, "text"); advancePosition(text); lua_createtable(L, 0, 0); - lua_setfield(L, -2, "trailingTrivia"); + lua_setfield(L, -2, "trailingtrivia"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istoken"); lastTokenRef = lua_ref(L, -1); } @@ -648,19 +759,13 @@ struct AstSerialize : public Luau::AstVisitor } void serializeAttribute(Luau::AstAttr* node) { - switch (node->type) - { - case Luau::AstAttr::Checked: - serializeToken(node->location.begin, "@checked"); - break; - case Luau::AstAttr::Native: - serializeToken(node->location.begin, "@native"); - break; - case Luau::AstAttr::Deprecated: - serializeToken(node->location.begin, "@deprecated"); - break; - } - serializeNodePreamble(node, "attribute"); + serializeToken(node->location.begin, ("@" + std::string(node->name.value)).c_str()); + lua_rawcheckstack(L, 2); + + lua_pushstring(L, "attribute"); + lua_setfield(L, -2, "kind"); + + withLocation(node->location); } void serializeEof(Luau::Position eofPosition) @@ -674,30 +779,30 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprGroup* node) { lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); + lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "group"); + serializeNodePreamble(node, "group", "expr"); serializeToken(node->location.begin, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serialize(Luau::AstExprConstantNil* node) { serializeToken(node->location.begin, "nil", preambleSize); - serializeNodePreamble(node, "nil"); + serializeNodePreamble(node, "nil", "expr"); } void serialize(Luau::AstExprConstantBool* node) { serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); - serializeNodePreamble(node, "boolean"); + serializeNodePreamble(node, "boolean", "expr"); lua_pushboolean(L, node->value); lua_setfield(L, -2, "value"); @@ -708,7 +813,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(node->location.begin, cstNode->value.data, preambleSize + 1); - serializeNodePreamble(node, "number"); + serializeNodePreamble(node, "number", "expr"); lua_pushnumber(L, node->value); lua_setfield(L, -2, "value"); @@ -718,7 +823,7 @@ struct AstSerialize : public Luau::AstVisitor { const auto cstNode = lookupCstNode(node); serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 2); - serializeNodePreamble(node, "string"); + serializeNodePreamble(node, "string", "expr"); switch (cstNode->quoteStyle) { @@ -735,10 +840,10 @@ struct AstSerialize : public Luau::AstVisitor lua_pushstring(L, "interp"); break; } - lua_setfield(L, -2, "quoteStyle"); + lua_setfield(L, -2, "quotestyle"); lua_pushnumber(L, cstNode->blockDepth); - lua_setfield(L, -2, "blockDepth"); + lua_setfield(L, -2, "blockdepth"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually @@ -751,7 +856,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "local"); + serializeNodePreamble(node, "local", "expr"); serializeToken(node->location.begin, node->local->name.value); lua_setfield(L, -2, "token"), @@ -768,7 +873,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 1); - serializeNodePreamble(node, "global"); + serializeNodePreamble(node, "global", "expr"); serializeToken(node->location.begin, node->name.value); lua_setfield(L, -2, "name"); @@ -777,7 +882,7 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprVarargs* node) { serializeToken(node->location.begin, "...", preambleSize); - serializeNodePreamble(node, "vararg"); + serializeNodePreamble(node, "vararg", "expr"); } void serialize(Luau::AstExprCall* node) @@ -787,7 +892,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 6); - serializeNodePreamble(node, "call"); + serializeNodePreamble(node, "call", "expr"); node->func->visit(this); lua_setfield(L, -2, "func"); @@ -796,7 +901,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(*cstNode->openParens, "("); else lua_pushnil(L); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); serializePunctuated(node->args, cstNode->commaPositions, ","); lua_setfield(L, -2, "arguments"); @@ -811,7 +916,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(*cstNode->closeParens, ")"); else lua_pushnil(L); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serialize(Luau::AstExprIndexName* node) @@ -819,7 +924,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "indexname"); + serializeNodePreamble(node, "indexname", "expr"); node->expr->visit(this); lua_setfield(L, -2, "expression"); @@ -830,7 +935,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->indexLocation.begin, node->index.value); lua_setfield(L, -2, "index"); serialize(node->indexLocation); - lua_setfield(L, -2, "indexLocation"); + lua_setfield(L, -2, "indexlocation"); } void serialize(Luau::AstExprIndexExpr* node) @@ -840,19 +945,19 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "index"); + serializeNodePreamble(node, "index", "expr"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(cstNode->openBracketPosition, "["); - lua_setfield(L, -2, "openBrackets"); + lua_setfield(L, -2, "openbrackets"); node->index->visit(this); lua_setfield(L, -2, "index"); serializeToken(cstNode->closeBracketPosition, "]"); - lua_setfield(L, -2, "closeBrackets"); + lua_setfield(L, -2, "closebrackets"); } void serializeFunctionBody(Luau::AstExprFunction* node) @@ -865,17 +970,17 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->openGenericsPosition, "<"); - lua_setfield(L, -2, "openGenerics"); + lua_setfield(L, -2, "opengenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericPacks"); + lua_setfield(L, -2, "genericpacks"); serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closeGenerics"); + lua_setfield(L, -2, "closegenerics"); } if (node->self) @@ -887,7 +992,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->argLocation) { serializeToken(node->argLocation->begin, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); } serializePunctuated(node->args, cstNode->argsCommaPositions, ",", cstNode->argsAnnotationColonPositions); @@ -903,7 +1008,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(cstNode->varargAnnotationColonPosition, ":"); else lua_pushnil(L); - lua_setfield(L, -2, "varargColon"); + lua_setfield(L, -2, "varargcolon"); if (node->varargAnnotation) { @@ -914,31 +1019,31 @@ struct AstSerialize : public Luau::AstVisitor } else lua_pushnil(L); - lua_setfield(L, -2, "varargAnnotation"); + lua_setfield(L, -2, "varargannotation"); if (node->argLocation) { serializeToken(Luau::Position{node->argLocation->end.line, node->argLocation->end.column - 1}, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } if (node->returnAnnotation) serializeToken(cstNode->returnSpecifierPosition, ":"); else lua_pushnil(L); - lua_setfield(L, -2, "returnSpecifier"); + lua_setfield(L, -2, "returnspecifier"); if (node->returnAnnotation) node->returnAnnotation->visit(this); else lua_pushnil(L); - lua_setfield(L, -2, "returnAnnotation"); + lua_setfield(L, -2, "returnannotation"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serialize(Luau::AstExprFunction* node) @@ -946,7 +1051,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "function"); + serializeNodePreamble(node, "function", "expr"); serializeAttributes(node->attributes); lua_setfield(L, -2, "attributes"); @@ -954,7 +1059,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); serializeFunctionBody(node); lua_setfield(L, -2, "body"); @@ -967,10 +1072,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "table"); + serializeNodePreamble(node, "table", "expr"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); + lua_setfield(L, -2, "openbrace"); lua_createtable(L, node->items.size, 0); for (size_t i = 0; i < node->items.size; i++) @@ -981,7 +1086,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "entries"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); + lua_setfield(L, -2, "closebrace"); } void serialize(Luau::AstExprUnary* node) @@ -989,7 +1094,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "unary"); + serializeNodePreamble(node, "unary", "expr"); const auto cstNode = lookupCstNode(node); serializeToken(cstNode->opPosition, toString(node->op).data()); @@ -1004,7 +1109,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "binary"); + serializeNodePreamble(node, "binary", "expr"); node->left->visit(this); lua_setfield(L, -2, "lhsoperand"); @@ -1022,7 +1127,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "cast"); + serializeNodePreamble(node, "cast", "expr"); node->expr->visit(this); lua_setfield(L, -2, "operand"); @@ -1042,10 +1147,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 7); - serializeNodePreamble(node, "conditional"); + serializeNodePreamble(node, "conditional", "expr"); serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifKeyword"); + lua_setfield(L, -2, "ifkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1053,13 +1158,13 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasThen) { serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); node->trueExpr->visit(this); } else lua_pushnil(L); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenexpr"); lua_createtable(L, 0, preambleSize + 4); int i = 0; @@ -1071,7 +1176,7 @@ struct AstSerialize : public Luau::AstVisitor cstNode = lookupCstNode(node); serializeToken(node->location.begin, "elseif"); - lua_setfield(L, -2, "elseifKeyword"); + lua_setfield(L, -2, "elseifkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1079,12 +1184,12 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasThen) { serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); node->trueExpr->visit(this); } else lua_pushnil(L); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenexpr"); lua_rawseti(L, -2, i + 1); i++; @@ -1094,12 +1199,12 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasElse) { serializeToken(cstNode->elsePosition, "else"); - lua_setfield(L, -2, "elseKeyword"); + lua_setfield(L, -2, "elsekeyword"); node->falseExpr->visit(this); } else lua_pushnil(L); - lua_setfield(L, -2, "antecedent"); + lua_setfield(L, -2, "elseexpr"); } void serialize(Luau::AstExprInterpString* node) @@ -1109,7 +1214,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "interpolatedstring"); + serializeNodePreamble(node, "interpolatedstring", "expr"); lua_createtable(L, node->strings.size, 0); lua_createtable(L, node->expressions.size, 0); @@ -1144,7 +1249,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "error"); + serializeNodePreamble(node, "error", "expr"); serializeExprs(node->expressions); lua_setfield(L, -2, "expressions"); @@ -1154,13 +1259,35 @@ struct AstSerialize : public Luau::AstVisitor void serializeStat(Luau::AstStatBlock* node) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 1); + const auto cstNode = cstNodeMap.find(node); + const Luau::CstStatDo* cstDo = cstNode ? (*cstNode)->as() : nullptr; - serializeNodePreamble(node, "block"); + if (cstDo) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); - serializeStats(node->body); - lua_setfield(L, -2, "statements"); + serializeNodePreamble(node, "do", "stat"); + + serializeToken(node->location.begin, "do"); + lua_setfield(L, -2, "dokeyword"); + + serializeStats(node->body); + lua_setfield(L, -2, "body"); + + serializeToken(cstDo->endPosition, "end"); + lua_setfield(L, -2, "endkeyword"); + } + else + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "block", "stat"); + + serializeStats(node->body); + lua_setfield(L, -2, "statements"); + } } void serializeStat(Luau::AstStatIf* node) @@ -1168,19 +1295,19 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 8); - serializeNodePreamble(node, "conditional"); + serializeNodePreamble(node, "conditional", "stat"); serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifKeyword"); + lua_setfield(L, -2, "ifkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); serializeToken(node->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); node->thenbody->visit(this); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenblock"); lua_createtable(L, 0, 0); int i = 0; @@ -1190,16 +1317,16 @@ struct AstSerialize : public Luau::AstVisitor auto elseif = node->elsebody->as(); serializeToken(elseif->location.begin, "elseif"); - lua_setfield(L, -2, "elseifKeyword"); + lua_setfield(L, -2, "elseifkeyword"); elseif->condition->visit(this); lua_setfield(L, -2, "condition"); serializeToken(elseif->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); elseif->thenbody->visit(this); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenblock"); lua_rawseti(L, -2, i + 1); node = elseif; @@ -1211,24 +1338,24 @@ struct AstSerialize : public Luau::AstVisitor { LUAU_ASSERT(node->elseLocation); serializeToken(node->elseLocation->begin, "else"); - lua_setfield(L, -2, "elseKeyword"); + lua_setfield(L, -2, "elsekeyword"); node->elsebody->visit(this); - lua_setfield(L, -2, "antecedent"); + lua_setfield(L, -2, "elseblock"); serializeToken(node->elsebody->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } else { lua_pushnil(L); - lua_setfield(L, -2, "elseKeyword"); + lua_setfield(L, -2, "elsekeyword"); lua_pushnil(L); - lua_setfield(L, -2, "antecedent"); + lua_setfield(L, -2, "elseblock"); serializeToken(node->thenbody->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } } @@ -1237,10 +1364,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 5); - serializeNodePreamble(node, "while"); + serializeNodePreamble(node, "while", "stat"); serializeToken(node->location.begin, "while"); - lua_setfield(L, -2, "whileKeyword"); + lua_setfield(L, -2, "whilekeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1249,13 +1376,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "doKeyword"); + lua_setfield(L, -2, "dokeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serializeStat(Luau::AstStatRepeat* node) @@ -1263,17 +1390,17 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "repeat"); + serializeNodePreamble(node, "repeat", "stat"); serializeToken(node->location.begin, "repeat"); - lua_setfield(L, -2, "repeatKeyword"); + lua_setfield(L, -2, "repeatkeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); auto cstNode = lookupCstNode(node); serializeToken(cstNode->untilPosition, "until"); - lua_setfield(L, -2, "untilKeyword"); + lua_setfield(L, -2, "untilkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1283,14 +1410,14 @@ struct AstSerialize : public Luau::AstVisitor { lua_rawcheckstack(L, 2); serializeToken(node->location.begin, "break", preambleSize); - serializeNodePreamble(node, "break"); + serializeNodePreamble(node, "break", "stat"); } void serializeStat(Luau::AstStatContinue* node) { lua_rawcheckstack(L, 2); serializeToken(node->location.begin, "continue", preambleSize); - serializeNodePreamble(node, "continue"); + serializeNodePreamble(node, "continue", "stat"); } void serializeStat(Luau::AstStatReturn* node) @@ -1298,10 +1425,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "return"); + serializeNodePreamble(node, "return", "stat"); serializeToken(node->location.begin, "return"); - lua_setfield(L, -2, "returnKeyword"); + lua_setfield(L, -2, "returnkeyword"); const auto cstNode = lookupCstNode(node); serializePunctuated(node->list, cstNode->commaPositions, ","); @@ -1313,7 +1440,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 1); - serializeNodePreamble(node, "expression"); + serializeNodePreamble(node, "expression", "stat"); node->expr->visit(this); lua_setfield(L, -2, "expression"); @@ -1324,10 +1451,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "local"); + serializeNodePreamble(node, "local", "stat"); serializeToken(node->location.begin, "local"); - lua_setfield(L, -2, "localKeyword"); + lua_setfield(L, -2, "localkeyword"); const auto cstNode = lookupCstNode(node); serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); @@ -1350,10 +1477,10 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "for"); + serializeNodePreamble(node, "for", "stat"); serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forKeyword"); + lua_setfield(L, -2, "forkeyword"); serialize(node->var, /* createToken= */ true, std::make_optional(cstNode->annotationColonPosition)); lua_setfield(L, -2, "variable"); @@ -1365,7 +1492,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "from"); serializeToken(cstNode->endCommaPosition, ","); - lua_setfield(L, -2, "toComma"); + lua_setfield(L, -2, "tocomma"); node->to->visit(this); lua_setfield(L, -2, "to"); @@ -1373,7 +1500,7 @@ struct AstSerialize : public Luau::AstVisitor if (cstNode->stepCommaPosition) { serializeToken(*cstNode->stepCommaPosition, ","); - lua_setfield(L, -2, "stepComma"); + lua_setfield(L, -2, "stepcomma"); } if (node->step) @@ -1386,13 +1513,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "doKeyword"); + lua_setfield(L, -2, "dokeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serializeStat(Luau::AstStatForIn* node) @@ -1402,10 +1529,10 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "forin"); + serializeNodePreamble(node, "forin", "stat"); serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forKeyword"); + lua_setfield(L, -2, "forkeyword"); serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); lua_setfield(L, -2, "variables"); @@ -1414,7 +1541,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->inLocation.begin, "in"); else lua_pushnil(L); - lua_setfield(L, -2, "inKeyword"); + lua_setfield(L, -2, "inkeyword"); serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); lua_setfield(L, -2, "values"); @@ -1423,13 +1550,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "doKeyword"); + lua_setfield(L, -2, "dokeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serializeStat(Luau::AstStatAssign* node) @@ -1439,7 +1566,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "assign"); + serializeNodePreamble(node, "assign", "stat"); serializePunctuated(node->vars, cstNode->varsCommaPositions, ","); lua_setfield(L, -2, "variables"); @@ -1456,7 +1583,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "compoundassign"); + serializeNodePreamble(node, "compoundassign", "stat"); node->var->visit(this); lua_setfield(L, -2, "variable"); @@ -1474,7 +1601,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "function"); + serializeNodePreamble(node, "function", "stat"); const auto cstNode = lookupCstNode(node); @@ -1482,7 +1609,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "attributes"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); node->name->visit(this); lua_setfield(L, -2, "name"); @@ -1496,7 +1623,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 5); - serializeNodePreamble(node, "localfunction"); + serializeNodePreamble(node, "localfunction", "stat"); serializeAttributes(node->func->attributes); lua_setfield(L, -2, "attributes"); @@ -1504,10 +1631,10 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->localKeywordPosition, "local"); - lua_setfield(L, -2, "localKeyword"); + lua_setfield(L, -2, "localkeyword"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); serialize(node->name); lua_setfield(L, -2, "name"); @@ -1528,7 +1655,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 9); - serializeNodePreamble(node, "typealias"); + serializeNodePreamble(node, "typealias", "stat"); const auto cstNode = lookupCstNode(node); @@ -1539,7 +1666,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "export"); serializeToken(cstNode->typeKeywordPosition, "type"); - lua_setfield(L, -2, "typeToken"); + lua_setfield(L, -2, "typetoken"); serializeToken(node->nameLocation.begin, node->name.value); lua_setfield(L, -2, "name"); @@ -1547,17 +1674,17 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->genericsOpenPosition, "<"); - lua_setfield(L, -2, "openGenerics"); + lua_setfield(L, -2, "opengenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericPacks"); + lua_setfield(L, -2, "genericpacks"); serializeToken(cstNode->genericsClosePosition, ">"); - lua_setfield(L, -2, "closeGenerics"); + lua_setfield(L, -2, "closegenerics"); } serializeToken(cstNode->equalsPosition, "="); @@ -1574,7 +1701,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "typefunction"); + serializeNodePreamble(node, "typefunction", "stat"); if (node->exported) serializeToken(node->location.begin, "export"); @@ -1586,7 +1713,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "type"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); serializeToken(node->nameLocation.begin, node->name.value); lua_setfield(L, -2, "name"); @@ -1615,7 +1742,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "error"); + serializeNodePreamble(node, "error", "stat"); serializeExprs(node->expressions); lua_setfield(L, -2, "expressions"); @@ -1631,7 +1758,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 6); - serializeNodePreamble(node, "reference"); + serializeNodePreamble(node, "reference", "type"); const auto cstNode = node->prefix || node->hasParameterList ? lookupCstNode(node).get() : nullptr; @@ -1644,7 +1771,7 @@ struct AstSerialize : public Luau::AstVisitor LUAU_ASSERT(cstNode); LUAU_ASSERT(cstNode->prefixPointPosition); serializeToken(*cstNode->prefixPointPosition, "."); - lua_setfield(L, -2, "prefixPoint"); + lua_setfield(L, -2, "prefixpoint"); } serializeToken(node->nameLocation.begin, node->name.value); @@ -1654,13 +1781,13 @@ struct AstSerialize : public Luau::AstVisitor { LUAU_ASSERT(cstNode); serializeToken(cstNode->openParametersPosition, "<"); - lua_setfield(L, -2, "openParameters"); + lua_setfield(L, -2, "openparameters"); serializePunctuated(node->parameters, cstNode->parametersCommaPositions, ","); lua_setfield(L, -2, "parameters"); serializeToken(cstNode->closeParametersPosition, ">"); - lua_setfield(L, -2, "closeParameters"); + lua_setfield(L, -2, "closeparameters"); } } @@ -1673,10 +1800,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "array"); + serializeNodePreamble(node, "array", "type"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); + lua_setfield(L, -2, "openbrace"); if (node->indexer->accessLocation) { @@ -1691,7 +1818,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "type"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); + lua_setfield(L, -2, "closebrace"); return; } @@ -1699,10 +1826,10 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "table"); + serializeNodePreamble(node, "table", "type"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); + lua_setfield(L, -2, "openbrace"); lua_createtable(L, cstNode->items.size, 0); const Luau::AstTableProp* prop = node->props.begin(); @@ -1730,13 +1857,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "access"); serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); + lua_setfield(L, -2, "indexeropen"); node->indexer->indexType->visit(this); lua_setfield(L, -2, "key"); serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); + lua_setfield(L, -2, "indexerclose"); serializeToken(item.colonPosition, ":"); lua_setfield(L, -2, "colon"); @@ -1775,12 +1902,15 @@ struct AstSerialize : public Luau::AstVisitor if (item.kind == Luau::CstTypeTable::Item::Kind::StringProperty) { serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); + lua_setfield(L, -2, "indexeropen"); { auto initialPosition = item.stringPosition; serializeToken(item.stringPosition, item.stringInfo->sourceString.data); + lua_pushstring(L, "string"); + lua_setfield(L, -2, "tag"); + switch (item.stringInfo->quoteStyle) { case Luau::CstExprConstantString::QuotedSingle: @@ -1792,7 +1922,7 @@ struct AstSerialize : public Luau::AstVisitor default: LUAU_ASSERT(false); } - lua_setfield(L, -2, "quoteStyle"); + lua_setfield(L, -2, "quotestyle"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually @@ -1806,7 +1936,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "key"); serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); + lua_setfield(L, -2, "indexerclose"); } else { @@ -1833,7 +1963,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "entries"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); + lua_setfield(L, -2, "closebrace"); } void serializeType(Luau::AstTypeFunction* node) @@ -1841,28 +1971,28 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 10); - serializeNodePreamble(node, "function"); + serializeNodePreamble(node, "function", "type"); const auto cstNode = lookupCstNode(node); if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->openGenericsPosition, "<"); - lua_setfield(L, -2, "openGenerics"); + lua_setfield(L, -2, "opengenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericPacks"); + lua_setfield(L, -2, "genericpacks"); serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closeGenerics"); + lua_setfield(L, -2, "closegenerics"); } serializeToken(cstNode->openArgsPosition, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); lua_createtable(L, node->argTypes.types.size, 0); for (size_t i = 0; i < node->argTypes.types.size; i++) @@ -1907,13 +2037,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "vararg"); serializeToken(cstNode->closeArgsPosition, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); serializeToken(cstNode->returnArrowPosition, "->"); - lua_setfield(L, -2, "returnArrow"); + lua_setfield(L, -2, "returnarrow"); node->returnTypes->visit(this); - lua_setfield(L, -2, "returnTypes"); + lua_setfield(L, -2, "returntypes"); } void serializeType(Luau::AstTypeTypeof* node) @@ -1921,20 +2051,20 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "typeof"); + serializeNodePreamble(node, "typeof", "type"); serializeToken(node->location.begin, "typeof"); lua_setfield(L, -2, "typeof"); const auto cstNode = lookupCstNode(node); serializeToken(cstNode->openPosition, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(cstNode->closePosition, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serializeType(Luau::AstTypeUnion* node) @@ -1944,7 +2074,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "union"); + serializeNodePreamble(node, "union", "type"); if (cstNode->leadingPosition) serializeToken(*cstNode->leadingPosition, "|"); @@ -1961,12 +2091,21 @@ struct AstSerialize : public Luau::AstVisitor if (node->types.data[i]->is()) { - serializeToken(node->types.data[i]->location.begin, "?", 1); + serializeToken(node->types.data[i]->location.begin, "?", 2); + lua_pushstring(L, "type"); + lua_setfield(L, -2, "kind"); lua_pushstring(L, "optional"); lua_setfield(L, -2, "tag"); lua_setfield(L, -2, "node"); - lua_pushnil(L); + // Since this option is an optional type, the separator is always present unless it's the last type + if (i < node->types.size - 1 && separatorPositions < cstNode->separatorPositions.size) + { + serializeToken(cstNode->separatorPositions.data[separatorPositions], "|"); + separatorPositions++; + } + else + lua_pushnil(L); lua_setfield(L, -2, "separator"); } else @@ -1974,13 +2113,16 @@ struct AstSerialize : public Luau::AstVisitor node->types.data[i]->visit(this); lua_setfield(L, -2, "node"); + // If the next type is optional, we don't have a separator token if (i < node->types.size - 1 && !node->types.data[i + 1]->is() && separatorPositions < cstNode->separatorPositions.size) + { serializeToken(cstNode->separatorPositions.data[separatorPositions], "|"); + separatorPositions++; + } else lua_pushnil(L); lua_setfield(L, -2, "separator"); - separatorPositions++; } lua_rawseti(L, -2, i + 1); @@ -1995,7 +2137,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "intersection"); + serializeNodePreamble(node, "intersection", "type"); if (cstNode->leadingPosition) serializeToken(*cstNode->leadingPosition, "&"); @@ -2010,7 +2152,7 @@ struct AstSerialize : public Luau::AstVisitor void serializeType(Luau::AstTypeSingletonBool* node) { serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); - serializeNodePreamble(node, "boolean"); + serializeNodePreamble(node, "boolean", "type"); lua_pushboolean(L, node->value); lua_setfield(L, -2, "value"); @@ -2020,7 +2162,7 @@ struct AstSerialize : public Luau::AstVisitor { const auto cstNode = lookupCstNode(node); serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 1); - serializeNodePreamble(node, "string"); + serializeNodePreamble(node, "string", "type"); switch (cstNode->quoteStyle) { @@ -2033,7 +2175,7 @@ struct AstSerialize : public Luau::AstVisitor default: LUAU_ASSERT(false); } - lua_setfield(L, -2, "quoteStyle"); + lua_setfield(L, -2, "quotestyle"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually @@ -2046,16 +2188,16 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "group"); + serializeNodePreamble(node, "group", "type"); serializeToken(node->location.begin, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); node->type->visit(this); lua_setfield(L, -2, "type"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serializeType(Luau::AstGenericType* node) @@ -2063,7 +2205,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "generic"); + serializeNodePreamble(node, "generic", "type"); const auto cstNode = lookupCstNode(node); @@ -2088,7 +2230,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "generic"); + serializeNodePreamble(node, "generic", "typepack"); const auto cstNode = lookupCstNode(node); @@ -2121,7 +2263,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "explicit"); + serializeNodePreamble(node, "explicit", "typepack"); const auto cstNode = lookupCstNode(node); @@ -2129,7 +2271,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(cstNode->openParenthesesPosition, "("); else lua_pushnil(L); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); serializePunctuated(node->typeList.types, cstNode->commaPositions, ","); lua_setfield(L, -2, "types"); @@ -2138,13 +2280,13 @@ struct AstSerialize : public Luau::AstVisitor node->typeList.tailType->visit(this); else lua_pushnil(L); - lua_setfield(L, -2, "tailType"); + lua_setfield(L, -2, "tailtype"); if (cstNode->hasParentheses) serializeToken(cstNode->closeParenthesesPosition, ")"); else lua_pushnil(L); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serializeTypePack(Luau::AstTypePackGeneric* node) @@ -2152,7 +2294,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "generic"); + serializeNodePreamble(node, "generic", "typepack"); serializeToken(node->location.begin, node->genericName.value); lua_setfield(L, -2, "name"); @@ -2167,7 +2309,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "variadic"); + serializeNodePreamble(node, "variadic", "typepack"); if (!forVarArg) serializeToken(node->location.begin, "..."); @@ -2586,7 +2728,7 @@ int luau_parse(lua_State* L) lua_pushnumber(L, serializer.lineOffsets[i]); lua_settable(L, -3); } - lua_setfield(L, -2, "lineOffsets"); + lua_setfield(L, -2, "lineoffsets"); return 1; } @@ -2626,7 +2768,7 @@ int luau_parseexpr(lua_State* L) } AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; - serializer.visit(result.parseResult.expr); + serializer.visit(result.parseResult.root); return 1; } @@ -2661,72 +2803,135 @@ int compile_luau(lua_State* L) std::string bytecode = Luau::compile(std::string(source, source_size), opts); - std::string* userdata = static_cast(lua_newuserdatatagged(L, sizeof(std::string), kCompilerResultTag)); + std::string* userdata = static_cast(lua_newuserdatadtor( + L, + sizeof(std::string), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + )); new (userdata) std::string(std::move(bytecode)); - luaL_getmetatable(L, COMPILE_RESULT_TYPE); + luaL_getmetatable(L, kCompileResultType); lua_setmetatable(L, -2); return 1; } +static int indexCompileResult(lua_State* L) +{ + const std::string* bytecode_string = static_cast(luaL_checkudata(L, 1, kCompileResultType)); + + if (std::strcmp(luaL_checkstring(L, 2), "bytecode") == 0) + { + lua_pushlstring(L, bytecode_string->c_str(), bytecode_string->size()); + + return 1; + } + + return 0; +} + int load_luau(lua_State* L) { - const std::string* bytecode_string = static_cast(luaL_checkudata(L, 1, COMPILE_RESULT_TYPE)); - const char* path = luaL_optlstring(L, 2, "=luau.load", nullptr); - std::string chunk_name = path; - if (chunk_name != "=luau.load") - chunk_name.insert(0, "@"); + const std::string* bytecodeString = static_cast(luaL_checkudata(L, 1, kCompileResultType)); + const char* chunkname = luaL_checkstring(L, 2); + int envIndex = lua_isnoneornil(L, 3) ? 0 : 3; - luau_load(L, chunk_name.c_str(), bytecode_string->c_str(), bytecode_string->length(), lua_gettop(L) > 2 ? 3 : 0); + if (luau_load(L, chunkname, bytecodeString->c_str(), bytecodeString->length(), envIndex) != 0) + lua_error(L); return 1; } -} // namespace luau - -static int index_result(lua_State* L) +int typeofmodule_luau(lua_State* L) { - const std::string* bytecode_string = static_cast(luaL_checkudata(L, 1, COMPILE_RESULT_TYPE)); + std::string modulePath = luaL_checkstring(L, 1); - if (std::strcmp(luaL_checkstring(L, 2), "bytecode") == 0) - { - lua_pushlstring(L, bytecode_string->c_str(), bytecode_string->size()); + Luau::LuteModuleResolver moduleResolver; + Luau::LuteConfigResolver configResolver(Luau::Mode::NoCheck); + Luau::FrontendOptions fopts; + fopts.retainFullTypeGraphs = true; + + Luau::Frontend frontend(&moduleResolver, &configResolver, fopts); + Luau::registerBuiltinGlobals(frontend, frontend.globals); + Luau::freeze(frontend.globals.globalTypes); + frontend.check(modulePath); + + Luau::ModulePtr modulePtr = frontend.moduleResolver.getModule(modulePath); + if (!modulePtr) + { + lua_pushnil(L); return 1; } - return 0; + // For now, we return a string representation of the module's type, but we will expand it to some Luau data structure representation of Type + // (similar to the AST types) in a subsequent PR. + Luau::ToStringOptions opts; + opts.exhaustive = true; + opts.useLineBreaks = true; + opts.functionTypeArguments = true; + opts.scope = modulePtr->getModuleScope(); + + std::string moduleTypeStr = Luau::toString(modulePtr->returnType, opts); + lua_pushlstring(L, moduleTypeStr.c_str(), moduleTypeStr.length()); + return 1; } // perform type mt registration, etc -static int init_luau_lib(lua_State* L) +static int initLuauLibrary(lua_State* L) { - luaL_newmetatable(L, COMPILE_RESULT_TYPE); + luaL_newmetatable(L, kCompileResultType); // Set __type - lua_pushstring(L, "CompilerResult"); + lua_pushstring(L, kCompileResultType); lua_setfield(L, -2, "__type"); - lua_pushcfunction(L, index_result, "CompilerResult.__index"); + lua_pushcfunction(L, luau::indexCompileResult, "CompilerResult.__index"); lua_setfield(L, -2, "__index"); + lua_setreadonly(L, -1, 1); + + lua_pop(L, 1); + + luaL_newmetatable(L, kSpanType); + + // Set __type + lua_pushstring(L, kSpanType); + lua_setfield(L, -2, "__type"); + + lua_pushcfunction(L, luau::indexSpan, "span.__index"); + lua_setfield(L, -2, "__index"); + + lua_pushcfunction(L, luau::ltSpan, "span.__lt"); + lua_setfield(L, -2, "__lt"); + + lua_setreadonly(L, -1, 1); + lua_pop(L, 1); return 1; } +} // namespace luau + int luaopen_luau(lua_State* L) { luaL_register(L, "luau", luau::lib); - return init_luau_lib(L); + return luau::initLuauLibrary(L); } int luteopen_luau(lua_State* L) { - lua_createtable(L, 0, std::size(luau::lib)); + lua_createtable(L, 0, std::size(luau::lib) + std::size(luau::properties)); + + // span library + luau::makeSpanLibrary(L); + lua_setfield(L, -2, luau::kSpanType); for (auto& [name, func] : luau::lib) { @@ -2739,5 +2944,5 @@ int luteopen_luau(lua_State* L) lua_setreadonly(L, -1, 1); - return init_luau_lib(L); + return luau::initLuauLibrary(L); } diff --git a/lute/luau/src/moduleresolver.cpp b/lute/luau/src/moduleresolver.cpp new file mode 100644 index 000000000..0f141b1e7 --- /dev/null +++ b/lute/luau/src/moduleresolver.cpp @@ -0,0 +1,40 @@ +#include "lute/moduleresolver.h" + +#include "lute/resolverequire.h" + +#include "Luau/Ast.h" +#include "Luau/FileUtils.h" + +namespace Luau +{ + +std::optional LuteModuleResolver::readSource(const Luau::ModuleName& name) +{ + if (std::optional source = readFile(name)) + return Luau::SourceCode{*source, Luau::SourceCode::Module}; + return std::nullopt; +} + +// We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. +std::optional LuteModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) +{ + if (auto expr = node->as()) + { + std::string requirePath(expr->value.data, expr->value.size); + + std::string error; + std::string requirerChunkname = "@" + context->name; + std::optional absolutePath = resolveRequire(requirePath, std::move(requirerChunkname), &error); + if (!absolutePath) + { + printf("Failed to resolve require: %s\n", error.c_str()); + return std::nullopt; + } + + return Luau::ModuleInfo{*absolutePath}; + } + + return std::nullopt; +} + +} // namespace Luau diff --git a/lute/luau/src/resolverequire.cpp b/lute/luau/src/resolverequire.cpp new file mode 100644 index 000000000..0c885d0c4 --- /dev/null +++ b/lute/luau/src/resolverequire.cpp @@ -0,0 +1,182 @@ +#include "lute/resolverequire.h" + +#include "lute/filevfs.h" +#include "lute/modulepath.h" + +#include "Luau/Common.h" +#include "Luau/RequireNavigator.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include + +// FileVfsContext +class FileVfsContext : public Luau::Require::NavigationContext +{ +public: + FileVfsContext(std::string requirerChunkname); + + std::string getRequirerIdentifier() const override; + + NavigateResult reset(const std::string& identifier) override; + NavigateResult jumpToAlias(const std::string& path) override; + + NavigateResult toParent() override; + NavigateResult toChild(const std::string& component) override; + + ConfigStatus getConfigStatus() const override; + + ConfigBehavior getConfigBehavior() const override; + std::optional getAlias(const std::string& alias) const override; + std::optional getConfig() const override; + + FileVfs vfs; + std::string requirerChunkname; +}; + +using NC = Luau::Require::NavigationContext; + +static NC::NavigateResult convert(NavigationStatus status) +{ + NC::NavigateResult result = NC::NavigateResult::NotFound; + switch (status) + { + case NavigationStatus::Success: + result = NC::NavigateResult::Success; + break; + case NavigationStatus::Ambiguous: + result = NC::NavigateResult::Ambiguous; + break; + case NavigationStatus::NotFound: + result = NC::NavigateResult::NotFound; + break; + } + return result; +} + +static NC::ConfigStatus convert(ConfigStatus status) +{ + NC::ConfigStatus result = NC::ConfigStatus::Ambiguous; + switch (status) + { + case ConfigStatus::Absent: + result = NC::ConfigStatus::Absent; + break; + case ConfigStatus::Ambiguous: + result = NC::ConfigStatus::Ambiguous; + break; + case ConfigStatus::PresentJson: + result = NC::ConfigStatus::PresentJson; + break; + case ConfigStatus::PresentLuau: + result = NC::ConfigStatus::PresentLuau; + break; + } + return result; +} + +FileVfsContext::FileVfsContext(std::string requirerChunkname) + : requirerChunkname(std::move(requirerChunkname)) +{ +} + +std::string FileVfsContext::getRequirerIdentifier() const +{ + return requirerChunkname; +} + +NC::NavigateResult FileVfsContext::reset(const std::string& identifier) +{ + return convert(vfs.resetToPath(identifier)); +} + +NC::NavigateResult FileVfsContext::jumpToAlias(const std::string& path) +{ + return convert(vfs.resetToPath(path)); +} + +NC::NavigateResult FileVfsContext::toParent() +{ + return convert(vfs.toParent()); +} + +NC::NavigateResult FileVfsContext::toChild(const std::string& component) +{ + return convert(vfs.toChild(component)); +} + +NC::ConfigStatus FileVfsContext::getConfigStatus() const +{ + return convert(vfs.getConfigStatus()); +} + +NC::ConfigBehavior FileVfsContext::getConfigBehavior() const +{ + return NC::ConfigBehavior::GetConfig; +} + +std::optional FileVfsContext::getAlias(const std::string& alias) const +{ + return std::nullopt; +} + +std::optional FileVfsContext::getConfig() const +{ + return vfs.getConfig(); +} + +// ErrorCapturer +class ErrorCapturer : public Luau::Require::ErrorHandler +{ +public: + void reportError(std::string message) override; + std::optional error = std::nullopt; +}; + +void ErrorCapturer::reportError(std::string message) +{ + error = message; +} + +// Public API +std::optional resolveRequire(std::string requirePath, std::string requirerChunkname, std::string* error) +{ + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + { + if (error) + *error = "requirer chunkname must start with '@'"; + return std::nullopt; + } + + FileVfsContext context{requirerChunkname.substr(1)}; + ErrorCapturer errorCapturer{}; + + Luau::Require::Navigator navigator{context, errorCapturer}; + Luau::Require::Navigator::Status status = navigator.navigate(requirePath); + + if (status == Luau::Require::Navigator::Status::ErrorReported) + { + if (error && errorCapturer.error) + *error = *errorCapturer.error; + return std::nullopt; + } + + std::string absolutePath = context.vfs.getAbsoluteFilePath(); + return absolutePath; +} + +int resolverequire_luau(lua_State* L) +{ + std::string requirePath = luaL_checkstring(L, 1); + std::string requirerChunkname = luaL_checkstring(L, 2); + + std::string error; + std::optional absolutePath = resolveRequire(requirePath, requirerChunkname, &error); + if (!absolutePath) + luaL_error(L, "%s", error.c_str()); + + lua_pushlstring(L, absolutePath->c_str(), absolutePath->size()); + return 1; +} diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 6f8faa077..6314674b6 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -2,29 +2,39 @@ #include "lute/runtime.h" -#include "curl/curl.h" -#include "App.h" #include "Luau/DenseHash.h" #include "Luau/Variant.h" #include "lua.h" #include "lualib.h" +#include "curl/curl.h" +#include "uv.h" + +#include +#include #include #include #include +#include "App.h" +#include "Loop.h" + namespace net { static const std::string kEmptyHeaderKey = ""; -struct CurlResponse { +struct CurlResponse +{ std::string error; std::vector body; Luau::DenseHashMap headers; long status = 0; - CurlResponse() : headers(kEmptyHeaderKey) {} + CurlResponse() + : headers(kEmptyHeaderKey) + { + } }; static size_t writeFunction(void* contents, size_t size, size_t nmemb, void* context) @@ -61,12 +71,16 @@ static CurlResponse requestData( curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); if (method != "GET") curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); if (!body.empty()) + { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); + } if (!headers.empty()) { @@ -133,7 +147,11 @@ int request(lua_State* L) lua_getfield(L, 2, "body"); if (lua_isstring(L, -1)) - body = lua_tostring(L, -1); + { + size_t len; + const char* data = lua_tolstring(L, -1, &len); + body.assign(data, data + len); + } lua_pop(L, 1); lua_getfield(L, 2, "headers"); @@ -166,7 +184,7 @@ int request(lua_State* L) token->fail("network request failed: " + resp.error); return; } - + token->complete( [resp = std::move(resp)](lua_State* L) { @@ -340,7 +358,11 @@ static void handleResponse(auto* res, lua_State* L, int responseIndex) lua_pop(L, 1); lua_getfield(L, responseIndex, "body"); - std::string body = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + + std::string body = ""; + size_t bodyLength; + const char* bodyData = lua_tolstring(L, -1, &bodyLength); + body.assign(bodyData, bodyData + bodyLength); lua_pop(L, 1); res->end(body); @@ -508,7 +530,9 @@ bool closeServer(int serverId) int lua_serve(lua_State* L) { - std::string hostname = "0.0.0.0"; + uWS::Loop::get(uv_default_loop()); + + std::string hostname = "127.0.0.1"; int port = 3000; bool reusePort = false; std::optional tlsOptions; @@ -631,28 +655,9 @@ int lua_serve(lua_State* L) return 0; } - state->loopFunction = [state]() - { - if (!state->running) - { - return; - } - Luau::visit( - [](auto* appPtr) - { - if (appPtr) - appPtr->run(); - }, - state->app - ); - state->runtime->schedule(state->loopFunction); - }; - serverInstances[serverId] = std::move(app); serverStates[serverId] = state; - runtime->schedule(state->loopFunction); - lua_createtable(L, 0, 3); lua_pushstring(L, "hostname"); diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index 88c888fd7..d163b5ff1 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -3,6 +3,9 @@ #include "lua.h" #include "lualib.h" +#include +#include + // open the library as a standard global luau library int luaopen_process(lua_State* L); // open the library as a table on top of the stack @@ -12,17 +15,23 @@ namespace process { int run(lua_State* L); +int system(lua_State* L); int homedir(lua_State* L); int cwd(lua_State* L); int exitFunc(lua_State* L); +std::optional getExecPath(std::string* error); +int execpath(lua_State* L); + static const luaL_Reg lib[] = { {"run", run}, + {"system", system}, {"homedir", homedir}, {"cwd", cwd}, {"exit", exitFunc}, + {"execpath", execpath}, {nullptr, nullptr} }; diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 7ec3cdcec..701513fca 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -1,19 +1,43 @@ #include "lute/process.h" + #include "lute/runtime.h" -#include -#include -#include -#include -#include -#include +#include "lute/uvutils.h" + #include "Luau/Common.h" #include "lua.h" #include "lualib.h" +#include "uv.h" + +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#ifdef PATH_MAX +#define LUTE_PATH_MAX PATH_MAX +#else +#define LUTE_PATH_MAX 8192 +#endif + namespace process { +void convertCRLFtoLF(std::string& str) +{ + size_t writePos = 0; + for (size_t readPos = 0; readPos < str.size(); ++readPos) + { + if (str[readPos] == '\r' && readPos + 1 < str.size() && str[readPos + 1] == '\n') + continue; // Skip the '\r' in CRLF + str[writePos++] = str[readPos]; + } + str.resize(writePos); +} + struct ProcessHandle { uv_process_t process; @@ -29,8 +53,6 @@ struct ProcessHandle std::shared_ptr self; std::atomic pendingCloses{0}; - ~ProcessHandle() {} - void closeHandles() { auto closeCb = [](uv_handle_t* handle) @@ -42,19 +64,19 @@ struct ProcessHandle } }; - if (stdoutPipe.loop && uv_is_active((uv_handle_t*)&stdoutPipe)) + if (!uv_is_closing((uv_handle_t*)&stdoutPipe)) { pendingCloses++; uv_read_stop((uv_stream_t*)&stdoutPipe); uv_close((uv_handle_t*)&stdoutPipe, closeCb); } - if (stderrPipe.loop && uv_is_active((uv_handle_t*)&stderrPipe)) + if (!uv_is_closing((uv_handle_t*)&stderrPipe)) { pendingCloses++; uv_read_stop((uv_stream_t*)&stderrPipe); uv_close((uv_handle_t*)&stderrPipe, closeCb); } - if (process.loop) + if (!uv_is_closing((uv_handle_t*)&process)) { pendingCloses++; uv_close((uv_handle_t*)&process, closeCb); @@ -86,7 +108,8 @@ struct ProcessHandle std::string finalStdout = stdoutData; std::string finalStderr = stderrData; std::string finalSignalStr = finalTermSignal ? std::to_string(finalTermSignal) : ""; - + convertCRLFtoLF(finalStdout); + convertCRLFtoLF(finalStderr); resumeToken->complete( [=](lua_State* L) { @@ -108,7 +131,7 @@ struct ProcessHandle if (!finalSignalStr.empty()) { - lua_pushstring(L, finalSignalStr.c_str()); + lua_pushlstring(L, finalSignalStr.c_str(), finalSignalStr.size()); } else { @@ -129,6 +152,14 @@ struct ProcessHandle } }; +struct ProcessOptions +{ + std::string cwd; + std::string stdioKind; + std::map env; + std::string customShell; // only used by system() +}; + static void onProcessExit(uv_process_t* process, int64_t exitStatus, int termSignal) { ProcessHandle* handle = static_cast(process->data); @@ -190,110 +221,9 @@ const std::string kStdioKindNone = "none"; // TODO: add forwarding // const std::string kStdioKindForward = "forward"; -int run(lua_State* L) +// helper function for run() and system() +int executionHelper(lua_State* L, std::vector args, ProcessOptions opts) { - std::vector args; - if (lua_istable(L, 1)) - { - int len = lua_objlen(L, 1); - for (int i = 1; i <= len; i++) - { - lua_rawgeti(L, 1, i); - args.push_back(lua_tostring(L, -1)); - lua_pop(L, 1); - } - } - else - { - args.push_back(lua_tostring(L, 1)); - } - - if (args.empty() || args[0].empty()) - { - luaL_error(L, "process.create requires a non-empty command"); - return 0; - } - - bool useShell = false; - std::string customShell; - std::string cwd; - std::string stdioKind; - std::map env; - - if (lua_istable(L, 2)) - { - lua_getfield(L, 2, "shell"); - if (lua_isboolean(L, -1)) - { - useShell = lua_toboolean(L, -1); - } - else if (lua_isstring(L, -1)) - { - customShell = lua_tostring(L, -1); - useShell = true; - } - - lua_pop(L, 1); - - lua_getfield(L, 2, "cwd"); - if (!lua_isnil(L, -1)) - cwd = lua_tostring(L, -1); - - lua_pop(L, 1); - - lua_getfield(L, 2, "stdio"); - if (lua_isstring(L, -1)) - { - stdioKind = lua_tostring(L, -1); - // TODO: support stdin and separate stdout/stderr kinds - } - lua_pop(L, 1); - - lua_getfield(L, 2, "env"); - if (lua_istable(L, -1)) - { - lua_pushnil(L); - while (lua_next(L, -2)) - { - env[luaL_checkstring(L, -2)] = luaL_checkstring(L, -1); - lua_pop(L, 1); - } - } - lua_pop(L, 1); - } - - if (useShell) - { - std::string commandStr = args[0]; - - for (size_t i = 1; i < args.size(); ++i) - { - commandStr += " "; - commandStr += args[i]; - } - -#ifdef _WIN32 - const char* shellVar = "COMSPEC"; - const char* shellFallback = "cmd.exe"; - const char* shellArg = "/c"; -#else - const char* shellVar = "SHELL"; - const char* shellFallback = "/bin/sh"; - const char* shellArg = "-c"; -#endif - - const char* shell = customShell.empty() ? nullptr : customShell.c_str(); - if (!shell) - { - char shellBuffer[1024]; - size_t shellSize = sizeof(shellBuffer); - int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); - shell = result == 0 ? shellBuffer : shellFallback; - } - - args = {shell, shellArg, commandStr}; - } - auto handle = std::make_shared(); handle->loop = uv_default_loop(); handle->self = handle; @@ -312,7 +242,7 @@ int run(lua_State* L) std::vector envStrings; std::vector envPtr; - if (!env.empty()) + if (!opts.env.empty()) { // Copy current environment into the new environment uv_env_item_t* currentEnvItems; @@ -320,20 +250,22 @@ int run(lua_State* L) int err = uv_os_environ(¤tEnvItems, ¤tEnvCount); if (err != 0) { + uv_os_free_environ(currentEnvItems, currentEnvCount); luaL_error(L, "Failed to get current environment: %s", uv_strerror(err)); - return 0; } for (int i = 0; i < currentEnvCount; i++) { - if (currentEnvItems[i].name && currentEnvItems[i].value && env.find(currentEnvItems[i].name) == env.end()) + if (currentEnvItems[i].name && currentEnvItems[i].value && opts.env.find(currentEnvItems[i].name) == opts.env.end()) { - env[currentEnvItems[i].name] = currentEnvItems[i].value; + opts.env[currentEnvItems[i].name] = currentEnvItems[i].value; } } + uv_os_free_environ(currentEnvItems, currentEnvCount); + // Turn the new environment into a char** array - envStrings.reserve(env.size()); - envPtr.reserve(env.size() + 1); - for (const auto& pair : env) + envStrings.reserve(opts.env.size()); + envPtr.reserve(opts.env.size() + 1); + for (const auto& pair : opts.env) { envStrings.push_back(pair.first + "=" + pair.second); } @@ -345,9 +277,9 @@ int run(lua_State* L) options.env = envPtr.data(); } - if (!cwd.empty()) + if (!opts.cwd.empty()) { - options.cwd = cwd.c_str(); + options.cwd = opts.cwd.c_str(); } uv_pipe_init(handle->loop, &handle->stdoutPipe, 0); @@ -356,19 +288,19 @@ int run(lua_State* L) options.stdio_count = 3; uv_stdio_container_t stdio[3]; stdio[0].flags = UV_IGNORE; - if (stdioKind == kStdioKindNone) + if (opts.stdioKind == kStdioKindNone) { stdio[1].flags = UV_IGNORE; stdio[2].flags = UV_IGNORE; } - else if (stdioKind == kStdioKindInherit) + else if (opts.stdioKind == kStdioKindInherit) { stdio[1].flags = UV_INHERIT_FD; stdio[1].data.fd = fileno(stdout); stdio[2].flags = UV_INHERIT_FD; stdio[2].data.fd = fileno(stderr); } - else if (stdioKind == kStdioKindDefault || stdioKind.empty()) + else if (opts.stdioKind == kStdioKindDefault || opts.stdioKind.empty()) { stdio[1].flags = static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); stdio[1].data.stream = (uv_stream_t*)&handle->stdoutPipe; @@ -377,8 +309,7 @@ int run(lua_State* L) } else { - luaL_error(L, "Invalid stdio kind: %s", stdioKind.c_str()); - return 0; + luaL_error(L, "Invalid stdio kind: %s", opts.stdioKind.c_str()); } options.stdio = stdio; @@ -400,7 +331,6 @@ int run(lua_State* L) handle->closeHandles(); luaL_error(L, "Failed to spawn process: %s", uv_strerror(spawnResult)); - return 0; } uv_read_start((uv_stream_t*)&handle->stdoutPipe, allocBuffer, onPipeRead); @@ -409,30 +339,136 @@ int run(lua_State* L) return lua_yield(L, 0); } -int homedir(lua_State* L) +ProcessOptions parseOptions(lua_State* L, int index) { - std::string buffer; + ProcessOptions opts; + + if (lua_isnoneornil(L, index)) + { + return opts; // use defaults + } - size_t homedir_size = 255; - buffer.reserve(homedir_size); + if (!lua_istable(L, index)) + { + luaL_error(L, "process options must be a table"); + } - int status = uv_os_homedir(buffer.data(), &homedir_size); - if (status == UV_ENOBUFS) + lua_getfield(L, index, "system"); + if (!lua_isnil(L, -1)) { - // libuv gives us the new size if it's under sized - buffer.reserve(homedir_size); + opts.customShell = luaL_checkstring(L, -1); + } + lua_pop(L, 1); - status = uv_os_homedir(buffer.data(), &homedir_size); + lua_getfield(L, index, "cwd"); + if (!lua_isnil(L, -1)) + { + opts.cwd = luaL_checkstring(L,-1); } + lua_pop(L, 1); - if (status != 0) + lua_getfield(L, index, "stdio"); + if (!lua_isnil(L, -1)) { - luaL_error(L, "failed to get home directory"); - return 1; + opts.stdioKind = luaL_checkstring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, index, "env"); + if (!lua_isnil(L, -1)) + { + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + opts.env[luaL_checkstring(L, -2)] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + } + else + { + luaL_error(L, "process option 'env' must be a table"); + } + } + lua_pop(L, 1); + + return opts; +} + +int run(lua_State* L) +{ + if (!lua_istable(L, 1)) + { + luaL_error(L, "process.run expects a table of arguments as the first parameter"); } - lua_pushstring(L, buffer.c_str()); + std::vector args; + int len = lua_objlen(L, 1); + for (int i = 1; i <= len; i++) + { + lua_rawgeti(L, 1, i); + args.push_back(luaL_checkstring(L, -1)); + lua_pop(L, 1); + } + if (args.empty()) + { + luaL_error(L, "process.run requires a non-empty table of arguments"); + } + if (args[0].empty()) + { + luaL_error(L, "process.run requires a non-empty command as the first argument"); + } + + ProcessOptions opts = parseOptions(L, 2); + return executionHelper(L, args, opts); +} + +int system(lua_State* L) +{ + std::string command = luaL_checkstring(L, 1); + if (command.empty()) + { + luaL_error(L, "process.system requires a non-empty string as the command"); + } + + ProcessOptions opts = parseOptions(L, 2); + +#ifdef _WIN32 + const char* shellVar = "COMSPEC"; + const char* shellFallback = "cmd.exe"; + const char* shellArg = "/c"; +#else + const char* shellVar = "SHELL"; + const char* shellFallback = "/bin/sh"; + const char* shellArg = "-c"; +#endif + + std::string resolvedShell; + if (opts.customShell.empty()) + { + char shellBuffer[1024]; + size_t shellSize = sizeof(shellBuffer); + int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); + resolvedShell = result == 0 ? shellBuffer : shellFallback; + } + else + { + resolvedShell = opts.customShell; + } + + return executionHelper(L, { resolvedShell, shellArg, command }, opts); +} + +int homedir(lua_State* L) +{ + auto result = uvutils::getStringFromUv(uv_os_homedir); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get home directory: %s", error->toString().c_str()); + + std::string* homeDir = result.get_if(); + lua_pushlstring(L, homeDir->c_str(), homeDir->size()); return 1; } @@ -440,7 +476,6 @@ int exitFunc(lua_State* L) { int exitCode = luaL_optinteger(L, 1, 0); - // Exit with the provided code std::exit(exitCode); @@ -449,30 +484,47 @@ int exitFunc(lua_State* L) int cwd(lua_State* L) { - std::string buffer; + auto result = uvutils::getStringFromUv(uv_cwd); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get current working directory: %s", error->toString().c_str()); - size_t cwd_size = 255; - buffer.reserve(cwd_size); + std::string* cwd = result.get_if(); + lua_pushlstring(L, cwd->c_str(), cwd->size()); + return 1; +}; - int status = uv_cwd(buffer.data(), &cwd_size); - if (status == UV_ENOBUFS) - { - // libuv gives us the new size if it's under sized - buffer.reserve(cwd_size); +std::optional getExecPath(std::string* error) +{ + // Executable path is not expected to change during process lifetime, so we + // can safely cache it after the first retrieval. + static std::optional cachedPath = std::nullopt; + if (cachedPath) + return *cachedPath; - status = uv_cwd(buffer.data(), &cwd_size); - } + char buf[LUTE_PATH_MAX]; + size_t len = sizeof(buf); - if (status != 0) + if (int status = uv_exepath(buf, &len); status < 0) { - luaL_error(L, "failed to get current working directory"); - return 1; + if (error) + *error = uv_strerror(status); + return std::nullopt; } - lua_pushstring(L, buffer.c_str()); + cachedPath = std::string(buf, len); + return *cachedPath; +} + +int execpath(lua_State* L) +{ + std::string error; + std::optional execPath = getExecPath(&error); + if (!execPath) + luaL_error(L, "Failed to get executable path: %s", error.c_str()); + lua_pushlstring(L, execPath->c_str(), execPath->size()); return 1; -}; +} static int envIndex(lua_State* L) { @@ -557,8 +609,9 @@ static int envIterNext(lua_State* L) return 0; } - lua_pushstring(L, iter->items[iter->index].name); - lua_pushstring(L, iter->items[iter->index].value); + uv_env_item_t item = iter->items[iter->index]; + lua_pushstring(L, item.name); + lua_pushstring(L, item.value); iter->index++; return 2; } @@ -580,7 +633,7 @@ static int envIter(lua_State* L) sizeof(EnvIter), [](void* ptr) { - static_cast(ptr)->~EnvIter(); + std::destroy_at(static_cast(ptr)); } ); diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 0059b866c..d5bd07276 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -1,24 +1,32 @@ add_library(Lute.Require) target_sources(Lute.Require PRIVATE + include/lute/bundlevfs.h include/lute/clivfs.h include/lute/filevfs.h + include/lute/lutevfs.h include/lute/modulepath.h include/lute/options.h + include/lute/packagerequirevfs.h include/lute/require.h include/lute/requirevfs.h include/lute/stdlibvfs.h + include/lute/userlandvfs.h + src/bundlevfs.cpp src/clivfs.cpp src/filevfs.cpp + src/lutevfs.cpp src/modulepath.cpp src/options.cpp + src/packagerequirevfs.cpp src/require.cpp src/requirevfs.cpp src/stdlibvfs.cpp + src/userlandvfs.cpp ) target_compile_features(Lute.Require PUBLIC cxx_std_17) target_include_directories(Lute.Require PUBLIC include) -target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands) +target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) target_compile_options(Lute.Require PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/require/include/lute/bundlevfs.h b/lute/require/include/lute/bundlevfs.h new file mode 100644 index 000000000..601c19d75 --- /dev/null +++ b/lute/require/include/lute/bundlevfs.h @@ -0,0 +1,32 @@ +#pragma once + +#include "lute/modulepath.h" + +#include "Luau/DenseHash.h" + +#include +#include +#include + +class BundleVfs +{ +public: + BundleVfs(Luau::DenseHashMap luaurcFiles, Luau::DenseHashMap bundleMap); + + NavigationStatus resetToPath(const std::string& path); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + bool isModulePresent() const; + std::string getIdentifier() const; + std::optional getContents(const std::string& path) const; + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + +private: + const Luau::DenseHashMap filePathToBytecode; + const Luau::DenseHashMap luaurcFiles; + std::optional modulePath; +}; diff --git a/lute/require/include/lute/clivfs.h b/lute/require/include/lute/clivfs.h index 4541a5680..c1c33b20b 100644 --- a/lute/require/include/lute/clivfs.h +++ b/lute/require/include/lute/clivfs.h @@ -16,7 +16,7 @@ class CliVfs std::string getIdentifier() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/include/lute/filevfs.h b/lute/require/include/lute/filevfs.h index 687a46bbe..ce601a53d 100644 --- a/lute/require/include/lute/filevfs.h +++ b/lute/require/include/lute/filevfs.h @@ -18,7 +18,7 @@ class FileVfs std::string getAbsoluteFilePath() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/include/lute/lutevfs.h b/lute/require/include/lute/lutevfs.h new file mode 100644 index 000000000..bc586021a --- /dev/null +++ b/lute/require/include/lute/lutevfs.h @@ -0,0 +1,30 @@ +#pragma once + +#include "lute/modulepath.h" + +#include "Luau/DenseHash.h" + +#include "lua.h" + +#include + +extern const Luau::DenseHashMap kLuteModules; + +class LuteVfs +{ +public: + NavigationStatus resetToPath(const std::string& path); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + bool isModulePresent() const; + std::string getIdentifier() const; + std::optional getContents(const std::string& path) const; + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + +private: + std::optional modulePath; +}; diff --git a/lute/require/include/lute/modulepath.h b/lute/require/include/lute/modulepath.h index c6c0ca667..f671010d4 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -10,11 +11,26 @@ enum class NavigationStatus NotFound }; +enum class ConfigStatus +{ + Absent, + Ambiguous, + PresentJson, + PresentLuau +}; + struct ResolvedRealPath { + enum class PathType + { + File, + Directory + }; + NavigationStatus status; std::string realPath; std::optional relativePath; + PathType type; }; class ModulePath @@ -29,13 +45,13 @@ class ModulePath static std::optional create( std::string rootDirectory, std::string filePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack = std::nullopt ); ResolvedRealPath getRealPath() const; - std::string getPotentialLuaurcPath() const; + std::string getPotentialConfigPath(const std::string& name) const; NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); @@ -44,13 +60,13 @@ class ModulePath ModulePath( std::string realPathPrefix, std::string modulePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack = std::nullopt ); - bool (*isAFile)(const std::string&); - bool (*isADirectory)(const std::string&); + std::function isAFile; + std::function isADirectory; std::string realPathPrefix; std::string modulePath; diff --git a/lute/require/include/lute/packagerequirevfs.h b/lute/require/include/lute/packagerequirevfs.h new file mode 100644 index 000000000..023aee368 --- /dev/null +++ b/lute/require/include/lute/packagerequirevfs.h @@ -0,0 +1,56 @@ +#pragma once + +#include "lute/lutevfs.h" +#include "lute/require.h" +#include "lute/stdlibvfs.h" +#include "lute/userlandvfs.h" + +namespace Package +{ + +class RequireVfs : public IRequireVfs +{ +public: + RequireVfs(UserlandVfs); + + bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const override; + + NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; + NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + NavigationStatus toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) override; + NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) override; + + NavigationStatus toParent(lua_State* L) override; + NavigationStatus toChild(lua_State* L, std::string_view name) override; + + bool isModulePresent(lua_State* L) const override; + std::string getContents(lua_State* L, const std::string& loadname) const override; + + std::string getChunkname(lua_State* L) const override; + std::string getLoadname(lua_State* L) const override; + std::string getCacheKey(lua_State* L) const override; + + ConfigStatus getConfigStatus(lua_State* L) const override; + std::string getConfig(lua_State* L) const override; + + bool isPrecompiled() const override + { + return false; + } + +private: + enum class VFSType + { + Userland, + Std, + Lute, + }; + + VFSType vfsType = VFSType::Userland; + + Package::UserlandVfs userlandVfs; + StdLibVfs stdLibVfs; + LuteVfs luteVfs; +}; + +} // namespace Package diff --git a/lute/require/include/lute/require.h b/lute/require/include/lute/require.h index 00facfc6d..911da889f 100644 --- a/lute/require/include/lute/require.h +++ b/lute/require/include/lute/require.h @@ -1,18 +1,45 @@ #pragma once -#include "lute/clivfs.h" -#include "lute/requirevfs.h" +#include "lute/modulepath.h" #include "Luau/Require.h" +#include #include void requireConfigInit(luarequire_Configuration* config); +class IRequireVfs +{ +public: + virtual ~IRequireVfs() = default; + + virtual bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const = 0; + + virtual NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) = 0; + virtual NavigationStatus jumpToAlias(lua_State* L, std::string_view path) = 0; + virtual NavigationStatus toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) = 0; + virtual NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) = 0; + + virtual NavigationStatus toParent(lua_State* L) = 0; + virtual NavigationStatus toChild(lua_State* L, std::string_view name) = 0; + + virtual bool isModulePresent(lua_State* L) const = 0; + virtual std::string getContents(lua_State* L, const std::string& loadname) const = 0; + + virtual std::string getChunkname(lua_State* L) const = 0; + virtual std::string getLoadname(lua_State* L) const = 0; + virtual std::string getCacheKey(lua_State* L) const = 0; + + virtual ConfigStatus getConfigStatus(lua_State* L) const = 0; + virtual std::string getConfig(lua_State* L) const = 0; + + virtual bool isPrecompiled() const = 0; +}; + struct RequireCtx { - RequireCtx(); - RequireCtx(CliVfs cliVfs); + RequireCtx(std::unique_ptr vfs); - RequireVfs vfs; + std::unique_ptr vfs; }; diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 8d143fe66..06292d9ee 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -1,8 +1,11 @@ #pragma once +#include "lute/bundlevfs.h" #include "lute/clivfs.h" #include "lute/filevfs.h" +#include "lute/lutevfs.h" #include "lute/modulepath.h" +#include "lute/require.h" #include "lute/stdlibvfs.h" #include "lua.h" @@ -10,29 +13,37 @@ #include #include -class RequireVfs +class RequireVfs : public IRequireVfs { public: RequireVfs() = default; RequireVfs(CliVfs cliVfs); + RequireVfs(BundleVfs bundleVfs); - bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const; + bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const override; - NavigationStatus reset(lua_State* L, std::string_view requirerChunkname); - NavigationStatus jumpToAlias(lua_State* L, std::string_view path); + NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; + NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + NavigationStatus toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) override; + NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) override; - NavigationStatus toParent(lua_State* L); - NavigationStatus toChild(lua_State* L, std::string_view name); + NavigationStatus toParent(lua_State* L) override; + NavigationStatus toChild(lua_State* L, std::string_view name) override; - bool isModulePresent(lua_State* L) const; - std::string getContents(lua_State* L, const std::string& loadname) const; + bool isModulePresent(lua_State* L) const override; + std::string getContents(lua_State* L, const std::string& loadname) const override; - std::string getChunkname(lua_State* L) const; - std::string getLoadname(lua_State* L) const; - std::string getCacheKey(lua_State* L) const; + std::string getChunkname(lua_State* L) const override; + std::string getLoadname(lua_State* L) const override; + std::string getCacheKey(lua_State* L) const override; - bool isConfigPresent(lua_State* L) const; - std::string getConfig(lua_State* L) const; + ConfigStatus getConfigStatus(lua_State* L) const override; + std::string getConfig(lua_State* L) const override; + + bool isPrecompiled() const override + { + return vfsType == VFSType::Bundle; + }; private: enum class VFSType @@ -40,6 +51,7 @@ class RequireVfs Disk, Std, Cli, + Bundle, Lute, }; @@ -47,8 +59,7 @@ class RequireVfs FileVfs fileVfs; StdLibVfs stdLibVfs; + LuteVfs luteVfs; std::optional cliVfs = std::nullopt; - std::string lutePath; - - bool atFakeRoot = false; + std::optional bundleVfs = std::nullopt; }; diff --git a/lute/require/include/lute/stdlibvfs.h b/lute/require/include/lute/stdlibvfs.h index fee171ee8..faa7023fe 100644 --- a/lute/require/include/lute/stdlibvfs.h +++ b/lute/require/include/lute/stdlibvfs.h @@ -16,7 +16,7 @@ class StdLibVfs std::string getIdentifier() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/include/lute/userlandvfs.h b/lute/require/include/lute/userlandvfs.h new file mode 100644 index 000000000..71967948c --- /dev/null +++ b/lute/require/include/lute/userlandvfs.h @@ -0,0 +1,111 @@ +#pragma once + +#include "lute/filevfs.h" +#include "lute/modulepath.h" + +#include "Luau/DenseHash.h" +#include "Luau/StringUtils.h" + +#include +#include +#include +#include + +namespace Package +{ + +struct Identifier +{ + std::string name; + std::string version; + + bool operator==(const Identifier& other) const + { + return std::tie(name, version) == std::tie(other.name, other.version); + } + bool operator!=(const Identifier& other) const + { + return !(*this == other); + } +}; + +struct IdentifierHashDefault +{ + size_t operator()(const Identifier& id) const + { + return Luau::detail::DenseHashDefault()(Luau::format("%s:%s", id.name.c_str(), id.version.c_str())); + } +}; + +struct Info +{ + std::string rootDirectory; + std::string entryFile; + std::vector dependencies; +}; + +class Subtree +{ +public: + static std::optional create(Info info); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + + bool isModulePresent() const; + + std::string getCurrentPath() const; + Info getInfo() const; + +private: + Subtree(ModulePath currentModulePath, Info info); + + ModulePath currentModulePath; + Info info; +}; + +class UserlandVfs +{ +public: + static UserlandVfs create(std::vector directDependencies, std::vector> allDependencies); + + NavigationStatus resetToPath(const std::string& path); + NavigationStatus toAliasFallback(std::string_view aliasUnprefixed); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + + bool isModulePresent() const; + std::optional getContents(const std::string& path) const; + + std::string getCurrentPath() const; + +private: + using DependencyMap = Luau::DenseHashMap; + + UserlandVfs(std::vector directDependencies, DependencyMap allDependencies); + + NavigationStatus jumpToDependencySubtree(const Identifier& dependency); + + enum class VFSType + { + Disk, + Subtree, + }; + + VFSType vfsType = VFSType::Disk; + + FileVfs fileVfs; + std::optional currentSubtree = std::nullopt; + + std::vector directDependencies; + DependencyMap allDependencies; +}; + +} // namespace Package diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp new file mode 100644 index 000000000..f1634dffd --- /dev/null +++ b/lute/require/src/bundlevfs.cpp @@ -0,0 +1,185 @@ +#include "lute/bundlevfs.h" + +#include "Luau/Common.h" +#include "Luau/DenseHash.h" + +#include +#include + +constexpr std::string_view kBundlePrefix = "@bundle"; +constexpr std::string_view kBundlePrefixPath = "@bundle/"; + +BundleVfs::BundleVfs(Luau::DenseHashMap luaurcFiles, Luau::DenseHashMap bundleMap) + : filePathToBytecode(std::move(bundleMap)) + , luaurcFiles(std::move(luaurcFiles)) +{ +} + +static bool isBundleModule(const Luau::DenseHashMap& bundleMap, const std::string& path) +{ + // The bundle root (@bundle or @bundle/) should never be treated as a file + if (path == kBundlePrefix || path == kBundlePrefixPath) + return false; + + // Strip @bundle/ prefix if present + std::string lookupPath = path; + if (path.rfind(kBundlePrefixPath, 0) == 0) + lookupPath = path.substr(kBundlePrefixPath.size()); + + // The bundle root should never be treated as a file, always as a directory + // This prevents ambiguity when there's an init.lua at the root + if (lookupPath == "init.lua" || lookupPath == "init.luau") + return false; + + // Check direct file match + if (bundleMap.find(lookupPath) != nullptr) + return true; + + return false; +} + +static bool isBundleDirectory(const Luau::DenseHashMap& bundleMap, const std::string& path) +{ + // Handle the root directory - both "@bundle" and "@bundle/" should be treated as the root + if (path == kBundlePrefix || path == kBundlePrefixPath) + return !bundleMap.empty(); + + // Strip @bundle/ prefix if present + std::string lookupPath = path; + if (path.rfind(kBundlePrefixPath, 0) == 0) + lookupPath = path.substr(kBundlePrefixPath.size()); + + // The root directory (@bundle/) exists if the bundle has any files + if (lookupPath.empty()) + return !bundleMap.empty(); + + // A directory exists if any file in the bundle starts with this path followed by a slash + std::string prefix = lookupPath + "/"; + for (const auto& [filePath, _] : bundleMap) + { + if (filePath.rfind(prefix, 0) == 0) + return true; + } + + return false; +} + +NavigationStatus BundleVfs::resetToPath(const std::string& path) +{ + // Handle "@bundle" root + if (path == "@bundle") + { + modulePath = ModulePath::create( + "@bundle", + "", + [this](const std::string& p) + { + return isBundleModule(filePathToBytecode, p); + }, + [this](const std::string& p) + { + return isBundleDirectory(filePathToBytecode, p); + } + ); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + } + + // Handle "@bundle/path/to/file" + if (path.rfind(kBundlePrefixPath, 0) != 0) + return NavigationStatus::NotFound; + + // Strip "@bundle/" to get the actual path + std::string filePath = path.substr(kBundlePrefixPath.size()); + + modulePath = ModulePath::create( + "@bundle", + filePath, + [this](const std::string& p) + { + return isBundleModule(filePathToBytecode, p); + }, + [this](const std::string& p) + { + return isBundleDirectory(filePathToBytecode, p); + } + ); + + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; +} + +NavigationStatus BundleVfs::toParent() +{ + LUAU_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus BundleVfs::toChild(const std::string& name) +{ + LUAU_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool BundleVfs::isModulePresent() const +{ + return isBundleModule(filePathToBytecode, getIdentifier()); +} + +std::string BundleVfs::getIdentifier() const +{ + LUAU_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUAU_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional BundleVfs::getContents(const std::string& path) const +{ + // Strip @bundle/ prefix if present + std::string lookupPath = path; + if (path.rfind(kBundlePrefixPath, 0) == 0) + lookupPath = path.substr(kBundlePrefixPath.size()); + + // Try direct lookup + const std::string* value = filePathToBytecode.find(lookupPath); + if (value != nullptr) + return *value; + + return std::nullopt; +} + +ConfigStatus BundleVfs::getConfigStatus() const +{ + LUAU_ASSERT(modulePath); + + // Get the potential config path for the current module path + std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); + + // Strip @bundle/ prefix if present + if (configPath.rfind(kBundlePrefixPath, 0) == 0) + configPath = configPath.substr(kBundlePrefixPath.size()); + + // Check if this config file exists in our luaurc map + if (luaurcFiles.find(configPath) != nullptr) + return ConfigStatus::PresentJson; + + return ConfigStatus::Absent; +} + +std::optional BundleVfs::getConfig() const +{ + LUAU_ASSERT(modulePath); + + // Get the potential config path for the current module path + std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); + + // Strip @bundle/ prefix if present + if (configPath.rfind(kBundlePrefixPath, 0) == 0) + configPath = configPath.substr(kBundlePrefixPath.size()); + + // Look up the config content in our luaurc map + const std::string* configContent = luaurcFiles.find(configPath); + if (configContent != nullptr) + return *configContent; + + return std::nullopt; +} diff --git a/lute/require/src/clivfs.cpp b/lute/require/src/clivfs.cpp index f232d081d..ce3c7f6b7 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -23,6 +23,9 @@ static std::optional readCliModule(const std::string& path) static bool isCliDirectory(const std::string& path) { + if (path == "@cli") + return true; + CliModuleResult result = getCliModule(path); return result.type == CliModuleType::Directory; } @@ -74,10 +77,10 @@ std::optional CliVfs::getContents(const std::string& path) const return readCliModule(path); } -bool CliVfs::isConfigPresent() const +ConfigStatus CliVfs::getConfigStatus() const { // Currently, we do not support .luaurc files in CLI commands. - return false; + return ConfigStatus::Absent; } std::optional CliVfs::getConfig() const diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index b9ae194f2..6cab7f446 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -1,9 +1,14 @@ #include "lute/filevfs.h" #include "lute/modulepath.h" +#include "lute/uvutils.h" #include "Luau/Common.h" +#include "Luau/Config.h" #include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" + +#include "uv.h" #include #include @@ -24,7 +29,25 @@ NavigationStatus FileVfs::resetToStdIn() NavigationStatus FileVfs::resetToPath(const std::string& path) { - std::string normalizedPath = normalizePath(path); + std::string pathToProcess = path; + + // Handle tilde expansion for home directory + if (!path.empty() && path[0] == '~') + { + auto result = uvutils::getStringFromUv(uv_os_homedir); + if (result.get_if() != nullptr) + return NavigationStatus::NotFound; + + std::string* homeDir = result.get_if(); + + // Replace ~ with home directory + if (path.size() == 1) + pathToProcess = *homeDir; + else if (path[1] == '/' || path[1] == '\\') + pathToProcess = *homeDir + path.substr(1); + } + + std::string normalizedPath = normalizePath(pathToProcess); if (isAbsolutePath(normalizedPath)) { @@ -88,14 +111,34 @@ std::optional FileVfs::getContents(const std::string& path) const return readFile(path); } -bool FileVfs::isConfigPresent() const +ConfigStatus FileVfs::getConfigStatus() const { LUAU_ASSERT(modulePath); - return isFile(modulePath->getPotentialLuaurcPath()); + + bool luaurcExists = isFile(modulePath->getPotentialConfigPath(Luau::kConfigName)); + bool luauConfigExists = isFile(modulePath->getPotentialConfigPath(Luau::kLuauConfigName)); + + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) + return ConfigStatus::PresentJson; + + return ConfigStatus::Absent; } std::optional FileVfs::getConfig() const { LUAU_ASSERT(modulePath); - return readFile(modulePath->getPotentialLuaurcPath()); + + ConfigStatus status = getConfigStatus(); + LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + + if (status == ConfigStatus::PresentJson) + return readFile(modulePath->getPotentialConfigPath(Luau::kConfigName)); + else if (status == ConfigStatus::PresentLuau) + return readFile(modulePath->getPotentialConfigPath(Luau::kLuauConfigName)); + + LUAU_UNREACHABLE(); } diff --git a/lute/require/src/lutevfs.cpp b/lute/require/src/lutevfs.cpp new file mode 100644 index 000000000..315d10538 --- /dev/null +++ b/lute/require/src/lutevfs.cpp @@ -0,0 +1,106 @@ +#include "lute/lutevfs.h" + +#include "lute/crypto.h" +#include "lute/fs.h" +#include "lute/io.h" +#include "lute/luau.h" +#include "lute/modulepath.h" +#include "lute/net.h" +#include "lute/process.h" +#include "lute/system.h" +#include "lute/task.h" +#include "lute/time.h" +#include "lute/vm.h" + +#include "Luau/DenseHash.h" + +#include "lua.h" + +const Luau::DenseHashMap kLuteModules = []() +{ + Luau::DenseHashMap map{""}; + map["@lute/crypto.luau"] = luteopen_crypto; + map["@lute/fs.luau"] = luteopen_fs; + map["@lute/luau.luau"] = luteopen_luau; + map["@lute/net.luau"] = luteopen_net; + map["@lute/process.luau"] = luteopen_process; + map["@lute/task.luau"] = luteopen_task; + map["@lute/vm.luau"] = luteopen_vm; + map["@lute/system.luau"] = luteopen_system; + map["@lute/time.luau"] = luteopen_time; + map["@lute/io.luau"] = luteopen_io; + return map; +}(); + +static bool isLuteModule(const std::string& path) +{ + return kLuteModules.contains(path); +} + +static bool isLuteDirectory(const std::string& path) +{ + return path == "@lute"; +} + +NavigationStatus LuteVfs::resetToPath(const std::string& path) +{ + if (path == "@lute") + { + modulePath = ModulePath::create("@lute", "", isLuteModule, isLuteDirectory); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + } + + std::string lutePrefix = "@lute/"; + + if (path.rfind(lutePrefix, 0) != 0) + return NavigationStatus::NotFound; + + modulePath = ModulePath::create("@lute", path.substr(lutePrefix.size()), isLuteModule, isLuteDirectory); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; +} + +NavigationStatus LuteVfs::toParent() +{ + LUAU_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus LuteVfs::toChild(const std::string& name) +{ + LUAU_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool LuteVfs::isModulePresent() const +{ + LUAU_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUAU_ASSERT(result.status == NavigationStatus::Success); + return result.type == ResolvedRealPath::PathType::File; +} + +std::string LuteVfs::getIdentifier() const +{ + LUAU_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUAU_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional LuteVfs::getContents(const std::string& path) const +{ + // Lute modules have no source code. + return ""; +} + +ConfigStatus LuteVfs::getConfigStatus() const +{ + // Currently, we do not support .luaurc files in Lute commands. + return ConfigStatus::Absent; +} + +std::optional LuteVfs::getConfig() const +{ + // Currently, we do not support .luaurc files in Lute commands. + return std::nullopt; +} diff --git a/lute/require/src/modulepath.cpp b/lute/require/src/modulepath.cpp index 059fde5c8..15cc31a58 100644 --- a/lute/require/src/modulepath.cpp +++ b/lute/require/src/modulepath.cpp @@ -39,8 +39,8 @@ static std::string_view removeExtension(std::string_view path) std::optional ModulePath::create( std::string rootDirectory, std::string filePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack ) { @@ -73,12 +73,12 @@ std::optional ModulePath::create( ModulePath::ModulePath( std::string realPathPrefix, std::string modulePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack ) - : isAFile(isAFile) - , isADirectory(isADirectory) + : isAFile(std::move(isAFile)) + , isADirectory(std::move(isADirectory)) , realPathPrefix(std::move(realPathPrefix)) , modulePath(std::move(modulePath)) , relativePathToTrack(std::move(relativePathToTrack)) @@ -87,7 +87,7 @@ ModulePath::ModulePath( ResolvedRealPath ModulePath::getRealPath() const { - bool found = false; + std::optional resolvedType; std::string suffix; std::string lastComponent; @@ -107,51 +107,52 @@ ResolvedRealPath ModulePath::getRealPath() const { if (isAFile(partialRealPath + std::string(potentialSuffix))) { - if (found) + if (resolvedType) return {NavigationStatus::Ambiguous}; + resolvedType = ResolvedRealPath::PathType::File; suffix = potentialSuffix; - found = true; } } } if (isADirectory(partialRealPath)) { - if (found) + if (resolvedType) return {NavigationStatus::Ambiguous}; for (std::string_view potentialSuffix : kInitSuffixes) { if (isAFile(partialRealPath + std::string(potentialSuffix))) { - if (found) + if (resolvedType) return {NavigationStatus::Ambiguous}; + resolvedType = ResolvedRealPath::PathType::File; suffix = potentialSuffix; - found = true; } } - found = true; + if (!resolvedType) + resolvedType = ResolvedRealPath::PathType::Directory; } - if (!found) + if (!resolvedType) return {NavigationStatus::NotFound}; std::optional relativePathWithSuffix; if (relativePathToTrack) relativePathWithSuffix = *relativePathToTrack + suffix; - return {NavigationStatus::Success, partialRealPath + suffix, relativePathWithSuffix}; + return {NavigationStatus::Success, partialRealPath + suffix, relativePathWithSuffix, *resolvedType}; } -std::string ModulePath::getPotentialLuaurcPath() const +std::string ModulePath::getPotentialConfigPath(const std::string& name) const { ResolvedRealPath result = getRealPath(); // No navigation has been performed; we should already be in a valid state. - assert(result.status == NavigationStatus::Success); + assert(result.status != NavigationStatus::NotFound); std::string_view directory = result.realPath; @@ -160,7 +161,7 @@ std::string ModulePath::getPotentialLuaurcPath() const if (hasSuffix(directory, suffix)) { directory.remove_suffix(suffix.size()); - return std::string(directory) + "/.luaurc"; + return std::string(directory) + "/" + name; } } for (std::string_view suffix : kSuffixes) @@ -168,11 +169,11 @@ std::string ModulePath::getPotentialLuaurcPath() const if (hasSuffix(directory, suffix)) { directory.remove_suffix(suffix.size()); - return std::string(directory) + "/.luaurc"; + return std::string(directory) + "/" + name; } } - return std::string(directory) + "/.luaurc"; + return std::string(directory) + "/" + name; } NavigationStatus ModulePath::toParent() @@ -188,11 +189,16 @@ NavigationStatus ModulePath::toParent() if (relativePathToTrack) relativePathToTrack = normalizePath(joinPaths(*relativePathToTrack, "..")); - return getRealPath().status; + // There is no ambiguity when navigating up in a tree. + NavigationStatus status = getRealPath().status; + return status == NavigationStatus::Ambiguous ? NavigationStatus::Success : status; } NavigationStatus ModulePath::toChild(const std::string& name) { + if (name == ".config") + return NavigationStatus::NotFound; + if (modulePath.empty()) modulePath = name; else diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp new file mode 100644 index 000000000..f4c0b11aa --- /dev/null +++ b/lute/require/src/packagerequirevfs.cpp @@ -0,0 +1,249 @@ +#include "lute/packagerequirevfs.h" + +#include "lute/modulepath.h" + +#include "Luau/FileUtils.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include + +namespace Package +{ + +RequireVfs::RequireVfs(UserlandVfs userlandVfs) + : userlandVfs(std::move(userlandVfs)) +{ +} + +bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const +{ + bool isFile = (!requirerChunkname.empty() && requirerChunkname[0] == '@'); + bool isStdLibFile = (requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/"); + return isFile || isStdLibFile; +} + +NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkname) +{ + if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/")) + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath(std::string(requirerChunkname.substr(1))); + } + + vfsType = VFSType::Userland; + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + return NavigationStatus::NotFound; + + if (!isAbsolutePath(requirerChunkname.substr(1))) + { + // For now, we only support absolute paths. + return NavigationStatus::NotFound; + } + + return userlandVfs.resetToPath(std::string(requirerChunkname.substr(1))); +} + +NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) +{ + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Userland: + status = userlandVfs.resetToPath(std::string(path)); + break; + case VFSType::Std: + status = stdLibVfs.resetToPath(std::string(path)); + break; + case VFSType::Lute: + status = luteVfs.resetToPath(std::string(path)); + break; + } + return status; +} + +NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) +{ + if (aliasUnprefixed == "std") + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath("@std"); + } + else if (aliasUnprefixed == "lute") + { + vfsType = VFSType::Lute; + return luteVfs.resetToPath("@lute"); + } + + return NavigationStatus::NotFound; +} + +NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +{ + NavigationStatus status = userlandVfs.toAliasFallback(aliasUnprefixed); + if (status == NavigationStatus::Success) + vfsType = VFSType::Userland; + return status; +} + +NavigationStatus RequireVfs::toParent(lua_State* L) +{ + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Userland: + status = userlandVfs.toParent(); + break; + case VFSType::Std: + status = stdLibVfs.toParent(); + break; + case VFSType::Lute: + status = luteVfs.toParent(); + break; + } + + return status; +} + +NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) +{ + switch (vfsType) + { + case VFSType::Userland: + return userlandVfs.toChild(std::string(name)); + case VFSType::Std: + return stdLibVfs.toChild(std::string(name)); + case VFSType::Lute: + return luteVfs.toChild(std::string(name)); + } + + return NavigationStatus::NotFound; +} + +bool RequireVfs::isModulePresent(lua_State* L) const +{ + switch (vfsType) + { + case VFSType::Userland: + return userlandVfs.isModulePresent(); + case VFSType::Std: + return stdLibVfs.isModulePresent(); + case VFSType::Lute: + return luteVfs.isModulePresent(); + } + + return false; +} + +std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) const +{ + std::optional contents; + switch (vfsType) + { + case VFSType::Userland: + contents = userlandVfs.getContents(loadname); + break; + case VFSType::Std: + contents = stdLibVfs.getContents(loadname); + break; + case VFSType::Lute: + contents = luteVfs.getContents(loadname); + break; + } + return contents ? *contents : ""; +} + +std::string RequireVfs::getChunkname(lua_State* L) const +{ + std::string chunkname; + switch (vfsType) + { + case VFSType::Userland: + chunkname = "@" + userlandVfs.getCurrentPath(); + break; + case VFSType::Std: + chunkname = "@" + stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + chunkname = "@" + luteVfs.getIdentifier(); + break; + } + return chunkname; +} + +std::string RequireVfs::getLoadname(lua_State* L) const +{ + std::string loadname; + switch (vfsType) + { + case VFSType::Userland: + loadname = userlandVfs.getCurrentPath(); + break; + case VFSType::Std: + loadname = stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + loadname = luteVfs.getIdentifier(); + break; + } + return loadname; +} + +std::string RequireVfs::getCacheKey(lua_State* L) const +{ + std::string cacheKey; + switch (vfsType) + { + case VFSType::Userland: + cacheKey = userlandVfs.getCurrentPath(); + break; + case VFSType::Std: + cacheKey = stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + cacheKey = luteVfs.getIdentifier(); + break; + } + return cacheKey; +} + +ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const +{ + ConfigStatus status = ConfigStatus::Ambiguous; + switch (vfsType) + { + case VFSType::Userland: + status = userlandVfs.getConfigStatus(); + break; + case VFSType::Std: + status = stdLibVfs.getConfigStatus(); + break; + case VFSType::Lute: + status = luteVfs.getConfigStatus(); + break; + } + return status; +} + +std::string RequireVfs::getConfig(lua_State* L) const +{ + std::optional configContents; + switch (vfsType) + { + case VFSType::Userland: + configContents = userlandVfs.getConfig(); + break; + case VFSType::Std: + configContents = stdLibVfs.getConfig(); + break; + case VFSType::Lute: + configContents = luteVfs.getConfig(); + break; + } + return configContents ? *configContents : ""; +} + +} // namespace Package diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 35e5fd3a4..a0c0414cb 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -1,16 +1,16 @@ #include "lute/require.h" -#include "lute/clivfs.h" +#include "lute/lutevfs.h" #include "lute/modulepath.h" #include "lute/options.h" +#include "Luau/CodeGen.h" +#include "Luau/Compiler.h" +#include "Luau/Require.h" + #include "lua.h" #include "lualib.h" -#include "Luau/Compiler.h" -#include "Luau/CodeGen.h" -#include "Luau/Require.h" -#include "Luau/StringUtils.h" #include static luarequire_WriteResult write(std::optional contents, char* buffer, size_t bufferSize, size_t* sizeOut) @@ -33,83 +33,133 @@ static luarequire_WriteResult write(std::optional contents, char* b static luarequire_NavigateResult convert(NavigationStatus status) { - if (status == NavigationStatus::Success) - return NAVIGATE_SUCCESS; - - if (status == NavigationStatus::Ambiguous) - return NAVIGATE_AMBIGUOUS; - - return NAVIGATE_NOT_FOUND; + luarequire_NavigateResult navigateResult = NAVIGATE_NOT_FOUND; + switch (status) + { + case NavigationStatus::Success: + navigateResult = NAVIGATE_SUCCESS; + break; + case NavigationStatus::Ambiguous: + navigateResult = NAVIGATE_AMBIGUOUS; + break; + case NavigationStatus::NotFound: + navigateResult = NAVIGATE_NOT_FOUND; + break; + }; + return navigateResult; +} + +static luarequire_ConfigStatus convert(ConfigStatus status) +{ + luarequire_ConfigStatus configStatus = CONFIG_AMBIGUOUS; + switch (status) + { + case ConfigStatus::Absent: + configStatus = CONFIG_ABSENT; + break; + case ConfigStatus::Ambiguous: + configStatus = CONFIG_AMBIGUOUS; + break; + case ConfigStatus::PresentJson: + configStatus = CONFIG_PRESENT_JSON; + break; + case ConfigStatus::PresentLuau: + configStatus = CONFIG_PRESENT_LUAU; + break; + }; + return configStatus; } static bool is_require_allowed(lua_State* L, void* ctx, const char* requirer_chunkname) { RequireCtx* reqCtx = static_cast(ctx); - return reqCtx->vfs.isRequireAllowed(L, requirer_chunkname); + return reqCtx->vfs->isRequireAllowed(L, requirer_chunkname); } static luarequire_NavigateResult reset(lua_State* L, void* ctx, const char* requirer_chunkname) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.reset(L, requirer_chunkname)); + return convert(reqCtx->vfs->reset(L, requirer_chunkname)); } static luarequire_NavigateResult jump_to_alias(lua_State* L, void* ctx, const char* path) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.jumpToAlias(L, path)); + return convert(reqCtx->vfs->jumpToAlias(L, path)); +} + +static luarequire_NavigateResult to_alias_override(lua_State* L, void* ctx, const char* alias_unprefixed) +{ + RequireCtx* reqCtx = static_cast(ctx); + return convert(reqCtx->vfs->toAliasOverride(L, alias_unprefixed)); +} + +static luarequire_NavigateResult to_alias_fallback(lua_State* L, void* ctx, const char* alias_unprefixed) +{ + RequireCtx* reqCtx = static_cast(ctx); + return convert(reqCtx->vfs->toAliasFallback(L, alias_unprefixed)); } static luarequire_NavigateResult to_parent(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.toParent(L)); + return convert(reqCtx->vfs->toParent(L)); } static luarequire_NavigateResult to_child(lua_State* L, void* ctx, const char* name) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.toChild(L, name)); + return convert(reqCtx->vfs->toChild(L, name)); } static bool is_module_present(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return reqCtx->vfs.isModulePresent(L); + return reqCtx->vfs->isModulePresent(L); } static luarequire_WriteResult get_chunkname(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getChunkname(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getChunkname(L), buffer, buffer_size, size_out); } static luarequire_WriteResult get_loadname(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getLoadname(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getLoadname(L), buffer, buffer_size, size_out); } static luarequire_WriteResult get_cache_key(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getCacheKey(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getCacheKey(L), buffer, buffer_size, size_out); } -static bool is_config_present(lua_State* L, void* ctx) +static luarequire_ConfigStatus get_config_status(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return reqCtx->vfs.isConfigPresent(L); + return convert(reqCtx->vfs->getConfigStatus(L)); } static luarequire_WriteResult get_config(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getConfig(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getConfig(L), buffer, buffer_size, size_out); } static int load(lua_State* L, void* ctx, const char* path, const char* chunkname, const char* loadname) { + // Lute modules are built-in and don't need to be compiled or executed. + if (strncmp(loadname, "@lute/", 6) == 0) + { + const lua_CFunction* func = kLuteModules.find(loadname); + LUAU_ASSERT(func); + lua_pushcfunction(L, *func, nullptr); + lua_call(L, 0, 1); + return 1; + } + // module needs to run in a new thread, isolated from the rest // note: we create ML on main thread so that it doesn't inherit environment of L lua_State* GL = lua_mainthread(L); @@ -120,12 +170,13 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname luaL_sandboxthread(ML); RequireCtx* reqCtx = static_cast(ctx); - std::optional contents = reqCtx->vfs.getContents(L, loadname); + std::optional contents = reqCtx->vfs->getContents(L, loadname); if (!contents) luaL_error(L, "could not read file '%s'", loadname); // now we can compile & run module on the new thread - std::string bytecode = Luau::compile(*contents, copts()); + std::string bytecode = reqCtx->vfs->isPrecompiled() ? *contents : Luau::compile(*contents, copts()); + bool errored = true; if (luau_load(ML, chunkname, bytecode.data(), bytecode.size(), 0) == 0) { if (getCodegenEnabled()) @@ -138,10 +189,10 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname if (status == 0) { - const std::string prefix = "module " + std::string(path) + " must"; - - if (lua_gettop(ML) == 0) - lua_pushstring(ML, (prefix + " return a value, if it has no return value, you should explicitly return `nil`\n").c_str()); + if (lua_gettop(ML) == 1) + errored = false; + else + lua_pushfstring(ML, "module %s must return a single value, if it has no return value, you should explicitly return `nil`\n", path); } else if (status == LUA_YIELD) { @@ -155,7 +206,7 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname // add ML result to L stack lua_xmove(ML, L, 1); - if (lua_isstring(L, -1)) + if (errored && lua_isstring(L, -1)) { lua_pushstring(L, lua_debugtrace(ML)); lua_concat(L, 2); @@ -177,24 +228,20 @@ void requireConfigInit(luarequire_Configuration* config) config->is_require_allowed = is_require_allowed; config->reset = reset; config->jump_to_alias = jump_to_alias; + config->to_alias_override = to_alias_override; + config->to_alias_fallback = to_alias_fallback; config->to_parent = to_parent; config->to_child = to_child; config->is_module_present = is_module_present; - config->is_config_present = is_config_present; + config->get_config_status = get_config_status; config->get_chunkname = get_chunkname; config->get_loadname = get_loadname; config->get_cache_key = get_cache_key; config->get_config = get_config; - config->get_alias = nullptr; // We use get_config instead of get_alias. config->load = load; } -RequireCtx::RequireCtx() - : vfs() -{ -} - -RequireCtx::RequireCtx(CliVfs cliVfs) - : vfs(cliVfs) +RequireCtx::RequireCtx(std::unique_ptr vfs) + : vfs(std::move(vfs)) { } diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 83b3e0cc0..278e21081 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -1,30 +1,35 @@ #include "lute/requirevfs.h" +#include "lute/bundlevfs.h" #include "lute/modulepath.h" #include "lute/stdlibvfs.h" -#include "lualib.h" - #include "Luau/Common.h" +#include "lualib.h" + RequireVfs::RequireVfs(CliVfs cliVfs) : cliVfs(std::move(cliVfs)) { } +RequireVfs::RequireVfs(BundleVfs bundleVfs) + : bundleVfs(std::move(bundleVfs)) +{ +} + bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const { bool isStdin = (requirerChunkname == "=stdin"); bool isFile = (!requirerChunkname.empty() && requirerChunkname[0] == '@'); bool isStdLibFile = (requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/"); bool isCliFile = (requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@cli/"); - return isStdin || isFile || isStdLibFile || (isCliFile && cliVfs); + bool isBundleFile = (requirerChunkname.size() >= 9 && requirerChunkname.substr(0, 9) == "@@bundle/"); + return isStdin || isFile || isStdLibFile || (isCliFile && cliVfs) || (isBundleFile && bundleVfs); } NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkname) { - atFakeRoot = false; - if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/")) { vfsType = VFSType::Std; @@ -38,6 +43,13 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn return cliVfs->resetToPath(std::string(requirerChunkname.substr(1))); } + if ((requirerChunkname.size() >= 9 && requirerChunkname.substr(0, 9) == "@@bundle/")) + { + vfsType = VFSType::Bundle; + LUAU_ASSERT(bundleVfs); + return bundleVfs->resetToPath(std::string(requirerChunkname.substr(1))); + } + vfsType = VFSType::Disk; if (requirerChunkname == "=stdin") return fileVfs.resetToStdIn(); @@ -50,37 +62,54 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) { - if (path == "$std") + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.resetToPath(std::string(path)); + break; + case VFSType::Std: + status = stdLibVfs.resetToPath(std::string(path)); + break; + case VFSType::Lute: + status = luteVfs.resetToPath(std::string(path)); + break; + case VFSType::Cli: + LUAU_ASSERT(cliVfs); + status = cliVfs->resetToPath(std::string(path)); + break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + status = bundleVfs->resetToPath(std::string(path)); + break; + } + return status; +} + +NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) +{ + if (aliasUnprefixed == "std") { - atFakeRoot = false; vfsType = VFSType::Std; return stdLibVfs.resetToPath("@std"); } - else if (path == "$lute") + else if (aliasUnprefixed == "lute") { vfsType = VFSType::Lute; - lutePath = "@lute"; - return NavigationStatus::Success; + return luteVfs.resetToPath("@lute"); } - switch (vfsType) - { - case VFSType::Disk: - return fileVfs.resetToPath(std::string(path)); - case VFSType::Std: - return stdLibVfs.resetToPath(std::string(path)); - case VFSType::Cli: - LUAU_ASSERT(cliVfs); - return cliVfs->resetToPath(std::string(path)); - default: - return NavigationStatus::NotFound; - } + return NavigationStatus::NotFound; } -NavigationStatus RequireVfs::toParent(lua_State* L) +NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) { - NavigationStatus status; + return NavigationStatus::NotFound; +} +NavigationStatus RequireVfs::toParent(lua_State* L) +{ + NavigationStatus status = NavigationStatus::NotFound; switch (vfsType) { case VFSType::Disk: @@ -89,49 +118,38 @@ NavigationStatus RequireVfs::toParent(lua_State* L) case VFSType::Std: status = stdLibVfs.toParent(); break; + case VFSType::Lute: + status = luteVfs.toParent(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); status = cliVfs->toParent(); break; - case VFSType::Lute: - luaL_error(L, "cannot get the parent of @lute"); + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + status = bundleVfs->toParent(); break; - default: - return NavigationStatus::NotFound; - } - - if (status == NavigationStatus::NotFound) - { - if (atFakeRoot) - return NavigationStatus::NotFound; - - atFakeRoot = true; - return NavigationStatus::Success; } - return status; } NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) { - atFakeRoot = false; - switch (vfsType) { case VFSType::Disk: return fileVfs.toChild(std::string(name)); case VFSType::Std: return stdLibVfs.toChild(std::string(name)); + case VFSType::Lute: + return luteVfs.toChild(std::string(name)); case VFSType::Cli: LUAU_ASSERT(cliVfs); return cliVfs->toChild(std::string(name)); - case VFSType::Lute: - luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); - break; - default: - break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + return bundleVfs->toChild(std::string(name)); } - return NavigationStatus::NotFound; } @@ -143,14 +161,15 @@ bool RequireVfs::isModulePresent(lua_State* L) const return fileVfs.isModulePresent(); case VFSType::Std: return stdLibVfs.isModulePresent(); + case VFSType::Lute: + return luteVfs.isModulePresent(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); return cliVfs->isModulePresent(); - case VFSType::Lute: - luaL_error(L, "@lute is not requirable"); - break; - default: - break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + return bundleVfs->isModulePresent(); } return false; @@ -168,11 +187,16 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c case VFSType::Std: contents = stdLibVfs.getContents(loadname); break; + case VFSType::Lute: + contents = luteVfs.getContents(loadname); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); contents = cliVfs->getContents(loadname); break; - default: + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + contents = bundleVfs->getContents(loadname); break; } @@ -181,86 +205,111 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c std::string RequireVfs::getChunkname(lua_State* L) const { + std::string chunkname; switch (vfsType) { case VFSType::Disk: - return "@" + fileVfs.getFilePath(); + chunkname = "@" + fileVfs.getFilePath(); + break; case VFSType::Std: - return "@" + stdLibVfs.getIdentifier(); + chunkname = "@" + stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + chunkname = "@" + luteVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return "@" + cliVfs->getIdentifier(); - default: - return ""; + chunkname = "@" + cliVfs->getIdentifier(); + break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + chunkname = "@" + bundleVfs->getIdentifier(); + break; } + return chunkname; } std::string RequireVfs::getLoadname(lua_State* L) const { + std::string loadname; switch (vfsType) { case VFSType::Disk: - return fileVfs.getAbsoluteFilePath(); + loadname = fileVfs.getAbsoluteFilePath(); + break; case VFSType::Std: - return stdLibVfs.getIdentifier(); + loadname = stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + loadname = luteVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->getIdentifier(); - default: - return ""; + loadname = cliVfs->getIdentifier(); + break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + loadname = bundleVfs->getIdentifier(); + break; } + return loadname; } std::string RequireVfs::getCacheKey(lua_State* L) const { + std::string cacheKey; switch (vfsType) { case VFSType::Disk: - return fileVfs.getAbsoluteFilePath(); + cacheKey = fileVfs.getAbsoluteFilePath(); + break; case VFSType::Std: - return stdLibVfs.getIdentifier(); + cacheKey = stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + cacheKey = luteVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->getIdentifier(); - default: - return ""; + cacheKey = cliVfs->getIdentifier(); + break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + cacheKey = bundleVfs->getIdentifier(); + break; } + return cacheKey; } -bool RequireVfs::isConfigPresent(lua_State* L) const +ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const { - if (atFakeRoot) - return true; - + ConfigStatus status = ConfigStatus::Absent; switch (vfsType) { case VFSType::Disk: - return fileVfs.isConfigPresent(); + status = fileVfs.getConfigStatus(); + break; case VFSType::Std: - return stdLibVfs.isConfigPresent(); + status = stdLibVfs.getConfigStatus(); + break; + case VFSType::Lute: + status = luteVfs.getConfigStatus(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->isConfigPresent(); - default: - return false; + status = cliVfs->getConfigStatus(); + break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + status = bundleVfs->getConfigStatus(); + break; } + return status; } std::string RequireVfs::getConfig(lua_State* L) const { - if (atFakeRoot) - { - std::string globalConfig = "{\n" - " \"aliases\": {\n" - " \"std\": \"$std\",\n" - " \"lute\": \"$lute\",\n" - " }\n" - "}\n"; - return globalConfig; - } - std::optional configContents; - switch (vfsType) { case VFSType::Disk: @@ -269,13 +318,17 @@ std::string RequireVfs::getConfig(lua_State* L) const case VFSType::Std: configContents = stdLibVfs.getConfig(); break; + case VFSType::Lute: + configContents = luteVfs.getConfig(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); configContents = cliVfs->getConfig(); break; - default: + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + configContents = bundleVfs->getConfig(); break; } - return configContents ? *configContents : ""; } diff --git a/lute/require/src/stdlibvfs.cpp b/lute/require/src/stdlibvfs.cpp index 6b0e0041d..ecc2aa4dc 100644 --- a/lute/require/src/stdlibvfs.cpp +++ b/lute/require/src/stdlibvfs.cpp @@ -77,10 +77,10 @@ std::optional StdLibVfs::getContents(const std::string& path) const return readStdLibModule(path); } -bool StdLibVfs::isConfigPresent() const +ConfigStatus StdLibVfs::getConfigStatus() const { // Currently, we do not support .luaurc files in the standard library. - return false; + return ConfigStatus::Absent; } std::optional StdLibVfs::getConfig() const diff --git a/lute/require/src/userlandvfs.cpp b/lute/require/src/userlandvfs.cpp new file mode 100644 index 000000000..c857aa9ea --- /dev/null +++ b/lute/require/src/userlandvfs.cpp @@ -0,0 +1,275 @@ +#include "lute/userlandvfs.h" + +#include "lute/modulepath.h" + +#include "Luau/Common.h" +#include "Luau/Config.h" +#include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" + +#include +#include + +namespace Package +{ + +std::optional Subtree::create(Info info) +{ + std::string_view entryFile = info.entryFile; + if (entryFile.rfind(info.rootDirectory, 0) != 0) + return std::nullopt; + + if (entryFile.size() <= info.rootDirectory.size()) + return std::nullopt; + + if (entryFile[info.rootDirectory.size()] != '/' && entryFile[info.rootDirectory.size()] != '\\') + return std::nullopt; + + std::string entryFileWithoutRoot = info.entryFile.substr(info.rootDirectory.size() + 1); + + std::optional currentModulePath = ModulePath::create(info.rootDirectory, entryFileWithoutRoot, isFile, isDirectory); + if (!currentModulePath) + return std::nullopt; + + return Subtree{std::move(*currentModulePath), std::move(info)}; +} + +Subtree::Subtree(ModulePath currentModulePath, Info info) + : currentModulePath(std::move(currentModulePath)) + , info(std::move(info)) +{ +} + +NavigationStatus Subtree::toParent() +{ + return currentModulePath.toParent(); +} + +NavigationStatus Subtree::toChild(const std::string& name) +{ + return currentModulePath.toChild(name); +} + +ConfigStatus Subtree::getConfigStatus() const +{ + bool luaurcExists = isFile(currentModulePath.getPotentialConfigPath(Luau::kConfigName)); + bool luauConfigExists = isFile(currentModulePath.getPotentialConfigPath(Luau::kLuauConfigName)); + + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) + return ConfigStatus::PresentJson; + + return ConfigStatus::Absent; +} + +std::optional Subtree::getConfig() const +{ + ConfigStatus status = getConfigStatus(); + LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + + if (status == ConfigStatus::PresentJson) + return readFile(currentModulePath.getPotentialConfigPath(Luau::kConfigName)); + else if (status == ConfigStatus::PresentLuau) + return readFile(currentModulePath.getPotentialConfigPath(Luau::kLuauConfigName)); + + LUAU_UNREACHABLE(); +} + +bool Subtree::isModulePresent() const +{ + ResolvedRealPath result = currentModulePath.getRealPath(); + if (result.status != NavigationStatus::Success) + return false; + + return isFile(result.realPath); +} + +std::string Subtree::getCurrentPath() const +{ + ResolvedRealPath result = currentModulePath.getRealPath(); + if (result.status != NavigationStatus::Success) + return ""; + + return result.realPath; +} + +Info Subtree::getInfo() const +{ + return info; +} + +UserlandVfs UserlandVfs::create(std::vector directDependencies, std::vector> allDependencies) +{ + DependencyMap allDependenciesMap{{}}; + for (auto& [identifier, info] : allDependencies) + { + allDependenciesMap[std::move(identifier)] = std::move(info); + } + + return UserlandVfs{std::move(directDependencies), std::move(allDependenciesMap)}; +} + +UserlandVfs::UserlandVfs(std::vector directDependencies, DependencyMap allDependencies) + : directDependencies(std::move(directDependencies)) + , allDependencies(std::move(allDependencies)) +{ +} + +NavigationStatus UserlandVfs::resetToPath(const std::string& path) +{ + for (const auto& [identifier, info] : allDependencies) + { + if (path.rfind(info.rootDirectory, 0) == 0) + return jumpToDependencySubtree(identifier); + } + + currentSubtree = std::nullopt; + vfsType = VFSType::Disk; + + return fileVfs.resetToPath(path); +} + +NavigationStatus UserlandVfs::toAliasFallback(std::string_view aliasUnprefixed) +{ + std::vector availableDependencies; + switch (vfsType) + { + case VFSType::Disk: + availableDependencies = directDependencies; + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + availableDependencies = currentSubtree->getInfo().dependencies; + } + + for (const Identifier& identifier : availableDependencies) + { + if (identifier.name == aliasUnprefixed) + return jumpToDependencySubtree(identifier); + } + + return NavigationStatus::NotFound; +} + +NavigationStatus UserlandVfs::jumpToDependencySubtree(const Identifier& dependency) +{ + Info* info = allDependencies.find(dependency); + if (!info) + return NavigationStatus::NotFound; + + std::optional st = Subtree::create(*info); + if (!st) + return NavigationStatus::NotFound; + + currentSubtree = std::move(*st); + vfsType = VFSType::Subtree; + + return NavigationStatus::Success; +} + +NavigationStatus UserlandVfs::toParent() +{ + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.toParent(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + status = currentSubtree->toParent(); + break; + } + + return status; +} + +NavigationStatus UserlandVfs::toChild(const std::string& name) +{ + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.toChild(name); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + status = currentSubtree->toChild(name); + break; + } + return status; +} + +ConfigStatus UserlandVfs::getConfigStatus() const +{ + ConfigStatus status = ConfigStatus::Ambiguous; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.getConfigStatus(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + status = currentSubtree->getConfigStatus(); + break; + } + return status; +} + +std::optional UserlandVfs::getConfig() const +{ + std::optional config; + switch (vfsType) + { + case VFSType::Disk: + config = fileVfs.getConfig(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + config = currentSubtree->getConfig(); + break; + } + return config; +} + +bool UserlandVfs::isModulePresent() const +{ + bool isPresent = false; + switch (vfsType) + { + case VFSType::Disk: + isPresent = fileVfs.isModulePresent(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + isPresent = currentSubtree->isModulePresent(); + break; + } + return isPresent; +} + +std::optional UserlandVfs::getContents(const std::string& path) const +{ + return readFile(path); +} + +std::string UserlandVfs::getCurrentPath() const +{ + std::string path; + switch (vfsType) + { + case VFSType::Disk: + path = fileVfs.getAbsoluteFilePath(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + path = currentSubtree->getCurrentPath(); + break; + } + return path; +} + +} // namespace Package diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 632d89ffd..4d6cf895a 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -4,12 +4,16 @@ target_sources(Lute.Runtime PRIVATE include/lute/ref.h include/lute/runtime.h include/lute/userdatas.h + include/lute/UVRequest.h + include/lute/uvutils.h src/ref.cpp src/runtime.cpp + src/UVRequest.cpp + src/uvutils.cpp ) target_compile_features(Lute.Runtime PUBLIC cxx_std_17) target_include_directories(Lute.Runtime PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Runtime PRIVATE Lute.Crypto Lute.Fs Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Luau.Common Luau.Require Luau.VM uv_a) +target_link_libraries(Lute.Runtime PRIVATE Luau.Common Luau.Require Luau.VM uv_a) target_compile_options(Lute.Runtime PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/runtime/include/lute/UVRequest.h b/lute/runtime/include/lute/UVRequest.h new file mode 100644 index 000000000..a8006b6c1 --- /dev/null +++ b/lute/runtime/include/lute/UVRequest.h @@ -0,0 +1,124 @@ +#pragma once + +#include "lute/runtime.h" + +#include "uv.h" + +#include +#include + +namespace uvutils +{ + +template +void cleanup_uv_req(ReqT* req) +{ +} + +template<> +void cleanup_uv_req(uv_fs_t* req); + +// Free template function to recover type +template +std::unique_ptr retake(ReqT* req) +{ + return std::unique_ptr(static_cast(req->data)); +} + +template +struct UVRequest +{ + UVRequest(lua_State* L) + : token(getResumeToken(L)) + { + req.data = this; + } + + UVRequest(const UVRequest&) = delete; + UVRequest& operator=(const UVRequest&) = delete; + UVRequest(UVRequest&&) = delete; + UVRequest& operator=(UVRequest&&) = delete; + + // Proxy to token->fail with format string support + template + void fail(const char* fmt, Args&&... args) + { + // First, determine the required size + int size = snprintf(nullptr, 0, fmt, std::forward(args)...); + if (size < 0) + { + token->fail("Format error in fail"); + return; + } + + // Allocate buffer with exact size needed (+1 for null terminator) + std::vector buffer(size + 1); + snprintf(buffer.data(), buffer.size(), fmt, std::forward(args)...); + token->fail(std::string(buffer.data())); + } + + template + void succeed(F&& cont) + { + token->complete(std::forward(cont)); + } + + ~UVRequest() + { + cleanup_uv_req(&req); + } + + ResumeToken token; + ReqT req; +}; + + +template +struct ScopedUVRequest +{ + + ScopedUVRequest(std::unique_ptr req) + : ptr(std::move(req)) + { + } + + // Constructor that creates the unique_ptr from arguments + template + explicit ScopedUVRequest(Args&&... args) + : ptr(std::make_unique(std::forward(args)...)) + { + } + + ~ScopedUVRequest() + { + // The actual libuv C request struct retains a pointer to the request in the data field. + // If we don't call release, this unique_pointer will be freed at the end of the scope that + // contains the request, when we actually want this to be freed inside the asynchronously called callback. + // The user should then take ownership of the request from the data via `retake` and the destructr + // will automatically be invoked after we've scheduled the Luau code to resume + if (ptr) + { + ptr.release(); + } + } + + // Non-copyable and non-movable to prevent accidental double-release + ScopedUVRequest(const ScopedUVRequest&) = delete; + ScopedUVRequest& operator=(const ScopedUVRequest&) = delete; + ScopedUVRequest(ScopedUVRequest&&) = delete; + ScopedUVRequest& operator=(ScopedUVRequest&&) = delete; + + T* get() const + { + return ptr.get(); + } + + T* operator->() const + { + return ptr.get(); + } + + std::unique_ptr ptr; +}; + +} // namespace uvutils diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 8b8d94e29..b3635d3bc 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -1,8 +1,10 @@ #pragma once -#include "Luau/Variant.h" #include "lute/ref.h" +#include "Luau/Variant.h" +#include "Luau/VecDeque.h" + #include #include #include @@ -79,7 +81,7 @@ struct Runtime std::mutex dataCopyMutex; std::unique_ptr dataCopy; - std::vector runningThreads; + Luau::VecDeque runningThreads; private: std::mutex continuationMutex; @@ -112,4 +114,4 @@ struct ResumeTokenData ResumeToken getResumeToken(lua_State* L); -lua_State* setupState(Runtime& runtime, void (*doBeforeSandbox)(lua_State*)); +lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox); diff --git a/lute/runtime/include/lute/userdatas.h b/lute/runtime/include/lute/userdatas.h index a951dbb36..b0eb73c5d 100644 --- a/lute/runtime/include/lute/userdatas.h +++ b/lute/runtime/include/lute/userdatas.h @@ -1,7 +1,8 @@ #pragma once // all tags count down from 128 -constexpr int kDurationTag = 127; -constexpr int kInstantTag = 126; -constexpr int kCompilerResultTag = 125; -constexpr int kWatchHandleTag = 124; \ No newline at end of file +constexpr int kDurationTag = 127; +constexpr int kInstantTag = 126; +constexpr int kWatchHandleTag = 125; +constexpr int kHashFunctionTag = 124; +constexpr int kSpanTag = 123; diff --git a/lute/runtime/include/lute/uvutils.h b/lute/runtime/include/lute/uvutils.h new file mode 100644 index 000000000..32f81455f --- /dev/null +++ b/lute/runtime/include/lute/uvutils.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Luau/Variant.h" + +#include +#include + +namespace uvutils +{ + +struct UvError +{ + UvError(int code); + std::string toString() const; + + int code; +}; + +typedef int (*BufferWriter)(char* buffer, size_t* size); + +// Abstracts away buffer management when getting strings from libuv functions. +Luau::Variant getStringFromUv(BufferWriter bufferWriter, size_t initialBufferSize = 256); + +} // namespace uvutils diff --git a/lute/runtime/src/UVRequest.cpp b/lute/runtime/src/UVRequest.cpp new file mode 100644 index 000000000..149e11bd1 --- /dev/null +++ b/lute/runtime/src/UVRequest.cpp @@ -0,0 +1,15 @@ +#include "lute/UVRequest.h" + +#include "uv.h" + +namespace uvutils +{ + +// UV request cleanup specializations +template<> +void cleanup_uv_req(uv_fs_t* req) +{ + uv_fs_req_cleanup(req); +} + +} // namespace uvutils diff --git a/lute/runtime/src/ref.cpp b/lute/runtime/src/ref.cpp index be4e0f60f..b8378fc4b 100644 --- a/lute/runtime/src/ref.cpp +++ b/lute/runtime/src/ref.cpp @@ -1,4 +1,5 @@ #include "lute/ref.h" + #include "lute/runtime.h" #include "lua.h" @@ -23,6 +24,7 @@ void Ref::push(lua_State* L) const std::shared_ptr getRefForThread(lua_State* L) { + lua_rawcheckstack(L, 1); lua_pushthread(L); std::shared_ptr ref = std::make_shared(L, -1); lua_pop(L, 1); diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 5dac64d64..e617ea420 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -1,15 +1,5 @@ #include "lute/runtime.h" -#include "lute/crypto.h" -#include "lute/fs.h" -#include "lute/luau.h" -#include "lute/net.h" -#include "lute/process.h" -#include "lute/system.h" -#include "lute/task.h" -#include "lute/vm.h" -#include "lute/time.h" - #include "Luau/Require.h" #include "lua.h" @@ -17,8 +7,8 @@ #include "uv.h" -#include #include +#include static void lua_close_checked(lua_State* L) { @@ -50,12 +40,16 @@ Runtime::~Runtime() bool Runtime::hasWork() { - return hasContinuations() || hasThreads() || activeTokens.load() != 0; + // TODO: activeTokens and uv_loop_alive have a decent amount of overlap. + // Unfortunately, we do currently have some places where we add/release + // tokens that don't correspond to libuv activity, so for now we keep both. + // uv_ref/unref could be used to patch tokens into the libuv loop itself. + return hasContinuations() || hasThreads() || activeTokens.load() != 0 || uv_loop_alive(uv_default_loop()); } RuntimeStep Runtime::runOnce() { - uv_run(uv_default_loop(), UV_RUN_DEFAULT); + uv_run(uv_default_loop(), UV_RUN_NOWAIT); // Complete all C++ continuations std::vector> copy; @@ -73,7 +67,7 @@ RuntimeStep Runtime::runOnce() return StepEmpty{}; auto next = std::move(runningThreads.front()); - runningThreads.erase(runningThreads.begin()); + runningThreads.pop_front(); next.ref->push(GL); lua_State* L = lua_tothread(GL, -1); @@ -274,7 +268,7 @@ void Runtime::releasePendingToken() Runtime* getRuntime(lua_State* L) { - return reinterpret_cast(lua_getthreaddata(lua_mainthread(L))); + return static_cast(lua_getthreaddata(lua_mainthread(L))); } void ResumeTokenData::fail(std::string error) @@ -307,30 +301,7 @@ ResumeToken getResumeToken(lua_State* L) return token; } -static void luteopen_libs(lua_State* L) -{ - std::vector> libs = {{ - {"@lute/crypto", luteopen_crypto}, - {"@lute/fs", luteopen_fs}, - {"@lute/luau", luteopen_luau}, - {"@lute/net", luteopen_net}, - {"@lute/process", luteopen_process}, - {"@lute/task", luteopen_task}, - {"@lute/vm", luteopen_vm}, - {"@lute/system", luteopen_system}, - {"@lute/time", luteopen_time}, - }}; - - for (const auto& [name, func] : libs) - { - lua_pushcfunction(L, luarequire_registermodule, nullptr); - lua_pushstring(L, name); - func(L); - lua_call(L, 2, 0); - } -} - -lua_State* setupState(Runtime& runtime, void (*doBeforeSandbox)(lua_State*)) +lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox) { // Separate VM for data copies runtime.dataCopy.reset(luaL_newstate()); @@ -346,8 +317,6 @@ lua_State* setupState(Runtime& runtime, void (*doBeforeSandbox)(lua_State*)) // register the builtin tables luaL_openlibs(L); - luteopen_libs(L); - lua_pushnil(L); lua_setglobal(L, "setfenv"); diff --git a/lute/runtime/src/uvutils.cpp b/lute/runtime/src/uvutils.cpp new file mode 100644 index 000000000..eebd00b7c --- /dev/null +++ b/lute/runtime/src/uvutils.cpp @@ -0,0 +1,39 @@ +#include "lute/uvutils.h" + +#include "uv.h" + +namespace uvutils +{ + +UvError::UvError(int code) + : code(code) +{ +} + +std::string UvError::toString() const +{ + return uv_strerror(code); +} + +Luau::Variant getStringFromUv(BufferWriter bufferWriter, size_t initialBufferSize) +{ + std::string buffer; + size_t size = initialBufferSize; + buffer.resize(size); + + int writeStatus = bufferWriter(buffer.data(), &size); + if (writeStatus == UV_ENOBUFS) + { + // `size` now contains the required size + buffer.resize(size); + writeStatus = bufferWriter(buffer.data(), &size); + } + + if (writeStatus < 0) + return UvError{writeStatus}; + + buffer.resize(size); + return buffer; +} + +} // namespace uvutils diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau new file mode 100644 index 000000000..c9e791ce8 --- /dev/null +++ b/lute/std/libs/fs.luau @@ -0,0 +1,199 @@ +--!strict +-- @std/fs +-- stdlib for `@lute/fs` +-- Provides file system utility functions that handles file paths as path types instead of strings + +local fs = require("@lute/fs") +local pathlib = require("@std/path") +local sys = require("@std/system") + +local fslib = {} + +export type handlemode = fs.HandleMode +export type file = fs.FileHandle +export type type = fs.FileType +export type metadata = fs.FileMetadata +export type directoryentry = fs.DirectoryEntry +export type watchhandle = fs.WatchHandle +export type watchevent = fs.WatchEvent + +type pathlike = pathlib.pathlike +type path = pathlib.path + +export type createdirectoryoptions = { + makeparents: boolean?, +} + +export type removedirectoryoptions = { + recursive: boolean?, +} + +export type walkoptions = { + recursive: boolean?, +} + +type watcher = { + next: (watcher) -> watchevent?, + close: (self: watcher) -> (), +} + +function fslib.open(path: pathlike, mode: handlemode?): file + return fs.open(pathlib.format(path), mode) +end + +function fslib.read(file: file): string + return fs.read(file) +end + +function fslib.write(file: file, contents: string): () + return fs.write(file, contents) +end + +function fslib.close(file: file): () + return fs.close(file) +end + +function fslib.remove(path: pathlike): () + return fs.remove(pathlib.format(path)) +end + +function fslib.metadata(path: pathlike): metadata + return fs.stat(pathlib.format(path)) +end + +function fslib.type(path: pathlike): type + return fs.type(pathlib.format(path)) +end + +function fslib.link(src: pathlike, dest: pathlike): () + return fs.link(pathlib.format(src), pathlib.format(dest)) +end + +function fslib.symboliclink(src: pathlike, dest: pathlike): () + return fs.symlink(pathlib.format(src), pathlib.format(dest)) +end + +--- Iterator function that yields filename and event pairs when changes occur in the watched path. +--- Also provides a `close` method to stop watching. +--- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. +function fslib.watch(path: pathlike): watcher + local queue = {} + local handle = fs.watch(pathlib.format(path), function(filename: string, event: watchevent) + table.insert(queue, { filename = pathlib.parse(filename), event = event }) + end) + + return { + next = function(self: watcher): watchevent? + if #queue == 0 then + return nil + end + local item = table.remove(queue, 1) + return item.event + end, + close = function(self: watcher): () + if handle then + handle:close() + end + end, + } +end + +function fslib.exists(path: pathlike): boolean + return fs.exists(pathlib.format(path)) +end + +function fslib.copy(src: pathlike, dest: pathlike): () + return fs.copy(pathlib.format(src), pathlib.format(dest)) +end + +function fslib.listdirectory(path: pathlike): { directoryentry } + return fs.listdir(pathlib.format(path)) +end + +function fslib.readfiletostring(filepath: pathlike): string + local f = fs.open(pathlib.format(filepath), "r") + local contents = fs.read(f) + fs.close(f) + return contents +end + +function fslib.writestringtofile(filepath: pathlike, contents: string): () + local f = fs.open(pathlib.format(filepath), "w+") + fs.write(f, contents) + fs.close(f) +end + +function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?): () + if options and options.makeparents then + local parsed = pathlib.parse(path) + local parts: { string } = parsed.parts + + local subdir: pathlike = "." + if pathlib.isabsolute(path) then + if sys.win32 then + -- Windows path - get the drive root like "C:\" + subdir = pathlib.win32.drive(parsed) + else + -- POSIX absolute path - start from root "/" + subdir = pathlib.format({ absolute = true, parts = {} }) + end + end + + for _, part in parts do + subdir = pathlib.join(subdir, part) + if not fslib.exists(subdir) then + fs.mkdir(pathlib.format(subdir)) + end + end + else + fs.mkdir(pathlib.format(path)) + end +end + +function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?): () + if options and options.recursive then + -- Recursively delete all contents first + if fslib.exists(path) and fslib.type(path) == "dir" then + local entries = fslib.listdirectory(path) + for _, entry in entries do + local entryPath = pathlib.join(path, entry.name) + if entry.type == "dir" then + fslib.removedirectory(entryPath, { recursive = true }) + else + fslib.remove(entryPath) + end + end + fs.rmdir(pathlib.format(path)) + end + else + fs.rmdir(pathlib.format(path)) + end +end + +--- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage. +function fslib.walk(path: pathlike, options: walkoptions?): () -> path? + local queue = { path } + + return function() + while #queue > 0 do + local current = table.remove(queue, 1) + if not current then + return nil + end + + if fslib.type(current) == "dir" and options and options.recursive then -- LUAUFIX: error on options.recursive because it thinks options could still be nil + local entries = fslib.listdirectory(current) + for _, entry in entries do + local fullPath = pathlib.join(current, entry.name) + table.insert(queue, fullPath) + end + end + + return current + end + + return nil + end +end + +return table.freeze(fslib) diff --git a/lute/std/libs/io.luau b/lute/std/libs/io.luau new file mode 100644 index 000000000..fa61cfeeb --- /dev/null +++ b/lute/std/libs/io.luau @@ -0,0 +1,19 @@ +--!strict +-- @std/io +-- stdlib for `@lute/io` +-- Provides standard input, output, and other I/O utility functions + +local io = require("@lute/io") + +local iolib = {} + +-- User input can be provided via command line stdin, piped input, or redirection from a file. See `examples/user_input.luau`. +function iolib.input(prompt: string?): string + if prompt then + -- We temporarily use `print` for prompt until we have proper stdout support, so there is a `\n` between prompt and input. + print(prompt) + end + return io.read() +end + +return table.freeze(iolib) diff --git a/batteries/json.luau b/lute/std/libs/json.luau similarity index 56% rename from batteries/json.luau rename to lute/std/libs/json.luau index 36eaf5521..14502f5e9 100644 --- a/batteries/json.luau +++ b/lute/std/libs/json.luau @@ -1,17 +1,23 @@ --!strict +-- @std/json +-- JSON serialization and deserialization library local json = { --- Not actually a nil value, a newproxy stand-in for a null value since Luau has no actual representation of `null` - NULL = newproxy() :: nil, + null = newproxy() :: nil, } -type JSONPrimitive = nil | number | string | boolean -type Object = { [string]: Value } -type Array = { Value } -export type Value = JSONPrimitive | Array | Object +export type object = { [string]: value } +export type array = { [number]: value } +export type value = nil | number | string | boolean | array | object + +-- a unique key to identify an object vs a table +local object_key = newproxy() -- serialization +local bufferSize = 1024 + type SerializerState = { buf: buffer, cursor: number, @@ -49,12 +55,12 @@ local function writeSpaces(state: SerializerState) if state.prettyPrint then checkState(state, state.depth * 4) - for i = 0, state.depth do + for i = 1, state.depth do buffer.writeu32(state.buf, state.cursor, 0x_20_20_20_20) state.cursor += 4 end else - buffer.writeu8(state.buf, state.cursor, string.byte(" ")) + buffer.writeu8(state.buf, state.cursor, (string.byte(" "))) state.cursor += 1 end @@ -68,7 +74,7 @@ local function writeString(state: SerializerState, str: string) state.cursor += #str end -local serializeAny +local serializeAny: (SerializerState, (array | object | boolean | number | string)?) -> () local ESCAPE_MAP = { [0x5C] = string.byte("\\"), -- 5C = '\' @@ -77,6 +83,7 @@ local ESCAPE_MAP = { [0x0A] = string.byte("n"), [0x0D] = string.byte("r"), [0x09] = string.byte("t"), + [0x22] = string.byte('"'), } local function serializeUnicode(codepoint: number) @@ -108,7 +115,7 @@ local function serializeString(state: SerializerState, str: string) writeByte(state, string.byte('"')) end -local function serializeArray(state: SerializerState, array: Array) +local function serializeArray(state: SerializerState, array: array) state.depth += 1 writeByte(state, string.byte("[")) @@ -143,7 +150,7 @@ local function serializeArray(state: SerializerState, array: Array) writeByte(state, string.byte("]")) end -local function serializeTable(state: SerializerState, object: Object) +local function serializeTable(state: SerializerState, object: object) writeByte(state, string.byte("{")) if state.prettyPrint then @@ -154,6 +161,10 @@ local function serializeTable(state: SerializerState, object: Object) local first = true for key, value in object do + if key == object_key then + continue + end + if not first then writeByte(state, string.byte(",")) writeByte(state, string.byte(" ")) @@ -167,9 +178,8 @@ local function serializeTable(state: SerializerState, object: Object) writeSpaces(state) - writeByte(state, string.byte('"')) - writeString(state, key) - writeString(state, '": ') + serializeString(state, key) + writeString(state, ": ") serializeAny(state, value) end @@ -185,10 +195,10 @@ local function serializeTable(state: SerializerState, object: Object) writeByte(state, string.byte("}")) end -serializeAny = function(state: SerializerState, value: Value) +serializeAny = function(state: SerializerState, value: value) local valueType = type(value) - if value == json.NULL then + if value == json.null then writeString(state, "null") elseif valueType == "boolean" then writeString(state, if value then "true" else "false") @@ -197,10 +207,10 @@ serializeAny = function(state: SerializerState, value: Value) elseif valueType == "string" then serializeString(state, value :: string) elseif valueType == "table" then - if #(value :: {}) == 0 and next(value :: {}) ~= nil then - serializeTable(state, value :: Object) + if #(value :: {}) == 0 and next(value :: { [unknown]: unknown }) ~= nil then + serializeTable(state, value :: object) else - serializeArray(state, value :: Array) + serializeArray(state, value :: array) end else error("Unknown value", 2) @@ -219,7 +229,7 @@ local function deserializerError(state: DeserializerState, msg: string): never end local function skipWhitespace(state: DeserializerState): boolean - state.cursor = string.find(state.src, "%S", state.cursor) :: number + state.cursor = string.find(state.src, "[^ \n\r\t]", state.cursor) :: number if not state.cursor then return false @@ -233,72 +243,103 @@ local function currentByte(state: DeserializerState) end local function deserializeNumber(state: DeserializerState) - -- first "segment" - local nStart, nEnd = string.find(state.src, "^[%-%deE]*", state.cursor) - + -- Integer part + local nStart, nEnd = string.find(state.src, "^-?[1-9]%d*", state.cursor) if not nStart then - -- i dont think this is possible - deserializerError(state, "Could not match a number literal?") - end - - if string.byte(state.src, nEnd :: number + 1) == string.byte(".") then -- decimal! - local decStart, decEnd = string.find(state.src, "^[eE%-+%d]+", nEnd :: number + 2) + nStart, nEnd = string.find(state.src, "^-?0", state.cursor) + if not nStart then + deserializerError(state, "Malformed number (bad integer part)") + end - if not decStart then - deserializerError(state, "Trailing '.' in number value") + local nextChar = string.byte(state.src, nEnd :: number + 1) + if nextChar and nextChar >= string.byte("0") and nextChar <= string.byte("9") then + deserializerError(state, "Malformed number (leading zeros not allowed)") end + end + -- Decimal part + local decStart, decEnd = string.find(state.src, "^%.%d+", nEnd :: number + 1) + if decStart then nEnd = decEnd end - local num = tonumber(string.sub(state.src, nStart :: number, nEnd)) + -- Exponential part + local expStart, expEnd = string.find(state.src, "^[eE][+-]?%d+", nEnd :: number + 1) + if expStart then + nEnd = expEnd + end + local num = tonumber(string.sub(state.src, nStart :: number, nEnd)) if not num then deserializerError(state, "Malformed number value") end state.cursor = nEnd :: number + 1 - return num end local function decodeSurrogatePair(high, low): string? local highVal = tonumber(high, 16) local lowVal = tonumber(low, 16) - if not highVal or not lowVal then return nil -- Invalid end - -- Calculate the actual Unicode codepoint local codepoint = 0x10000 + ((highVal - 0xD800) * 0x400) + (lowVal - 0xDC00) return utf8.char(codepoint) end local function deserializeString(state: DeserializerState): string + -- Must start with " + if currentByte(state) ~= string.byte('"') then + return deserializerError(state, "Expected string opening quote") + end + + -- Skip opening quote " state.cursor += 1 local startPos = state.cursor - if currentByte(state) == string.byte('"') then - state.cursor += 1 + while state.cursor <= #state.src do + local byte = currentByte(state) - return "" - end + -- Check for unescaped control characters (0x00-0x1F) + if byte < 0x20 then + return deserializerError(state, "Unescaped control character in string") + end - while state.cursor <= #state.src do - if currentByte(state) == string.byte('"') then + if byte == string.byte('"') then state.cursor += 1 - local source = string.sub(state.src, startPos, state.cursor - 2) + -- Validate escape sequences + -- Remove valid \\ first + local temp = string.gsub(source, "\\\\", "") + + -- Check for invalid single-char escapes + if temp:match('\\[^bfnrt"\\/u]') then + return deserializerError(state, "Invalid escape sequence") + end + + -- Check that all \u escapes have exactly 4 hex digits + if temp:match("\\u[^0-9a-fA-F]") or temp:match("\\u%x?%x?%x?$") then + return deserializerError(state, "Incomplete or invalid \\u escape") + end + + -- Handle all surrogate pairs source = string.gsub( source, - "\\u([dD]83[dD])\\u(d[cC]%w%w)", + "\\u([dD][89aAbB][0-9a-fA-F][0-9a-fA-F])\\u([dD][c-fC-F][0-9a-fA-F][0-9a-fA-F])", function(high, low) return decodeSurrogatePair(high, low) or deserializerError(state, "Invalid unicode surrogate pair") end :: any ) + + -- Detect incomplete/invalid surrogates + if source:match("\\u[dD][89aAbB][0-9a-fA-F][0-9a-fA-F]") then + return deserializerError(state, "Incomplete surrogate pair") + end + -- Handle regular Unicode escapes source = string.gsub(source, "\\u(%x%x%x%x)", function(code) return utf8.char(tonumber(code, 16) :: number) @@ -311,66 +352,85 @@ local function deserializeString(state: DeserializerState): string source = string.gsub(source, "\\r", "\r") source = string.gsub(source, "\\t", "\t") source = string.gsub(source, '\\"', '"') + source = string.gsub(source, "\\/", "/") source = string.gsub(source, "\0", "\\") return source end - if currentByte(state) == string.byte("\\") then + if byte == string.byte("\\") then state.cursor += 1 end state.cursor += 1 end - -- error - state.cursor = startPos - return deserializerError(state, "Unterminated string") end -local deserialize +local deserialize: (DeserializerState) -> (array | object | boolean | number | string)? + +local function deserializeArray(state: DeserializerState): array + if currentByte(state) ~= string.byte("[") then + return deserializerError(state, "Expected array opening '['") + end -local function deserializeArray(state: DeserializerState): Array state.cursor += 1 + skipWhitespace(state) - local current: Array = {} + local current: array = {} + local index = 1 - local expectingValue = false - while state.cursor < #state.src do - skipWhitespace(state) + -- empty array + if currentByte(state) == string.byte("]") then + state.cursor += 1 + return current + end - if currentByte(state) == string.byte(",") then - expectingValue = true - state.cursor += 1 - end + local expectingValue = true - skipWhitespace(state) + while state.cursor <= #state.src do + if not expectingValue then + -- Expect a comma or closing bracket + if currentByte(state) == string.byte(",") then + expectingValue = true + state.cursor += 1 + if not skipWhitespace(state) then + return deserializerError(state, "Unterminated array") + end + elseif currentByte(state) == string.byte("]") then + state.cursor += 1 + return current + else + return deserializerError(state, "Expected ',' or ']' after array value") + end + else + -- Expect a value next + if currentByte(state) == string.byte("]") then + if index == 1 then + -- empty array is ok + state.cursor += 1 + return current + else + return deserializerError(state, "Trailing comma in array") + end + end - if currentByte(state) == string.byte("]") then - break + table.insert(current, deserialize(state)) + index += 1 + expectingValue = false end - table.insert(current, deserialize(state)) - - expectingValue = false - end - - if expectingValue then - deserializerError(state, "Trailing comma") - end - - if not skipWhitespace(state) or currentByte(state) ~= string.byte("]") then - deserializerError(state, "Unterminated array") + if not skipWhitespace(state) then + return deserializerError(state, "Unterminated array") + end end - state.cursor += 1 - - return current + return deserializerError(state, "Unterminated array") end -local function deserializeObject(state: DeserializerState): Object +local function deserializeObject(state: DeserializerState): object state.cursor += 1 local current = {} @@ -430,29 +490,31 @@ local function deserializeObject(state: DeserializerState): Object return current end -deserialize = function(state: DeserializerState): Value +deserialize = function(state: DeserializerState): value skipWhitespace(state) local fourChars = string.sub(state.src, state.cursor, state.cursor + 3) - if fourChars == "null" then + if string.match(fourChars, "^null") then state.cursor += 4 - return json.NULL - elseif fourChars == "true" then + return json.null + elseif string.match(fourChars, "^true") then state.cursor += 4 return true - elseif string.sub(state.src, state.cursor, state.cursor + 4) == "false" then + elseif string.match(string.sub(state.src, state.cursor, state.cursor + 4), "^false") then state.cursor += 5 return false - elseif string.match(state.src, "^[%d%.]", state.cursor) then - -- number + elseif string.match(state.src, "^[%-%d]", state.cursor) then + -- potential number return deserializeNumber(state) - elseif string.byte(state.src, state.cursor) == string.byte('"') then + elseif string.match(state.src, '^"', state.cursor) then return deserializeString(state) - elseif string.byte(state.src, state.cursor) == string.byte("[") then + elseif string.match(state.src, "^%[", state.cursor) then return deserializeArray(state) - elseif string.byte(state.src, state.cursor) == string.byte("{") then - return deserializeObject(state) + elseif string.match(state.src, "^{", state.cursor) then + local obj = deserializeObject(state) + obj[object_key] = true + return obj end return deserializerError(state, `Unexpected token '{string.sub(state.src, state.cursor, state.cursor)}'`) @@ -460,9 +522,9 @@ end -- user-facing -json.serialize = function(value: Value, prettyPrint: boolean?) +function json.serialize(value: value, prettyPrint: boolean?) local state: SerializerState = { - buf = buffer.create(1024), + buf = buffer.create(bufferSize), cursor = 0, prettyPrint = prettyPrint or false, depth = 0, @@ -473,13 +535,46 @@ json.serialize = function(value: Value, prettyPrint: boolean?) return buffer.readstring(state.buf, 0, state.cursor) end -json.deserialize = function(src: string) +function json.deserialize(src: string) local state = { src = src, cursor = 0, } - return deserialize(state) + local result = deserialize(state) + + -- After parsing a value, there must be no non-whitespace trailing characters. + if skipWhitespace(state) then + deserializerError(state, "Trailing characters after JSON value") + end + + return result +end + +function json.object(props: { [string]: value }): object + local res: object = { [object_key] = true } + + for key, value in props do + res[key] = value + end + + return res +end + +function json.asobject(value: value): object? + if typeof(value) == "table" and value[object_key] then + return value :: object + end + + return nil +end + +function json.asarray(value: value): array? + if typeof(value) == "table" and not value[object_key] then + return value :: array + end + + return nil end return table.freeze(json) diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau new file mode 100644 index 000000000..932801aaf --- /dev/null +++ b/lute/std/libs/luau.luau @@ -0,0 +1,46 @@ +--!strict +-- @std/luau +-- stdlib for `@lute/luau` +-- Provides utilities for parsing and compiling Luau source code + +local luteLuau = require("@lute/luau") + +local fs = require("@lute/fs") +local path = require("@std/path") + +local luau = {} + +export type bytecode = luteLuau.Bytecode + +function luau.compile(source: string): bytecode + return luteLuau.compile(source) +end + +function luau.load(bytecode: bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any + return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) +end + +function luau.loadbypath(requirePath: path.pathlike, env: { [any]: any }?): any + local requirePathStr = if typeof(requirePath) == "string" then requirePath else path.format(requirePath) + + local migrationHandle = fs.open(requirePathStr, "r") + + local migrationBytecode = luau.compile(fs.read(migrationHandle)) + + fs.close(migrationHandle) + + return luau.load(migrationBytecode, requirePathStr, env)() +end + +function luau.typeofmodule(modulepath: path.pathlike): string + local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) + + return luteLuau.typeofmodule(modulePathStr) +end + +function luau.resolverequire(modulepath: string, frompath: path.pathlike): string + local frompathString = if typeof(frompath) == "string" then frompath else path.format(frompath) + return luteLuau.resolverequire(modulepath, `@{frompathString}`) +end + +return table.freeze(luau) diff --git a/lute/std/libs/net.luau b/lute/std/libs/net.luau new file mode 100644 index 000000000..0f1ea8b34 --- /dev/null +++ b/lute/std/libs/net.luau @@ -0,0 +1,16 @@ +--!strict +-- @std/net +-- stdlib for `@lute/net` + +local net = require("@lute/net") + +local netlib = {} + +export type metadata = net.Metadata +export type response = net.Response + +function netlib.request(url: string, metadata: metadata?): response + return net.request(url, metadata) +end + +return table.freeze(netlib) diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau new file mode 100644 index 000000000..78555415a --- /dev/null +++ b/lute/std/libs/path/init.luau @@ -0,0 +1,98 @@ +local platform = require("@std/system/platform") + +local posix = require("@self/posix") +local win32 = require("@self/win32") + +local pathtypes = require("@self/types") + +local pathlib = {} + +export type path = pathtypes.path +export type pathlike = pathtypes.pathlike + +local onWindows = platform.win32 + +function pathlib.basename(path: pathlike): string? + if onWindows then + return win32.basename(path :: win32.pathlike) + else + return posix.basename(path :: posix.pathlike) + end +end + +function pathlib.dirname(path: pathlike): string + if onWindows then + return win32.dirname(path :: win32.pathlike) + else + return posix.dirname(path :: posix.pathlike) + end +end + +function pathlib.extname(path: pathlike): string + if onWindows then + return win32.extname(path :: win32.pathlike) + else + return posix.extname(path :: posix.pathlike) + end +end + +function pathlib.format(path: pathlike): string + if onWindows then + return win32.format(path :: win32.pathlike) + else + return posix.format(path :: posix.pathlike) + end +end + +function pathlib.isabsolute(path: pathlike): boolean + if onWindows then + return win32.isabsolute(path :: win32.pathlike) + else + return posix.isabsolute(path :: posix.pathlike) + end +end + +function pathlib.join(...: pathlike): path + if onWindows then + return win32.join(...) + else + return posix.join(...) + end +end + +function pathlib.normalize(path: pathlike): path + if onWindows then + return win32.normalize(path :: win32.pathlike) + else + return posix.normalize(path :: posix.pathlike) + end +end + +function pathlib.parse(path: pathlike): path + if onWindows then + return win32.parse(path :: win32.pathlike) + else + return posix.parse(path :: posix.pathlike) + end +end + +function pathlib.relative(from: pathlike, to: pathlike): path + if onWindows then + return win32.relative(from :: win32.pathlike, to :: win32.pathlike) + else + return posix.relative(from :: posix.pathlike, to :: posix.pathlike) + end +end + +function pathlib.resolve(...: pathlike): path + if onWindows then + return win32.resolve(...) + else + return posix.resolve(...) + end +end + +pathlib.win32 = win32 +pathlib.posix = posix + +return table.freeze(pathlib) diff --git a/lute/std/libs/path/pathinterface.luau b/lute/std/libs/path/pathinterface.luau new file mode 100644 index 000000000..4bd143b01 --- /dev/null +++ b/lute/std/libs/path/pathinterface.luau @@ -0,0 +1,5 @@ +export type pathinterface = { + __tostring: () -> string, +} + +return table.freeze({}) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau new file mode 100644 index 000000000..0652b0b64 --- /dev/null +++ b/lute/std/libs/path/posix/init.luau @@ -0,0 +1,244 @@ +local process = require("@lute/process") +local posixtypes = require("@self/types") + +export type path = posixtypes.path +export type pathlike = posixtypes.pathlike + +local posix = {} +posix.pathmt = {} :: posixtypes.pathMT + +function posix.basename(path: pathlike): string? + path = if typeof(path) == "string" then posix.parse(path) else path + return if #path.parts > 0 then path.parts[#path.parts] else nil +end + +function posix.dirname(path: pathlike): string + path = if typeof(path) == "string" then posix.parse(path) else path + + if #path.parts == 0 then + return posix.format(path) + else + return posix.format({ + parts = { table.unpack(path.parts, 1, #path.parts - 1) }, + absolute = path.absolute, + }) + end +end + +function posix.extname(path: pathlike): string + path = if typeof(path) == "string" then posix.parse(path) else path + + if #path.parts == 0 then + return "" + end + + local filename = path.parts[#path.parts] + + if filename == "." or filename == ".." then + return "" + end + + local dotIndex, _ = filename:find("%.[^%.]*$") + if dotIndex == nil or dotIndex == 1 then + return "" + end + + return filename:sub(dotIndex) +end + +function posix.format(path: pathlike): string + if typeof(path) == "string" then + return path + end + + if #path.parts == 0 then + return if path.absolute then "/" else "." + else + return (if path.absolute then "/" else "") .. table.concat(path.parts, "/") + end +end + +function posix.isabsolute(path: pathlike): boolean + path = if typeof(path) == "string" then posix.parse(path) else path + return path.absolute +end + +-- In place extends path with addend +local function joinHelper(path: path, addend: pathlike): () + addend = if typeof(addend) == "string" then posix.parse(addend) else addend + + if addend.absolute then + local stringPath = posix.format(path) + local stringAddend = posix.format(addend) + error( + `Could not join {if path.absolute then "absolute" else "relative"} path {stringPath} and absolute path {stringAddend}` + ) + end + + table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) +end + +function posix.join(...: pathlike): path + local parts: { pathlike } = { ... } + if #parts == 0 then + return setmetatable({ + parts = {}, + absolute = false, + }, posix.pathmt) + end + + local path: path = if typeof(parts[1]) == "string" + then posix.parse(parts[1]) + else setmetatable({ + parts = table.clone((parts[1] :: path).parts), -- LUAUFIX: Shouldn't need cast + absolute = (parts[1] :: path).absolute, -- LUAUFIX: Shouldn't need cast + }, posix.pathmt) + + for i = 2, #parts do + joinHelper(path, parts[i]) + end + + return path +end + +function posix.normalize(path: pathlike): path + path = if typeof(path) == "string" then posix.parse(path) else path + + local newParts = {} + for _, part in path.parts do + if part == "" or part == "." then + continue + elseif part == ".." then + if path.absolute then + if #newParts > 0 then -- if absolute, only pop if we're not at the root + table.remove(newParts, #newParts) + end + else -- if a relative path, we keep .. prefixes + if #newParts > 0 and newParts[#newParts] ~= ".." then + table.remove(newParts, #newParts) + else + table.insert(newParts, "..") + end + end + else + table.insert(newParts, part) + end + end + + return setmetatable({ + parts = newParts, + absolute = path.absolute, + }, posix.pathmt) +end + +function posix.parse(path: pathlike): path + if typeof(path) == "table" then + return setmetatable(path, posix.pathmt) + end + if typeof(path) ~= "string" then + error("Expected string or path") + end + + local isAbs = false + + -- Check if path is absolute + isAbs = string.sub(path, 1, 1) == "/" + + local parts = {} + if isAbs then + -- Strip out the leading / for absolute paths + parts = path:sub(2):split("/") + else + parts = path:split("/") + end + + -- Filter out empty parts (can happen with leading/trailing separators) + if #parts > 0 then + if parts[#parts] == "" then + table.remove(parts, #parts) + end + if #parts > 0 and parts[1] == "" then + table.remove(parts, 1) + end + end + + return setmetatable({ + parts = parts, + absolute = isAbs, + }, posix.pathmt) +end + +function posix.resolve(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return posix.parse(process.cwd()) + end + + local path = posix.parse(parts[#parts]) + + for i = #parts - 1, 1, -1 do + if posix.isabsolute(path) then + break + end + + local segment = posix.parse(parts[i]) + + if #segment.parts == 0 then + continue + end + + joinHelper(segment, path) + path = segment + end + + if not posix.isabsolute(path) then + local cwd = posix.parse(process.cwd()) + joinHelper(cwd, path) + path = cwd + end + + return posix.normalize(path) +end + +function posix.relative(from: pathlike, to: pathlike): path + from = if typeof(from) == "string" then posix.parse(from) else from + to = if typeof(to) == "string" then posix.parse(to) else to + + if from.absolute ~= to.absolute then + error("Cannot compute relative path between absolute and relative paths") + end + + local commonPrefixLength = 0 + + for i = 1, math.min(#from.parts, #to.parts) do + if from.parts[i] ~= to.parts[i] then + break + end + commonPrefixLength += 1 + end + + local relativeParts = {} + + if #from.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #from.parts do + table.insert(relativeParts, "..") + end + end + + if #to.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #to.parts do + table.insert(relativeParts, to.parts[i]) + end + end + + return setmetatable({ + parts = relativeParts, + absolute = false, + }, posix.pathmt) +end + +function posix.pathmt:__tostring(): string + return posix.format(self :: path) +end + +return table.freeze(posix) diff --git a/lute/std/libs/path/posix/types.luau b/lute/std/libs/path/posix/types.luau new file mode 100644 index 000000000..f158038ac --- /dev/null +++ b/lute/std/libs/path/posix/types.luau @@ -0,0 +1,12 @@ +local pathinterface = require("../pathinterface") + +export type pathdata = { + parts: { string }, + absolute: boolean, +} + +export type path = setmetatable + +export type pathlike = string | path | pathdata + +return {} diff --git a/lute/std/libs/path/types.luau b/lute/std/libs/path/types.luau new file mode 100644 index 000000000..08cf91461 --- /dev/null +++ b/lute/std/libs/path/types.luau @@ -0,0 +1,7 @@ +local posixtypes = require("@std/path/posix/types") +local win32types = require("@std/path/win32/types") + +export type path = posixtypes.path | win32types.path +export type pathlike = string | path + +return {} diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau new file mode 100644 index 000000000..abf723d4f --- /dev/null +++ b/lute/std/libs/path/win32/init.luau @@ -0,0 +1,313 @@ +local process = require("@lute/process") +local win32types = require("@self/types") + +export type pathkind = win32types.pathkind +export type path = win32types.path +export type pathlike = win32types.pathlike + +local win32 = {} +win32.pathmt = {} :: win32types.pathMT + +function win32.basename(path: pathlike): string? + path = if typeof(path) == "string" then win32.parse(path) else path + return if #path.parts > 0 then path.parts[#path.parts] else nil +end + +function win32.dirname(path: pathlike): string + path = if typeof(path) == "string" then win32.parse(path) else path + + if #path.parts == 0 then + return win32.format(path) + else + return win32.format({ + parts = { table.unpack(path.parts, 1, #path.parts - 1) }, + kind = path.kind, + drive = path.drive, + }) + end +end + +function win32.extname(path: pathlike): string + path = if typeof(path) == "string" then win32.parse(path) else path + + if #path.parts == 0 then + return "" + end + + local filename = path.parts[#path.parts] + + if filename == "." or filename == ".." then + return "" + end + + local dotIndex, _ = filename:find("%.[^%.]*$") + if dotIndex == nil or dotIndex == 1 then + return "" + end + + return filename:sub(dotIndex) +end + +function win32.drive(path: pathlike): path + path = if typeof(path) == "string" then win32.parse(path) else path + + if path.drive == nil then + error("Path does not have a drive letter: " .. win32.format(path)) + end + + return setmetatable( + { + parts = {}, + kind = "absolute", + drive = path.drive, + } :: win32types.pathdata, + win32.pathmt + ) +end + +function win32.format(path: pathlike): string + if typeof(path) == "string" then + return path + end + + if #path.parts == 0 then + if path.kind == "unc" then + return "\\\\" + elseif path.drive ~= nil then + return table.concat({ path.drive, ":", (if path.kind == "absolute" then "\\" else "") }) + else + return "." + end + else + local parts = table.concat(path.parts, "\\") + if path.kind == "unc" then + return "\\\\" .. parts + elseif path.drive ~= nil then + return table.concat({ path.drive, ":", (if path.kind == "absolute" then "\\" else ""), parts }) + else + return parts + end + end +end + +function win32.isabsolute(path: pathlike): boolean + path = if typeof(path) == "string" then win32.parse(path) else path + return path.kind ~= "relative" +end + +-- In place extends path with addend +local function joinHelper(path: path, addend: pathlike): () + addend = if typeof(addend) == "string" then win32.parse(addend) else addend + + if addend.kind ~= "relative" then + error(`Could not join {path.kind} path {win32.format(path)} and {addend.kind} path {win32.format(addend)}`) + end + + local driveLettersIncompatible = path.drive ~= nil + and addend.drive ~= nil + and addend.drive:lower() ~= path.drive:lower() + + if driveLettersIncompatible then + error(`Could not join paths with incompatible drive letters: {win32.format(path)} and {win32.format(addend)}`) + end + + table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) + + if path.drive == nil and addend.drive ~= nil then + path.drive = addend.drive + end +end + +function win32.join(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return setmetatable( + { + parts = {}, + kind = "relative", + drive = nil, + } :: win32types.pathdata, -- Cast needed because of table invariance + win32.pathmt + ) + end + + local path: path = if typeof(parts[1]) == "string" + then win32.parse(parts[1]) + else setmetatable({ + parts = table.clone((parts[1] :: path).parts), -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + kind = (parts[1] :: path).kind, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + drive = (parts[1] :: path).drive, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + }, win32.pathmt) + + for i = 2, #parts do + joinHelper(path, parts[i]) + end + + return path +end + +function win32.normalize(path: pathlike): path + path = if typeof(path) == "string" then win32.parse(path) else path + + local newParts = {} + for _, part in path.parts do + if part == "" or part == "." then + continue + elseif part == ".." then + if path.kind == "unc" or path.kind == "absolute" then + if #newParts > 0 then -- if absolute, only pop if we're not at the root + table.remove(newParts, #newParts) + end + else -- if a relative path, we keep .. prefixes + if #newParts > 0 and newParts[#newParts] ~= ".." then + table.remove(newParts, #newParts) + else + table.insert(newParts, "..") + end + end + else + table.insert(newParts, part) + end + end + + return setmetatable({ + parts = newParts, + kind = path.kind, + drive = path.drive, + }, win32.pathmt) +end + +function win32.parse(path: pathlike): path + if typeof(path) == "table" then + return setmetatable(path, win32.pathmt) + end + if typeof(path) ~= "string" then + error("Expected string or path") + end + + local kind: pathkind = "relative" + local driveLetter = nil + + -- Check if path is absolute and if it's a unc path on Windows + if string.sub(path, 1, 2) == "\\\\" then + -- unc path + kind = "unc" + elseif string.match(path, "^%a:") ~= nil then + driveLetter = string.sub(path, 1, 1) + -- Windows: starts with drive letter + backslash (C:\) + kind = if string.sub(path, 3, 3) == "\\" then "absolute" else "relative" + end + + local parts = {} + -- Windows supports mixed separators, so sub forward slashes for backslashes first + path = path:gsub("/", "\\") + + if kind == "unc" then + -- Strip out the leading \\ for unc paths + parts = path:sub(3):split("\\") + elseif kind == "absolute" then + -- Strip out the drive letter, colon, and backslash (C:\) + parts = path:sub(4):split("\\") + elseif driveLetter ~= nil then + -- Strip out the drive letter and colon (C:) + parts = path:sub(3):split("\\") + else + parts = path:split("\\") + end + + -- Filter out empty parts (can happen with leading/trailing separators) + if #parts > 0 then + if parts[#parts] == "" then + table.remove(parts, #parts) + end + if #parts > 0 and parts[1] == "" then + table.remove(parts, 1) + end + end + + return setmetatable({ + parts = parts, + kind = kind, + drive = driveLetter, + }, win32.pathmt) +end + +function win32.relative(from: pathlike, to: pathlike): path + from = if typeof(from) == "string" then win32.parse(from) else from + to = if typeof(to) == "string" then win32.parse(to) else to + + if from.kind ~= to.kind then + error("Cannot compute relative path between different kinds of paths") + end + + if from.drive ~= to.drive then + error("Cannot compute relative path between different drives") + end + + local commonPrefixLength = 0 + + for i = 1, math.min(#from.parts, #to.parts) do + if from.parts[i] ~= to.parts[i] then + break + end + commonPrefixLength += 1 + end + + local relativeParts = {} + + if #from.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #from.parts do + table.insert(relativeParts, "..") + end + end + + if #to.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #to.parts do + table.insert(relativeParts, to.parts[i]) + end + end + + return setmetatable({ + parts = relativeParts, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) +end + +function win32.resolve(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return win32.parse(process.cwd()) + end + + local path = win32.parse(parts[#parts]) + + for i = #parts - 1, 1, -1 do + if win32.isabsolute(path) then + break + end + + local segment = win32.parse(parts[i]) + + if #segment.parts == 0 then + continue + end + + joinHelper(segment, path) + path = segment + end + + if not win32.isabsolute(path) then + local cwd = win32.parse(process.cwd()) + joinHelper(cwd, path) + path = cwd + end + + return win32.normalize(path) +end + +function win32.pathmt:__tostring(): string + return win32.format(self :: path) +end + +return table.freeze(win32) diff --git a/lute/std/libs/path/win32/types.luau b/lute/std/libs/path/win32/types.luau new file mode 100644 index 000000000..b07c6967b --- /dev/null +++ b/lute/std/libs/path/win32/types.luau @@ -0,0 +1,15 @@ +local pathinterface = require("../pathinterface") + +export type pathkind = "unc" | "absolute" | "relative" + +export type pathdata = { + parts: { string }, + kind: pathkind, + drive: string?, +} + +export type path = setmetatable + +export type pathlike = string | pathdata | path + +return {} diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau new file mode 100644 index 000000000..8f8a8883b --- /dev/null +++ b/lute/std/libs/process.luau @@ -0,0 +1,45 @@ +--!strict +-- @std/process +-- stdlib for `@lute/process` +-- Provides process-related utility functions that handles file paths as path types instead of strings + +local process = require("@lute/process") +local pathlib = require("@std/path") + +local processlib = {} + +export type stdiokind = process.StdioKind +export type processrunoptions = process.ProcessRunOptions +export type processsystemoptions = process.ProcessSystemOptions +export type processresult = process.ProcessResult +export type path = pathlib.path +export type pathlike = pathlib.pathlike + +function processlib.homedir(): path + return pathlib.parse(process.homedir()) +end + +function processlib.cwd(): path + return pathlib.parse(process.cwd()) +end + +function processlib.run(args: { string }, options: processrunoptions?): processresult + return process.run(args, options) +end + +function processlib.system(command: string, options: processsystemoptions?): processresult + return process.system(command, options) +end + +function processlib.exit(exitcode: number): never + return process.exit(exitcode) +end + +function processlib.execpath(): path + return pathlib.parse(process.execpath()) +end + +-- re-exports +processlib.env = process.env + +return table.freeze(processlib) diff --git a/lute/std/libs/stringext.luau b/lute/std/libs/stringext.luau new file mode 100644 index 000000000..ead98abb5 --- /dev/null +++ b/lute/std/libs/stringext.luau @@ -0,0 +1,59 @@ +--!strict +-- @std/stringext +-- stdlib for an extension of the built-in string library in Luau +-- Provides additional utility functions for string operations + +local stringext = {} + +function stringext.hasprefix(str: string, prefix: string): boolean + if #prefix > #str then + return false + end + + return str:sub(1, #prefix) == prefix +end + +function stringext.removeprefix(str: string, prefix: string): string + if not stringext.hasprefix(str, prefix) then + return str + end + + return str:sub(#prefix + 1) +end + +function stringext.hassuffix(str: string, suffix: string): boolean + return str:sub(-#suffix) == suffix +end + +function stringext.removesuffix(str: string, suffix: string): string + if not stringext.hassuffix(str, suffix) then + return str + end + + return str:sub(0, -#suffix - 1) +end + +function stringext.trim(str: string): string + -- Pattern is guaranteed to return a result + return str:match("^%s*(.-)%s*$") :: string +end + +-- Counts the number of occurrences of pattern in str between startPos and endPos (inclusive) +function stringext.count(str: string, pattern: string, startPos: number?, endPos: number?): number + if startPos and endPos then + str = str:sub(startPos, endPos) + elseif startPos then + str = str:sub(startPos) + elseif endPos then + str = str:sub(1, endPos) + end + + local count = 0 + for _ in str:gmatch(pattern) do + count += 1 + end + + return count +end + +return table.freeze(stringext) diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau new file mode 100644 index 000000000..246f127d3 --- /dev/null +++ b/lute/std/libs/syntax/init.luau @@ -0,0 +1,187 @@ +local parser = require("@self/parser") +local types = require("@self/types") + +local luau = require("@lute/luau") + +export type span = types.span + +local span = { + create = luau.span.create, +} + +function span.subsumes(haystack: span, needle: span): boolean + return ( + if haystack.beginline == needle.beginline + then haystack.begincolumn <= needle.begincolumn + else haystack.beginline < needle.beginline + ) + and ( + if haystack.endline == needle.endline + then haystack.endcolumn >= needle.endcolumn + else haystack.endline > needle.endline + ) +end + +export type Whitespace = types.Whitespace +export type SingleLineComment = types.SingleLineComment +export type MultiLineComment = types.MultiLineComment + +export type Trivia = types.Trivia + +export type Token = types.Token + +export type Eof = types.Eof + +export type Pair = types.Pair +export type Punctuated = types.Punctuated + +export type AstLocal = types.AstLocal + +export type AstExprGroup = types.AstExprGroup + +export type AstExprConstantNil = types.AstExprConstantNil + +export type AstExprConstantBool = types.AstExprConstantBool + +export type AstExprConstantNumber = types.AstExprConstantNumber + +export type AstExprConstantString = types.AstExprConstantString + +export type AstExprLocal = types.AstExprLocal + +export type AstExprGlobal = types.AstExprGlobal + +export type AstExprVarargs = types.AstExprVarargs + +export type AstExprCall = types.AstExprCall + +export type AstExprIndexName = types.AstExprIndexName + +export type AstExprIndexExpr = types.AstExprIndexExpr + +export type AstFunctionBody = types.AstFunctionBody + +export type AstExprAnonymousFunction = types.AstExprAnonymousFunction + +export type AstExprTableItemList = types.AstExprTableItemList + +export type AstExprTableItemRecord = types.AstExprTableItemRecord + +export type AstExprTableItemGeneral = types.AstExprTableItemGeneral + +export type AstExprTableItem = types.AstExprTableItem + +export type AstExprTable = types.AstExprTable + +export type AstExprUnary = types.AstExprUnary + +export type AstExprBinary = types.AstExprBinary + +export type AstExprInterpString = types.AstExprInterpString + +export type AstExprTypeAssertion = types.AstExprTypeAssertion + +export type AstElseIfExpr = types.AstElseIfExpr + +export type AstExprIfElse = types.AstExprIfElse + +export type AstExpr = types.AstExpr + +export type AstStatBlock = types.AstStatBlock + +export type AstStatDo = types.AstStatDo + +export type AstElseIfStat = types.AstElseIfStat + +export type AstStatIf = types.AstStatIf + +export type AstStatWhile = types.AstStatWhile + +export type AstStatRepeat = types.AstStatRepeat + +export type AstStatBreak = types.AstStatBreak + +export type AstStatContinue = types.AstStatContinue + +export type AstStatReturn = types.AstStatReturn + +export type AstStatExpr = types.AstStatExpr + +export type AstStatLocal = types.AstStatLocal + +export type AstStatFor = types.AstStatFor + +export type AstStatForIn = types.AstStatForIn + +export type AstStatAssign = types.AstStatAssign + +export type AstStatCompoundAssign = types.AstStatCompoundAssign + +export type AstAttribute = types.AstAttribute + +export type AstStatFunction = types.AstStatFunction + +export type AstStatLocalFunction = types.AstStatLocalFunction + +export type AstStatTypeAlias = types.AstStatTypeAlias + +export type AstStatTypeFunction = types.AstStatTypeFunction + +export type AstStat = types.AstStat + +export type AstGenericType = types.AstGenericType + +export type AstGenericTypePack = types.AstGenericTypePack + +export type AstTypeReference = types.AstTypeReference + +export type AstTypeSingletonBool = types.AstTypeSingletonBool + +export type AstTypeSingletonString = types.AstTypeSingletonString + +export type AstTypeTypeof = types.AstTypeTypeof + +export type AstTypeGroup = types.AstTypeGroup + +export type AstTypeOptional = types.AstTypeOptional + +export type AstTypeUnion = types.AstTypeUnion + +export type AstTypeIntersection = types.AstTypeIntersection + +export type AstTypeArray = types.AstTypeArray + +export type AstTypeTableItemIndexer = types.AstTypeTableItemIndexer + +export type AstTypeTableItemStringProperty = types.AstTypeTableItemStringProperty + +export type AstTypeTableItemProperty = types.AstTypeTableItemProperty + +export type AstTypeTableItem = types.AstTypeTableItem + +export type AstTypeTable = types.AstTypeTable + +export type AstTypeFunctionParameter = types.AstTypeFunctionParameter + +export type AstTypeFunction = types.AstTypeFunction + +export type AstType = types.AstType + +export type AstTypePackExplicit = types.AstTypePackExplicit + +export type AstTypePackGeneric = types.AstTypePackGeneric + +export type AstTypePackVariadic = types.AstTypePackVariadic + +export type AstTypePack = types.AstTypePack + +export type AstNode = types.AstNode + +export type ParseResult = types.ParseResult + +return table.freeze({ + parseblock = parser.parseblock, + parseexpr = parser.parseexpr, + parse = parser.parse, + span = table.freeze(span), +}) diff --git a/lute/std/libs/syntax/parser.luau b/lute/std/libs/syntax/parser.luau index 5c76d2620..713383574 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -1,28 +1,23 @@ --!strict +-- @std/syntax/parser +-- Parser for Luau source code into Luau AST nodes local luau = require("@lute/luau") +local types = require("./types") + +local parser = {} --- Parses Luau source code into an AstStatBlock -local function parse(source: string): luau.AstStatBlock +function parser.parseblock(source: string): types.AstStatBlock return luau.parse(source).root end -local function parseexpr(source: string): luau.AstExpr +function parser.parseexpr(source: string): types.AstExpr return luau.parseexpr(source) end -export type ParseResult = { - root: luau.AstStatBlock, - eof: luau.Eof, -} - -local function parsefile(source: string): ParseResult - local result = luau.parse(source) - return { root = result.root, eof = result.eof } +function parser.parse(source: string): types.ParseResult + return luau.parse(source) end -return { - parse = parse, - parseexpr = parseexpr, - parsefile = parsefile, -} +return table.freeze(parser) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index ada74933e..564878096 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -1,151 +1,207 @@ --!strict -local luau = require("@lute/luau") +-- @std/syntax/printer + +local triviaUtils = require("./utils/trivia") +local types = require("./types") local visitor = require("./visitor") +local printerLib = {} + +type PrintVisitor = visitor.Visitor & { + result: buffer, + cursor: number, + replacements: types.replacements, + write: (self: PrintVisitor, str: string) -> (), + printTrivia: (self: PrintVisitor, trivia: types.Trivia) -> (), + printTriviaList: (self: PrintVisitor, trivia: { types.Trivia }) -> (), + printToken: (self: PrintVisitor, token: types.Token) -> (), + printString: (self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) -> (), + printInterpolatedString: (self: PrintVisitor, expr: types.AstExprInterpString) -> (), + printReplacement: (self: PrintVisitor, node: types.AstNode, replacement: types.replacement) -> (), +} local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) end -local function printTrivia(trivia: luau.Trivia): string +local function printTrivia(self: PrintVisitor, trivia: types.Trivia) if trivia.tag == "whitespace" or trivia.tag == "comment" or trivia.tag == "blockcomment" then - return trivia.text + self:write(trivia.text) else - return exhaustiveMatch(trivia.tag) + exhaustiveMatch(trivia.tag) end end -local function printTriviaList(trivia: { luau.Trivia }) - local result = "" +local function printTriviaList(self: PrintVisitor, trivia: { types.Trivia }) for _, trivia in trivia do - result ..= printTrivia(trivia) + self:printTrivia(trivia) end - return result end -local function printToken(token: luau.Token): string - return printTriviaList(token.leadingTrivia) .. token.text .. printTriviaList(token.trailingTrivia) +local function printToken(self: PrintVisitor, token: types.Token) + if self.replacements[token] ~= nil then + self:printReplacement(token, self.replacements[token]) + return + end + + self:printTriviaList(token.leadingtrivia) + self:write(token.text) + self:printTriviaList(token.trailingtrivia) end -local function printString(expr: luau.AstExprConstantString): string - local result = printTriviaList(expr.leadingTrivia) - - if expr.quoteStyle == "single" then - result ..= `'{expr.text}'` - elseif expr.quoteStyle == "double" then - result ..= `"{expr.text}"` - elseif expr.quoteStyle == "block" then - local equals = string.rep("=", expr.blockDepth) - result ..= `[{equals}[{expr.text}]{equals}]` - elseif expr.quoteStyle == "interp" then - result ..= "`" .. expr.text .. "`" +local function printString(self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) + if self.replacements[expr] ~= nil then + self:printReplacement(expr, self.replacements[expr]) + return + end + + self:printTriviaList(expr.leadingtrivia) + + if expr.quotestyle == "single" then + self:write(`'{expr.text}'`) + elseif expr.quotestyle == "double" then + self:write(`"{expr.text}"`) + elseif expr.quotestyle == "block" then + local equals = string.rep("=", expr.blockdepth) + self:write(`[{equals}[{expr.text}]{equals}]`) + elseif expr.quotestyle == "interp" then + self:write("`" .. expr.text .. "`") else - return exhaustiveMatch(expr.quoteStyle) + exhaustiveMatch(expr.quotestyle) end - result ..= printTriviaList(expr.trailingTrivia) - return result + self:printTriviaList(expr.trailingtrivia) end -local function printInterpolatedString(expr: luau.AstExprInterpString): string - local result = "" +local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprInterpString) + if self.replacements[expr] ~= nil then + self:printReplacement(expr, self.replacements[expr]) + return + end for i = 1, #expr.strings do - result ..= printTriviaList(expr.strings[i].leadingTrivia) + self:printTriviaList(expr.strings[i].leadingtrivia) if i == 1 then - result ..= "`" + self:write("`") else - result ..= "}" + self:write("}") end - result ..= expr.strings[i].text + self:write(expr.strings[i].text) if i == #expr.strings then - result ..= "`" - result ..= printTriviaList(expr.strings[i].trailingTrivia) + self:write("`") + self:printTriviaList(expr.strings[i].trailingtrivia) else - result ..= "{" - result ..= printTriviaList(expr.strings[i].trailingTrivia) - result ..= printExpr(expr.expressions[i]) + self:write("{") + self:printTriviaList(expr.strings[i].trailingtrivia) + visitor.visitexpression(expr.expressions[i], self) end end +end + +local function printReplacement(self: PrintVisitor, node: types.AstNode, replacement: types.replacement) + local leftmostTrivia = triviaUtils.leftmosttrivia(node) + if leftmostTrivia ~= nil then + self:printTriviaList(leftmostTrivia) + end - return result + if typeof(replacement) == "string" then + self:write(replacement) + elseif replacement.kind ~= nil then -- LUAUFIX: type solver upset comparing string singleton union to nil + -- TODO: preserve trivia + visitor.visit(replacement, self) + else + error("Unsupported replacement type") + end + + local rightmostTrivia = triviaUtils.rightmosttrivia(node) + if rightmostTrivia ~= nil then + self:printTriviaList(rightmostTrivia) + end end -type PrintVisitor = visitor.Visitor & { - result: buffer, - cursor: number, -} +local function write(self: PrintVisitor, str: string) + local totalSize = self.cursor + #str + local bufferSize = buffer.len(self.result) -local function printVisitor() - local printer = visitor.createVisitor() :: PrintVisitor + if totalSize >= bufferSize then + repeat + bufferSize *= 2 + until bufferSize >= totalSize - printer.result = buffer.create(1024) - printer.cursor = 0 + local newBuffer = buffer.create(bufferSize) + buffer.copy(newBuffer, 0, self.result) + self.result = newBuffer + end - local function write(str: string) - local totalSize = printer.cursor + #str - local bufferSize = buffer.len(printer.result) + buffer.writestring(self.result, self.cursor, str) + self.cursor = totalSize +end - if totalSize >= bufferSize then - repeat - bufferSize *= 2 - until bufferSize >= totalSize +local function printVisitor(replacements: types.replacements?) + local result = buffer.create(1024) + local cursor = 0 - local newBuffer = buffer.create(bufferSize) - buffer.copy(newBuffer, 0, printer.result) - printer.result = newBuffer - end + local printer: PrintVisitor = {} :: PrintVisitor - buffer.writestring(printer.result, printer.cursor, str) - printer.cursor = totalSize + -- The visitor library doesn't use methods, so we access replacements through a closure + local function maybeReplace(node: types.AstNode): boolean + if replacements == nil then + return true + end + local replacement = replacements[node] + if replacement == nil then + return true + end + printer:printReplacement(node, replacement) + return false end - printer.visitToken = function(node: luau.Token) - write(printToken(node)) + printer = visitor.create(maybeReplace) :: PrintVisitor + + printer.result = result + printer.cursor = cursor + printer.replacements = if replacements ~= nil then replacements else {} :: types.replacements + printer.write = write + printer.printTrivia = printTrivia + printer.printTriviaList = printTriviaList + printer.printToken = printToken + printer.printString = printString + printer.printInterpolatedString = printInterpolatedString + printer.printReplacement = printReplacement + printer.visitExprInterpString = function(node: types.AstExprInterpString) + printer:printInterpolatedString(node) return false end - - printer.visitString = function(node: luau.AstExprConstantString) - write(printString(node)) + printer.visitTypeSingletonString = function(node: types.AstTypeSingletonString) + printer:printString(node) return false end - - printer.visitTypeString = function(node: luau.AstTypeSingletonString) - write(printString(node)) + printer.visitToken = function(node: types.Token) + printer:printToken(node) return false end - - printer.visitInterpolatedString = function(node: luau.AstExprInterpString) - write(printInterpolatedString(node)) + printer.visitExprConstantString = function(node: types.AstExprConstantString) + printer:printString(node) return false end return printer end ---- Returns a string representation of an AstStatBlock -local function printBlock(block: luau.AstStatBlock): string - local printer = printVisitor() - visitor.visitBlock(block, printer) +function printerLib.printnode(node: types.AstNode, replacements: types.replacements?): string + local printer = printVisitor(replacements) + visitor.visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) end ---- Returns a string representation of an AstExpr -function printExpr(block: luau.AstExpr): string - local printer = printVisitor() - visitor.visitExpression(block, printer) +function printerLib.printfile( + result: { root: types.AstStatBlock, eof: types.Eof }, + replacements: types.replacements? +): string + local printer = printVisitor(replacements) + visitor.visitblock(result.root, printer) + visitor.visittoken(result.eof, printer) return buffer.readstring(printer.result, 0, printer.cursor) end -function printFile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string - local printer = printVisitor() - visitor.visitBlock(result.root, printer) - visitor.visitToken(result.eof, printer) - return buffer.readstring(printer.result, 0, printer.cursor) -end - -return { - print = printBlock, - printexpr = printExpr, - printfile = printFile, -} +return table.freeze(printerLib) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau new file mode 100644 index 000000000..4be2b7ac4 --- /dev/null +++ b/lute/std/libs/syntax/query.luau @@ -0,0 +1,151 @@ +--!strict +-- @std/syntax/query +-- Provides utility functions for querying Luau AST nodes + +local tableext = require("@std/tableext") +local types = require("./types") +local visitor = require("./visitor") +local utils = require("./utils") + +type node = types.AstNode + +export type query = { + nodes: { T }, + filter: (self: query, pred: (T) -> boolean) -> query, + replace: (self: query, repl: (T) -> types.replacement?) -> types.replacements, + map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported + foreach: (self: query, callback: (T) -> ()) -> query, + findall: (self: query, fn: (node) -> U?) -> query, + flatmap: (self: query, fn: (T) -> { U }) -> query, + maptoarray: (self: query, fn: (T) -> U?) -> { U }, +} + +local queryLib = {} + +function queryLib.filter(self: query, pred: (T) -> boolean): query + local newNodes = {} + for _, node in self.nodes do + if pred(node) then + table.insert(newNodes, node) + end + end + self.nodes = newNodes + return self +end + +function queryLib.replace(self: query, repl: (T) -> types.replacement?): types.replacements + local replacements = {} + for _, node in self.nodes do + local r = repl(node) + if r ~= nil then + replacements[node] = r + end + end + return replacements +end + +-- map will return a query where its nodes are all the non-nil values returned by fn when called on each node of the original query +function queryLib.map(self: query, fn: (T) -> U?): query + local newNodes = {} + for _, node in self.nodes do + local mapped = fn(node) + if mapped ~= nil then + table.insert(newNodes, mapped) + end + end + self.nodes = newNodes + return self +end + +function queryLib.foreach(self: query, callback: (T) -> ()): query + for _, node in self.nodes do + callback(node) + end + return self +end + +local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visitor + local function visit(n: node) -- LUAUFIX: type checker doesn't like assigning visit in the visitor fields with n: node + local selected = fn(n) + if selected ~= nil then + table.insert(nodes, selected) + end + return true + end + + local selectVisitor = visitor.create(visit) + selectVisitor.visitStatBlockEnd = function(n) end + selectVisitor.visitStatLocalDeclarationEnd = function(n) end + selectVisitor.visitExpr = function(n) + return true + end + selectVisitor.visitExprEnd = function(n) end + selectVisitor.visitToken = function(n) + if utils.isBaseToken(n) then + -- only visit if it is a base token, so we don't double count + return visit(n) + end + return true + end + + return selectVisitor +end + +function queryLib.findall(self: query, fn: (node) -> U?): query + local selectedNodes: { U } = {} + + local selectVisitor = newSelectVisitor(selectedNodes, fn) + + for _, node in self.nodes do + visitor.visit(node, selectVisitor) + end + + self.nodes = selectedNodes + + return self +end + +function queryLib.flatmap(self: query, fn: (T) -> { U }): query + local newNodes: { U } = {} + for _, node in self.nodes do + local mapped = fn(node) + if #mapped > 0 then + tableext.extend(newNodes, mapped) + end + end + self.nodes = newNodes + return self +end + +function queryLib.maptoarray(self: query, fn: (T) -> U?): { U } + local result = {} + for _, node in self.nodes do + local mapped = fn(node) + if mapped ~= nil then + table.insert(result, mapped) + end + end + return result +end + +function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) -> T?): query + local nodes: { T } = {} + + local selectVisitor = newSelectVisitor(nodes, fn) + + local root: node = if ast.root ~= nil then ast.root else ast + visitor.visit(root, selectVisitor) + + return { + nodes = nodes, + filter = queryLib.filter, + replace = queryLib.replace, + map = queryLib.map, + foreach = queryLib.foreach, + findall = queryLib.findall, + flatmap = queryLib.flatmap, + maptoarray = queryLib.maptoarray, + } -- LUAUFIX: queryLib.map has generics quantified at a different level than expected +end + +return table.freeze(queryLib) diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau new file mode 100644 index 000000000..bb3be3adc --- /dev/null +++ b/lute/std/libs/syntax/types.luau @@ -0,0 +1,165 @@ +local luau = require("@lute/luau") + +export type span = luau.span + +export type Whitespace = luau.Whitespace +export type SingleLineComment = luau.SingleLineComment +export type MultiLineComment = luau.MultiLineComment + +export type Trivia = luau.Trivia + +export type Token = luau.Token + +export type Eof = luau.Eof + +export type Pair = luau.Pair +export type Punctuated = luau.Punctuated + +export type AstLocal = luau.AstLocal + +export type AstExprGroup = luau.AstExprGroup + +export type AstExprConstantNil = luau.AstExprConstantNil + +export type AstExprConstantBool = luau.AstExprConstantBool + +export type AstExprConstantNumber = luau.AstExprConstantNumber + +export type AstExprConstantString = luau.AstExprConstantString + +export type AstExprLocal = luau.AstExprLocal + +export type AstExprGlobal = luau.AstExprGlobal + +export type AstExprVarargs = luau.AstExprVarargs + +export type AstExprCall = luau.AstExprCall + +export type AstExprIndexName = luau.AstExprIndexName + +export type AstExprIndexExpr = luau.AstExprIndexExpr + +export type AstFunctionBody = luau.AstFunctionBody + +export type AstExprAnonymousFunction = luau.AstExprAnonymousFunction + +export type AstExprTableItemList = luau.AstExprTableItemList + +export type AstExprTableItemRecord = luau.AstExprTableItemRecord + +export type AstExprTableItemGeneral = luau.AstExprTableItemGeneral + +export type AstExprTableItem = luau.AstExprTableItem + +export type AstExprTable = luau.AstExprTable + +export type AstExprUnary = luau.AstExprUnary + +export type AstExprBinary = luau.AstExprBinary + +export type AstExprInterpString = luau.AstExprInterpString + +export type AstExprTypeAssertion = luau.AstExprTypeAssertion + +export type AstElseIfExpr = luau.AstElseIfExpr + +export type AstExprIfElse = luau.AstExprIfElse + +export type AstExpr = luau.AstExpr + +export type AstStatBlock = luau.AstStatBlock + +export type AstStatDo = luau.AstStatDo + +export type AstElseIfStat = luau.AstElseIfStat + +export type AstStatIf = luau.AstStatIf + +export type AstStatWhile = luau.AstStatWhile + +export type AstStatRepeat = luau.AstStatRepeat + +export type AstStatBreak = luau.AstStatBreak + +export type AstStatContinue = luau.AstStatContinue + +export type AstStatReturn = luau.AstStatReturn + +export type AstStatExpr = luau.AstStatExpr + +export type AstStatLocal = luau.AstStatLocal + +export type AstStatFor = luau.AstStatFor + +export type AstStatForIn = luau.AstStatForIn + +export type AstStatAssign = luau.AstStatAssign + +export type AstStatCompoundAssign = luau.AstStatCompoundAssign + +export type AstAttribute = luau.AstAttribute + +export type AstStatFunction = luau.AstStatFunction + +export type AstStatLocalFunction = luau.AstStatLocalFunction + +export type AstStatTypeAlias = luau.AstStatTypeAlias + +export type AstStatTypeFunction = luau.AstStatTypeFunction + +export type AstStat = luau.AstStat + +export type AstGenericType = luau.AstGenericType + +export type AstGenericTypePack = luau.AstGenericTypePack + +export type AstTypeReference = luau.AstTypeReference + +export type AstTypeSingletonBool = luau.AstTypeSingletonBool + +export type AstTypeSingletonString = luau.AstTypeSingletonString + +export type AstTypeTypeof = luau.AstTypeTypeof + +export type AstTypeGroup = luau.AstTypeGroup + +export type AstTypeOptional = luau.AstTypeOptional + +export type AstTypeUnion = luau.AstTypeUnion + +export type AstTypeIntersection = luau.AstTypeIntersection + +export type AstTypeArray = luau.AstTypeArray + +export type AstTypeTableItemIndexer = luau.AstTypeTableItemIndexer + +export type AstTypeTableItemStringProperty = luau.AstTypeTableItemStringProperty + +export type AstTypeTableItemProperty = luau.AstTypeTableItemProperty + +export type AstTypeTableItem = luau.AstTypeTableItem + +export type AstTypeTable = luau.AstTypeTable + +export type AstTypeFunctionParameter = luau.AstTypeFunctionParameter + +export type AstTypeFunction = luau.AstTypeFunction + +export type AstType = luau.AstType + +export type AstTypePackExplicit = luau.AstTypePackExplicit + +export type AstTypePackGeneric = luau.AstTypePackGeneric + +export type AstTypePackVariadic = luau.AstTypePackVariadic + +export type AstTypePack = luau.AstTypePack + +export type AstNode = luau.AstNode + +export type ParseResult = luau.ParseResult + +export type replacement = string | AstNode +export type replacements = { [AstNode]: replacement } + +return {} diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau new file mode 100644 index 000000000..1518450b1 --- /dev/null +++ b/lute/std/libs/syntax/utils/init.luau @@ -0,0 +1,245 @@ +local types = require("./types") + +local utils = {} + +function utils.isLocal(n: types.AstNode): types.AstLocal? + return if n.kind == "local" then n else nil +end + +function utils.isExprGroup(n: types.AstNode): types.AstExprGroup? + return if n.kind == "expr" and n.tag == "group" then n else nil +end + +function utils.isExprConstantNil(n: types.AstNode): types.AstExprConstantNil? + return if n.kind == "expr" and n.tag == "nil" then n else nil +end + +function utils.isExprConstantBool(n: types.AstNode): types.AstExprConstantBool? + return if n.kind == "expr" and n.tag == "boolean" then n else nil +end + +function utils.isExprConstantNumber(n: types.AstNode): types.AstExprConstantNumber? + return if n.kind == "expr" and n.tag == "number" then n else nil +end + +function utils.isExprConstantString(n: types.AstNode): types.AstExprConstantString? + return if n.kind == "expr" and n.tag == "string" then n else nil +end + +function utils.isExprLocal(n: types.AstNode): types.AstExprLocal? + return if n.kind == "expr" and n.tag == "local" then n else nil +end + +function utils.isExprGlobal(n: types.AstNode): types.AstExprGlobal? + return if n.kind == "expr" and n.tag == "global" then n else nil +end + +function utils.isExprVarargs(n: types.AstNode): types.AstExprVarargs? + return if n.kind == "expr" and n.tag == "vararg" then n else nil +end + +function utils.isExprCall(n: types.AstNode): types.AstExprCall? + return if n.kind == "expr" and n.tag == "call" then n else nil +end + +function utils.isExprIndexName(n: types.AstNode): types.AstExprIndexName? + return if n.kind == "expr" and n.tag == "indexname" then n else nil +end + +function utils.isExprIndexExpr(n: types.AstNode): types.AstExprIndexExpr? + return if n.kind == "expr" and n.tag == "index" then n else nil +end + +function utils.isExprAnonymousFunction(n: types.AstNode): types.AstExprAnonymousFunction? + return if n.kind == "expr" and n.tag == "function" then n else nil +end + +function utils.isExprTable(n: types.AstNode): types.AstExprTable? + return if n.kind == "expr" and n.tag == "table" then n else nil +end + +function utils.isExprTableItem(n: types.AstNode): types.AstExprTableItem? + return if n.istableitem then n else nil +end + +function utils.isExprUnary(n: types.AstNode): types.AstExprUnary? + return if n.kind == "expr" and n.tag == "unary" then n else nil +end + +function utils.isExprBinary(n: types.AstNode): types.AstExprBinary? + return if n.kind == "expr" and n.tag == "binary" then n else nil +end + +function utils.isExprInterpString(n: types.AstNode): types.AstExprInterpString? + return if n.kind == "expr" and n.tag == "interpolatedstring" then n else nil +end + +function utils.isExprTypeAssertion(n: types.AstNode): types.AstExprTypeAssertion? + return if n.kind == "expr" and n.tag == "cast" then n else nil +end + +function utils.isExprIfElse(n: types.AstNode): types.AstExprIfElse? + return if n.kind == "expr" and n.tag == "conditional" then n else nil +end + +function utils.isExpr(n: types.AstNode): types.AstExpr? + return if n.kind == "expr" then n else nil +end + +function utils.isStatBlock(n: types.AstNode): types.AstStatBlock? + return if n.kind == "stat" and n.tag == "block" then n else nil +end + +function utils.isStatIf(n: types.AstNode): types.AstStatIf? + return if n.kind == "stat" and n.tag == "conditional" then n else nil +end + +function utils.isStatWhile(n: types.AstNode): types.AstStatWhile? + return if n.kind == "stat" and n.tag == "while" then n else nil +end + +function utils.isStatRepeat(n: types.AstNode): types.AstStatRepeat? + return if n.kind == "stat" and n.tag == "repeat" then n else nil +end + +function utils.isStatBreak(n: types.AstNode): types.AstStatBreak? + return if n.kind == "stat" and n.tag == "break" then n else nil +end + +function utils.isStatContinue(n: types.AstNode): types.AstStatContinue? + return if n.kind == "stat" and n.tag == "continue" then n else nil +end + +function utils.isStatReturn(n: types.AstNode): types.AstStatReturn? + return if n.kind == "stat" and n.tag == "return" then n else nil +end + +function utils.isStatExpr(n: types.AstNode): types.AstStatExpr? + return if n.kind == "stat" and n.tag == "expression" then n else nil +end + +function utils.isStatLocal(n: types.AstNode): types.AstStatLocal? + return if n.kind == "stat" and n.tag == "local" then n else nil +end + +function utils.isStatFor(n: types.AstNode): types.AstStatFor? + return if n.kind == "stat" and n.tag == "for" then n else nil +end + +function utils.isStatForIn(n: types.AstNode): types.AstStatForIn? + return if n.kind == "stat" and n.tag == "forin" then n else nil +end + +function utils.isStatAssign(n: types.AstNode): types.AstStatAssign? + return if n.kind == "stat" and n.tag == "assign" then n else nil +end + +function utils.isStatCompoundAssign(n: types.AstNode): types.AstStatCompoundAssign? + return if n.kind == "stat" and n.tag == "compoundassign" then n else nil +end + +function utils.isStatFunction(n: types.AstNode): types.AstStatFunction? + return if n.kind == "stat" and n.tag == "function" then n else nil +end + +function utils.isStatLocalFunction(n: types.AstNode): types.AstStatLocalFunction? + return if n.kind == "stat" and n.tag == "localfunction" then n else nil +end + +function utils.isStatTypeAlias(n: types.AstNode): types.AstStatTypeAlias? + return if n.kind == "stat" and n.tag == "typealias" then n else nil +end + +function utils.isStatTypeFunction(n: types.AstNode): types.AstStatTypeFunction? + return if n.kind == "stat" and n.tag == "typefunction" then n else nil +end + +function utils.isStat(n: types.AstNode): types.AstStat? + return if n.kind == "stat" then n else nil +end + +function utils.isGenericType(n: types.AstNode): types.AstGenericType? + return if n.kind == "type" and n.tag == "generic" then n else nil +end + +function utils.isGenericTypePack(n: types.AstNode): types.AstGenericTypePack? + return if n.kind == "typepack" and n.tag == "genericpack" then n else nil +end + +function utils.isTypeReference(n: types.AstNode): types.AstTypeReference? + return if n.kind == "type" and n.tag == "reference" then n else nil +end + +function utils.isTypeSingletonBool(n: types.AstNode): types.AstTypeSingletonBool? + return if n.kind == "type" and n.tag == "boolean" then n else nil +end + +function utils.isTypeSingletonString(n: types.AstNode): types.AstTypeSingletonString? + return if n.kind == "type" and n.tag == "string" then n else nil +end + +function utils.isTypeTypeof(n: types.AstNode): types.AstTypeTypeof? + return if n.kind == "type" and n.tag == "typeof" then n else nil +end + +function utils.isTypeGroup(n: types.AstNode): types.AstTypeGroup? + return if n.kind == "type" and n.tag == "group" then n else nil +end + +function utils.isTypeOptional(n: types.AstNode): types.AstTypeOptional? + return if n.kind == "type" and n.tag == "optional" then n else nil +end + +function utils.isTypeUnion(n: types.AstNode): types.AstTypeUnion? + return if n.kind == "type" and n.tag == "union" then n else nil +end + +function utils.isTypeIntersection(n: types.AstNode): types.AstTypeIntersection? + return if n.kind == "type" and n.tag == "intersection" then n else nil +end + +function utils.isTypeArray(n: types.AstNode): types.AstTypeArray? + return if n.kind == "type" and n.tag == "array" then n else nil +end + +function utils.isTypeTable(n: types.AstNode): types.AstTypeTable? + return if n.kind == "type" and n.tag == "table" then n else nil +end + +function utils.isTypeFunction(n: types.AstNode): types.AstTypeFunction? + return if n.kind == "type" and n.tag == "function" then n else nil +end + +function utils.isType(n: types.AstNode): types.AstType? + return if n.kind == "type" then n else nil +end + +function utils.isTypePackExplicit(n: types.AstNode): types.AstTypePackExplicit? + return if n.kind == "typepack" and n.tag == "explicit" then n else nil +end + +function utils.isTypePackVariadic(n: types.AstNode): types.AstTypePackVariadic? + return if n.kind == "typepack" and n.tag == "variadic" then n else nil +end + +function utils.isTypePackGeneric(n: types.AstNode): types.AstTypePackGeneric? + return if n.kind == "typepack" and n.tag == "generic" then n else nil +end + +function utils.isTypePack(n: types.AstNode): types.AstTypePack? + return if n.kind == "typepack" then n else nil +end + +function utils.isToken(n: types.AstNode): types.Token? + return if n.istoken then n else nil +end + +function utils.isBaseToken(n: types.AstNode): types.Token? + return if n.istoken and not n.kind then n else nil +end + +function utils.isAttribute(n: types.AstAttribute): types.AstAttribute? + return if n.kind == "attribute" then n else nil +end + +return table.freeze(utils) diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau new file mode 100644 index 000000000..4f9a81a32 --- /dev/null +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -0,0 +1,52 @@ +local query = require("../query") +local types = require("../types") + +local retrieverLib = {} + +function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } + local q: query.query = query.findallfromroot(n, function(n) + return if n.istoken ~= nil and n.istoken then n else nil + end) + + local leftmostToken: types.Token? = nil + + for _, token in q.nodes do + if leftmostToken == nil then + leftmostToken = token + elseif token.location.beginline < leftmostToken.location.beginline then + leftmostToken = token + elseif + token.location.beginline == leftmostToken.location.beginline + and token.location.begincolumn < leftmostToken.location.begincolumn + then + leftmostToken = token + end + end + + return if leftmostToken ~= nil then leftmostToken.leadingtrivia else {} +end + +function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } + local q: query.query = query.findallfromroot(n, function(n) + return if n.istoken ~= nil and n.istoken then n else nil + end) + + local rightmostToken: types.Token? = nil + + for _, token in q.nodes do + if rightmostToken == nil then + rightmostToken = token + elseif token.location.beginline > rightmostToken.location.beginline then + rightmostToken = token + elseif + token.location.beginline == rightmostToken.location.beginline + and token.location.begincolumn > rightmostToken.location.begincolumn + then + rightmostToken = token + end + end + + return if rightmostToken ~= nil then rightmostToken.trailingtrivia else {} +end + +return table.freeze(retrieverLib) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index f0f163ebd..fd90b82d7 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1,64 +1,76 @@ --!strict +-- @std/syntax/visitor +-- Visitor pattern implementation for traversing Luau AST nodes -local luau = require("@lute/luau") +local types = require("./types") + +local visitorlib = {} export type Visitor = { - visitBlock: (luau.AstStatBlock) -> boolean, - visitBlockEnd: (luau.AstStatBlock) -> (), - visitIf: (luau.AstStatIf) -> boolean, - visitWhile: (luau.AstStatWhile) -> boolean, - visitRepeat: (luau.AstStatRepeat) -> boolean, - visitReturn: (luau.AstStatReturn) -> boolean, - visitLocalDeclaration: (luau.AstStatLocal) -> boolean, - visitLocalDeclarationEnd: (luau.AstStatLocal) -> (), - visitFor: (luau.AstStatFor) -> boolean, - visitForIn: (luau.AstStatForIn) -> boolean, - visitAssign: (luau.AstStatAssign) -> boolean, - visitCompoundAssign: (luau.AstStatCompoundAssign) -> boolean, - visitFunction: (luau.AstStatFunction) -> boolean, - visitLocalFunction: (luau.AstStatLocalFunction) -> boolean, - visitTypeAlias: (luau.AstStatTypeAlias) -> boolean, - visitStatTypeFunction: (luau.AstStatTypeFunction) -> boolean, - - visitExpression: (luau.AstExpr) -> boolean, - visitExpressionEnd: (luau.AstExpr) -> (), - visitLocalReference: (luau.AstExprLocal) -> boolean, - visitGlobal: (luau.AstExprGlobal) -> boolean, - visitCall: (luau.AstExprCall) -> boolean, - visitUnary: (luau.AstExprUnary) -> boolean, - visitBinary: (luau.AstExprBinary) -> boolean, - visitAnonymousFunction: (luau.AstExprAnonymousFunction) -> boolean, - visitTableItem: (luau.AstExprTableItem) -> boolean, - visitTable: (luau.AstExprTable) -> boolean, - visitIndexName: (luau.AstExprIndexName) -> boolean, - visitIndexExpr: (luau.AstExprIndexExpr) -> boolean, - visitGroup: (luau.AstExprGroup) -> boolean, - visitInterpolatedString: (luau.AstExprInterpString) -> boolean, - visitTypeAssertion: (luau.AstExprTypeAssertion) -> boolean, - visitIfExpression: (luau.AstExprIfElse) -> boolean, - - visitTypeReference: (luau.AstTypeReference) -> boolean, - visitTypeBoolean: (luau.AstTypeSingletonBool) -> boolean, - visitTypeString: (luau.AstTypeSingletonString) -> boolean, - visitTypeTypeof: (luau.AstTypeTypeof) -> boolean, - visitTypeGroup: (luau.AstTypeGroup) -> boolean, - visitTypeUnion: (luau.AstTypeUnion) -> boolean, - visitTypeIntersection: (luau.AstTypeIntersection) -> boolean, - visitTypeArray: (luau.AstTypeArray) -> boolean, - visitTypeTable: (luau.AstTypeTable) -> boolean, - visitTypeFunction: (luau.AstTypeFunction) -> boolean, - - visitTypePackExplicit: (luau.AstTypePackExplicit) -> boolean, - visitTypePackGeneric: (luau.AstTypePackGeneric) -> boolean, - visitTypePackVariadic: (luau.AstTypePackVariadic) -> boolean, - - visitToken: (luau.Token) -> boolean, - visitNil: (luau.AstExprConstantNil) -> boolean, - visitString: (luau.AstExprConstantString) -> boolean, - visitBoolean: (luau.AstExprConstantBool) -> boolean, - visitNumber: (luau.AstExprConstantNumber) -> boolean, - visitLocal: (luau.AstLocal) -> boolean, - visitVarargs: (luau.AstExprVarargs) -> boolean, + visitStatBlock: (types.AstStatBlock) -> boolean, + visitStatBlockEnd: (types.AstStatBlock) -> (), + visitStatDo: (types.AstStatDo) -> boolean, + visitStatIf: (types.AstStatIf) -> boolean, + visitStatWhile: (types.AstStatWhile) -> boolean, + visitStatRepeat: (types.AstStatRepeat) -> boolean, + visitStatBreak: (types.AstStatBreak) -> boolean, + visitStatContinue: (types.AstStatContinue) -> boolean, + visitStatReturn: (types.AstStatReturn) -> boolean, + visitStatLocalDeclaration: (types.AstStatLocal) -> boolean, + visitStatLocalDeclarationEnd: (types.AstStatLocal) -> (), + visitStatFor: (types.AstStatFor) -> boolean, + visitStatForIn: (types.AstStatForIn) -> boolean, + visitStatAssign: (types.AstStatAssign) -> boolean, + visitStatCompoundAssign: (types.AstStatCompoundAssign) -> boolean, + visitStatFunction: (types.AstStatFunction) -> boolean, + visitStatLocalFunction: (types.AstStatLocalFunction) -> boolean, + visitStatTypeAlias: (types.AstStatTypeAlias) -> boolean, + visitStatTypeFunction: (types.AstStatTypeFunction) -> boolean, + visitStatExpr: (types.AstStatExpr) -> boolean, + + visitExpr: (types.AstExpr) -> boolean, + visitExprEnd: (types.AstExpr) -> (), + visitExprConstantNil: (types.AstExprConstantNil) -> boolean, + visitExprConstantString: (types.AstExprConstantString) -> boolean, + visitExprConstantBool: (types.AstExprConstantBool) -> boolean, + visitExprConstantNumber: (types.AstExprConstantNumber) -> boolean, + visitExprLocal: (types.AstExprLocal) -> boolean, + visitExprGlobal: (types.AstExprGlobal) -> boolean, + visitExprCall: (types.AstExprCall) -> boolean, + visitExprUnary: (types.AstExprUnary) -> boolean, + visitExprBinary: (types.AstExprBinary) -> boolean, + visitExprAnonymousFunction: (types.AstExprAnonymousFunction) -> boolean, + visitExprTableItem: (types.AstExprTableItem) -> boolean, + visitExprTable: (types.AstExprTable) -> boolean, + visitExprIndexName: (types.AstExprIndexName) -> boolean, + visitExprIndexExpr: (types.AstExprIndexExpr) -> boolean, + visitExprGroup: (types.AstExprGroup) -> boolean, + visitExprInterpString: (types.AstExprInterpString) -> boolean, + visitExprTypeAssertion: (types.AstExprTypeAssertion) -> boolean, + visitExprIfElse: (types.AstExprIfElse) -> boolean, + visitExprVarargs: (types.AstExprVarargs) -> boolean, + + visitTypeReference: (types.AstTypeReference) -> boolean, + visitTypeSingletonBool: (types.AstTypeSingletonBool) -> boolean, + visitTypeSingletonString: (types.AstTypeSingletonString) -> boolean, + visitTypeTypeof: (types.AstTypeTypeof) -> boolean, + visitTypeGroup: (types.AstTypeGroup) -> boolean, + visitTypeOptional: (types.AstTypeOptional) -> boolean, + visitTypeUnion: (types.AstTypeUnion) -> boolean, + visitTypeIntersection: (types.AstTypeIntersection) -> boolean, + visitTypeArray: (types.AstTypeArray) -> boolean, + visitTypeTable: (types.AstTypeTable) -> boolean, + visitTypeFunction: (types.AstTypeFunction) -> boolean, + + visitTypePackExplicit: (types.AstTypePackExplicit) -> boolean, + visitTypePackGeneric: (types.AstTypePackGeneric) -> boolean, + visitTypePackVariadic: (types.AstTypePackVariadic) -> boolean, + + visitToken: (types.Token) -> boolean, + + visitLocal: (types.AstLocal) -> boolean, + + visitAttribute: (types.AstAttribute) -> boolean, } local function alwaysVisit(...: any) @@ -66,45 +78,55 @@ local function alwaysVisit(...: any) end local defaultVisitor: Visitor = { - visitBlock = alwaysVisit :: any, - visitBlockEnd = alwaysVisit :: any, - visitIf = alwaysVisit :: any, - visitWhile = alwaysVisit :: any, - visitRepeat = alwaysVisit :: any, - visitReturn = alwaysVisit :: any, - visitLocalDeclaration = alwaysVisit :: any, - visitLocalDeclarationEnd = alwaysVisit :: any, - visitFor = alwaysVisit :: any, - visitForIn = alwaysVisit :: any, - visitAssign = alwaysVisit :: any, - visitCompoundAssign = alwaysVisit :: any, - visitFunction = alwaysVisit :: any, - visitLocalFunction = alwaysVisit :: any, - visitTypeAlias = alwaysVisit :: any, + visitStatBlock = alwaysVisit :: any, + visitStatBlockEnd = alwaysVisit :: any, + visitStatDo = alwaysVisit :: any, + visitStatIf = alwaysVisit :: any, + visitStatWhile = alwaysVisit :: any, + visitStatRepeat = alwaysVisit :: any, + visitStatBreak = alwaysVisit :: any, + visitStatContinue = alwaysVisit :: any, + visitStatReturn = alwaysVisit :: any, + visitStatLocalDeclaration = alwaysVisit :: any, + visitStatLocalDeclarationEnd = alwaysVisit :: any, + visitStatFor = alwaysVisit :: any, + visitStatForIn = alwaysVisit :: any, + visitStatAssign = alwaysVisit :: any, + visitStatCompoundAssign = alwaysVisit :: any, + visitStatFunction = alwaysVisit :: any, + visitStatLocalFunction = alwaysVisit :: any, + visitStatTypeAlias = alwaysVisit :: any, visitStatTypeFunction = alwaysVisit :: any, - - visitExpression = alwaysVisit :: any, - visitExpressionEnd = alwaysVisit :: any, - visitLocalReference = alwaysVisit :: any, - visitGlobal = alwaysVisit :: any, - visitCall = alwaysVisit :: any, - visitUnary = alwaysVisit :: any, - visitBinary = alwaysVisit :: any, - visitAnonymousFunction = alwaysVisit :: any, - visitTableItem = alwaysVisit :: any, - visitTable = alwaysVisit :: any, - visitIndexName = alwaysVisit :: any, - visitIndexExpr = alwaysVisit :: any, - visitGroup = alwaysVisit :: any, - visitInterpolatedString = alwaysVisit, - visitTypeAssertion = alwaysVisit, - visitIfExpression = alwaysVisit, + visitStatExpr = alwaysVisit :: any, + + visitExpr = alwaysVisit :: any, + visitExprConstantNil = alwaysVisit :: any, + visitExprConstantString = alwaysVisit :: any, + visitExprConstantBool = alwaysVisit :: any, + visitExprConstantNumber = alwaysVisit :: any, + visitExprEnd = alwaysVisit :: any, + visitExprLocal = alwaysVisit :: any, + visitExprGlobal = alwaysVisit :: any, + visitExprCall = alwaysVisit :: any, + visitExprUnary = alwaysVisit :: any, + visitExprBinary = alwaysVisit :: any, + visitExprAnonymousFunction = alwaysVisit :: any, + visitExprTableItem = alwaysVisit :: any, + visitExprTable = alwaysVisit :: any, + visitExprIndexName = alwaysVisit :: any, + visitExprIndexExpr = alwaysVisit :: any, + visitExprGroup = alwaysVisit :: any, + visitExprInterpString = alwaysVisit, + visitExprTypeAssertion = alwaysVisit, + visitExprIfElse = alwaysVisit, + visitExprVarargs = alwaysVisit :: any, visitTypeReference = alwaysVisit :: any, - visitTypeBoolean = alwaysVisit :: any, - visitTypeString = alwaysVisit :: any, + visitTypeSingletonBool = alwaysVisit :: any, + visitTypeSingletonString = alwaysVisit :: any, visitTypeTypeof = alwaysVisit :: any, visitTypeGroup = alwaysVisit :: any, + visitTypeOptional = alwaysVisit :: any, visitTypeUnion = alwaysVisit :: any, visitTypeIntersection = alwaysVisit :: any, visitTypeArray = alwaysVisit :: any, @@ -116,23 +138,21 @@ local defaultVisitor: Visitor = { visitTypePackVariadic = alwaysVisit, visitToken = alwaysVisit :: any, - visitNil = alwaysVisit :: any, - visitString = alwaysVisit :: any, - visitBoolean = alwaysVisit :: any, - visitNumber = alwaysVisit :: any, + visitLocal = alwaysVisit :: any, - visitVarargs = alwaysVisit :: any, + + visitAttribute = alwaysVisit :: any, } local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) end -local function visitToken(token: luau.Token, visitor: Visitor) +local function visitToken(token: types.Token, visitor: Visitor) visitor.visitToken(token) end -local function visitPunctuated(list: luau.Punctuated, visitor: Visitor, apply: (T, Visitor) -> ()) +local function visitPunctuated(list: types.Punctuated, visitor: Visitor, apply: (T, Visitor) -> ()) for _, item in list do apply(item.node, visitor) if item.separator then @@ -141,7 +161,7 @@ local function visitPunctuated(list: luau.Punctuated end end -local function visitLocal(node: luau.AstLocal, visitor: Visitor) +local function visitLocal(node: types.AstLocal, visitor: Visitor) if visitor.visitLocal(node) then visitToken(node.name, visitor) if node.colon then @@ -153,126 +173,148 @@ local function visitLocal(node: luau.AstLocal, visitor: Visitor) end end -local function visitBlock(block: luau.AstStatBlock, visitor: Visitor) - if visitor.visitBlock(block) then +local function visitStatBlock(block: types.AstStatBlock, visitor: Visitor) + if visitor.visitStatBlock(block) then for _, statement in block.statements do visitStatement(statement, visitor) end - visitor.visitBlockEnd(block) + visitor.visitStatBlockEnd(block) + end +end + +local function visitStatDo(node: types.AstStatDo, visitor: Visitor) + if visitor.visitStatDo(node) then + visitToken(node.dokeyword, visitor) + for _, statement in node.body do + visitStatement(statement, visitor) + end + visitToken(node.endkeyword, visitor) end end -local function visitIf(node: luau.AstStatIf, visitor: Visitor) - if visitor.visitIf(node) then - visitToken(node.ifKeyword, visitor) - visitExpression(node.condition, visitor) - visitToken(node.thenKeyword, visitor) - visitBlock(node.consequent, visitor) +local function visitStatIf(node: types.AstStatIf, visitor: Visitor) + if visitor.visitStatIf(node) then + visitToken(node.ifkeyword, visitor) + visitExpr(node.condition, visitor) + visitToken(node.thenkeyword, visitor) + visitStatBlock(node.thenblock, visitor) for _, elseifNode in node.elseifs do - visitToken(elseifNode.elseifKeyword, visitor) - visitExpression(elseifNode.condition, visitor) - visitToken(elseifNode.thenKeyword, visitor) - visitBlock(elseifNode.consequent, visitor) + visitToken(elseifNode.elseifkeyword, visitor) + visitExpr(elseifNode.condition, visitor) + visitToken(elseifNode.thenkeyword, visitor) + visitStatBlock(elseifNode.thenblock, visitor) end - if node.elseKeyword then - visitToken(node.elseKeyword, visitor) + if node.elsekeyword then + visitToken(node.elsekeyword, visitor) end - if node.antecedent then - visitBlock(node.antecedent, visitor) + if node.elseblock then + visitStatBlock(node.elseblock, visitor) end - visitToken(node.endKeyword, visitor) + visitToken(node.endkeyword, visitor) end end -local function visitWhile(node: luau.AstStatWhile, visitor: Visitor) - if visitor.visitWhile(node) then - visitToken(node.whileKeyword, visitor) - visitExpression(node.condition, visitor) - visitToken(node.doKeyword, visitor) - visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) +local function visitStatWhile(node: types.AstStatWhile, visitor: Visitor) + if visitor.visitStatWhile(node) then + visitToken(node.whilekeyword, visitor) + visitExpr(node.condition, visitor) + visitToken(node.dokeyword, visitor) + visitStatBlock(node.body, visitor) + visitToken(node.endkeyword, visitor) end end -local function visitRepeat(node: luau.AstStatRepeat, visitor: Visitor) - if visitor.visitRepeat(node) then - visitToken(node.repeatKeyword, visitor) - visitBlock(node.body, visitor) - visitToken(node.untilKeyword, visitor) - visitExpression(node.condition, visitor) +local function visitStatRepeat(node: types.AstStatRepeat, visitor: Visitor) + if visitor.visitStatRepeat(node) then + visitToken(node.repeatkeyword, visitor) + visitStatBlock(node.body, visitor) + visitToken(node.untilkeyword, visitor) + visitExpr(node.condition, visitor) end end -local function visitReturn(node: luau.AstStatReturn, visitor: Visitor) - if visitor.visitReturn(node) then - visitToken(node.returnKeyword, visitor) - visitPunctuated(node.expressions, visitor, visitExpression) +local function visitStatBreak(node: types.AstStatBreak, visitor: Visitor) + if visitor.visitStatBreak(node) then + visitToken(node, visitor) end end -local function visitLocalStatement(node: luau.AstStatLocal, visitor: Visitor) - if visitor.visitLocalDeclaration(node) then - visitToken(node.localKeyword, visitor) +local function visitStatContinue(node: types.AstStatContinue, visitor: Visitor) + if visitor.visitStatContinue(node) then + visitToken(node, visitor) + end +end + +local function visitStatReturn(node: types.AstStatReturn, visitor: Visitor) + if visitor.visitStatReturn(node) then + visitToken(node.returnkeyword, visitor) + visitPunctuated(node.expressions, visitor, visitExpr) + end +end + +local function visitLocalStatement(node: types.AstStatLocal, visitor: Visitor) + if visitor.visitStatLocalDeclaration(node) then + visitToken(node.localkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) if node.equals then visitToken(node.equals, visitor) end - visitPunctuated(node.values, visitor, visitExpression) + visitPunctuated(node.values, visitor, visitExpr) - visitor.visitLocalDeclarationEnd(node) + visitor.visitStatLocalDeclarationEnd(node) end end -local function visitFor(node: luau.AstStatFor, visitor: Visitor) - if visitor.visitFor(node) then - visitToken(node.forKeyword, visitor) +local function visitStatFor(node: types.AstStatFor, visitor: Visitor) + if visitor.visitStatFor(node) then + visitToken(node.forkeyword, visitor) visitLocal(node.variable, visitor) visitToken(node.equals, visitor) - visitExpression(node.from, visitor) - visitToken(node.toComma, visitor) - visitExpression(node.to, visitor) - if node.stepComma then - visitToken(node.stepComma, visitor) + visitExpr(node.from, visitor) + visitToken(node.tocomma, visitor) + visitExpr(node.to, visitor) + if node.stepcomma then + visitToken(node.stepcomma, visitor) end if node.step then - visitExpression(node.step, visitor) + visitExpr(node.step, visitor) end - visitToken(node.doKeyword, visitor) - visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitToken(node.dokeyword, visitor) + visitStatBlock(node.body, visitor) + visitToken(node.endkeyword, visitor) end end -local function visitForIn(node: luau.AstStatForIn, visitor: Visitor) - if visitor.visitForIn(node) then - visitToken(node.forKeyword, visitor) +local function visitStatForIn(node: types.AstStatForIn, visitor: Visitor) + if visitor.visitStatForIn(node) then + visitToken(node.forkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) - visitToken(node.inKeyword, visitor) - visitPunctuated(node.values, visitor, visitExpression) - visitToken(node.doKeyword, visitor) - visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitToken(node.inkeyword, visitor) + visitPunctuated(node.values, visitor, visitExpr) + visitToken(node.dokeyword, visitor) + visitStatBlock(node.body, visitor) + visitToken(node.endkeyword, visitor) end end -local function visitAssign(node: luau.AstStatAssign, visitor: Visitor) - if visitor.visitAssign(node) then - visitPunctuated(node.variables, visitor, visitExpression) +local function visitStatAssign(node: types.AstStatAssign, visitor: Visitor) + if visitor.visitStatAssign(node) then + visitPunctuated(node.variables, visitor, visitExpr) visitToken(node.equals, visitor) - visitPunctuated(node.values, visitor, visitExpression) + visitPunctuated(node.values, visitor, visitExpr) end end -local function visitCompoundAssign(node: luau.AstStatCompoundAssign, visitor: Visitor) - if visitor.visitCompoundAssign(node) then - visitExpression(node.variable, visitor) +local function visitStatCompoundAssign(node: types.AstStatCompoundAssign, visitor: Visitor) + if visitor.visitStatCompoundAssign(node) then + visitExpr(node.variable, visitor) visitToken(node.operand, visitor) - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) end end -local function visitGeneric(node: luau.AstGenericType, visitor: Visitor) +local function visitGeneric(node: types.AstGenericType, visitor: Visitor) visitToken(node.name, visitor) if node.equals then visitToken(node.equals, visitor) @@ -282,7 +324,7 @@ local function visitGeneric(node: luau.AstGenericType, visitor: Visitor) end end -local function visitGenericPack(node: luau.AstGenericTypePack, visitor: Visitor) +local function visitGenericPack(node: types.AstGenericTypePack, visitor: Visitor) visitToken(node.name, visitor) visitToken(node.ellipsis, visitor) if node.equals then @@ -293,198 +335,206 @@ local function visitGenericPack(node: luau.AstGenericTypePack, visitor: Visitor) end end -local function visitTypeAlias(node: luau.AstStatTypeAlias, visitor: Visitor) - if visitor.visitTypeAlias(node) then +local function visitStatTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor) + if visitor.visitStatTypeAlias(node) then if node.export then visitToken(node.export, visitor) end - visitToken(node.typeToken, visitor) + visitToken(node.typetoken, visitor) visitToken(node.name, visitor) - if node.openGenerics then - visitToken(node.openGenerics, visitor) + if node.opengenerics then + visitToken(node.opengenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericPacks then - visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) + if node.closegenerics then + visitToken(node.closegenerics, visitor) end visitToken(node.equals, visitor) visitType(node.type, visitor) end end -local function visitString(node: luau.AstExprConstantString, visitor: Visitor) - if visitor.visitString(node) then +local function visitStatExpr(node: types.AstStatExpr, visitor: Visitor) + if visitor.visitStatExpr(node) then + visitExpr(node.expression, visitor) + end +end + +local function visitExprConstantString(node: types.AstExprConstantString, visitor: Visitor) + if visitor.visitExprConstantString(node) then visitor.visitToken(node) end end -local function visitNil(node: luau.AstExprConstantNil, visitor: Visitor) - if visitor.visitNil(node) then +local function visitExprConstantNil(node: types.AstExprConstantNil, visitor: Visitor) + if visitor.visitExprConstantNil(node) then visitToken(node, visitor) end end -local function visitBoolean(node: luau.AstExprConstantBool, visitor: Visitor) - if visitor.visitBoolean(node) then +local function visitExprConstantBool(node: types.AstExprConstantBool, visitor: Visitor) + if visitor.visitExprConstantBool(node) then visitToken(node, visitor) end end -local function visitNumber(node: luau.AstExprConstantNumber, visitor: Visitor) - if visitor.visitNumber(node) then +local function visitExprConstantNumber(node: types.AstExprConstantNumber, visitor: Visitor) + if visitor.visitExprConstantNumber(node) then visitToken(node, visitor) end end -local function visitLocalReference(node: luau.AstExprLocal, visitor: Visitor) - if visitor.visitLocalReference(node) then +local function visitExprLocal(node: types.AstExprLocal, visitor: Visitor) + if visitor.visitExprLocal(node) then visitor.visitToken(node.token) end end -local function visitGlobal(node: luau.AstExprGlobal, visitor: Visitor) - if visitor.visitGlobal(node) then +local function visitExprGlobal(node: types.AstExprGlobal, visitor: Visitor) + if visitor.visitExprGlobal(node) then visitor.visitToken(node.name) end end -local function visitVarargs(node: luau.AstExprVarargs, visitor: Visitor) - if visitor.visitVarargs(node) then +local function visitExprVarargs(node: types.AstExprVarargs, visitor: Visitor) + if visitor.visitExprVarargs(node) then visitToken(node, visitor) end end -local function visitCall(node: luau.AstExprCall, visitor: Visitor) - if visitor.visitCall(node) then - visitExpression(node.func, visitor) - if node.openParens then - visitToken(node.openParens, visitor) +local function visitExprCall(node: types.AstExprCall, visitor: Visitor) + if visitor.visitExprCall(node) then + visitExpr(node.func, visitor) + if node.openparens then + visitToken(node.openparens, visitor) end - visitPunctuated(node.arguments, visitor, visitExpression) - if node.closeParens then - visitToken(node.closeParens, visitor) + visitPunctuated(node.arguments, visitor, visitExpr) + if node.closeparens then + visitToken(node.closeparens, visitor) end end end -local function visitUnary(node: luau.AstExprUnary, visitor: Visitor) - if visitor.visitUnary(node) then +local function visitExprUnary(node: types.AstExprUnary, visitor: Visitor) + if visitor.visitExprUnary(node) then visitToken(node.operator, visitor) - visitExpression(node.operand, visitor) + visitExpr(node.operand, visitor) end end -local function visitBinary(node: luau.AstExprBinary, visitor: Visitor) - if visitor.visitBinary(node) then - visitExpression(node.lhsoperand, visitor) +local function visitExprBinary(node: types.AstExprBinary, visitor: Visitor) + if visitor.visitExprBinary(node) then + visitExpr(node.lhsoperand, visitor) visitToken(node.operator, visitor) - visitExpression(node.rhsoperand, visitor) + visitExpr(node.rhsoperand, visitor) end end -local function visitFunctionBody(node: luau.AstFunctionBody, visitor: Visitor) - if node.openGenerics then - visitToken(node.openGenerics, visitor) +local function visitStatFunctionBody(node: types.AstFunctionBody, visitor: Visitor) + if node.opengenerics then + visitToken(node.opengenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericPacks then - visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) + if node.closegenerics then + visitToken(node.closegenerics, visitor) end - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitPunctuated(node.parameters, visitor, visitLocal) if node.vararg then visitToken(node.vararg, visitor) end - if node.varargColon then - visitToken(node.varargColon, visitor) + if node.varargcolon then + visitToken(node.varargcolon, visitor) end - if node.varargAnnotation then - visitTypePack(node.varargAnnotation, visitor) + if node.varargannotation then + visitTypePack(node.varargannotation, visitor) end - visitToken(node.closeParens, visitor) - if node.returnSpecifier then - visitToken(node.returnSpecifier, visitor) + visitToken(node.closeparens, visitor) + if node.returnspecifier then + visitToken(node.returnspecifier, visitor) end - if node.returnAnnotation then - visitTypePack(node.returnAnnotation, visitor) + if node.returnannotation then + visitTypePack(node.returnannotation, visitor) end - visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitStatBlock(node.body, visitor) + visitToken(node.endkeyword, visitor) end -local function visitAttribute(node: luau.AstAttribute, visitor) - visitToken(node, visitor) +local function visitAttribute(node: types.AstAttribute, visitor: Visitor) + if visitor.visitAttribute(node) then + visitToken(node, visitor) + end end -local function visitAnonymousFunction(node: luau.AstExprAnonymousFunction, visitor: Visitor) - if visitor.visitAnonymousFunction(node) then +local function visitExprAnonymousFunction(node: types.AstExprAnonymousFunction, visitor: Visitor) + if visitor.visitExprAnonymousFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) end - visitToken(node.functionKeyword, visitor) - visitFunctionBody(node.body, visitor) + visitToken(node.functionkeyword, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitFunction(node: luau.AstStatFunction, visitor: Visitor) - if visitor.visitFunction(node) then +local function visitStatFunction(node: types.AstStatFunction, visitor: Visitor) + if visitor.visitStatFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) end - visitToken(node.functionKeyword, visitor) - visitExpression(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitToken(node.functionkeyword, visitor) + visitExpr(node.name, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitLocalFunction(node: luau.AstStatLocalFunction, visitor: Visitor) - if visitor.visitLocalFunction(node) then +local function visitStatLocalFunction(node: types.AstStatLocalFunction, visitor: Visitor) + if visitor.visitStatLocalFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) end - visitToken(node.localKeyword, visitor) - visitToken(node.functionKeyword, visitor) + visitToken(node.localkeyword, visitor) + visitToken(node.functionkeyword, visitor) visitLocal(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitStatTypeFunction(node: luau.AstStatTypeFunction, visitor: Visitor) +local function visitStatTypeFunction(node: types.AstStatTypeFunction, visitor: Visitor) if visitor.visitStatTypeFunction(node) then if node.export then visitToken(node.export, visitor) end visitToken(node.type, visitor) - visitToken(node.functionKeyword, visitor) + visitToken(node.functionkeyword, visitor) visitToken(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) - if visitor.visitTableItem(node) then +local function visitExprTableItem(node: types.AstExprTableItem, visitor: Visitor) + if visitor.visitExprTableItem(node) then if node.kind == "list" then - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) elseif node.kind == "record" then visitToken(node.key, visitor) visitToken(node.equals, visitor) - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) elseif node.kind == "general" then - visitToken(node.indexerOpen, visitor) - visitExpression(node.key, visitor) - visitToken(node.indexerClose, visitor) + visitToken(node.indexeropen, visitor) + visitExpr(node.key, visitor) + visitToken(node.indexerclose, visitor) visitToken(node.equals, visitor) - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) else exhaustiveMatch(node.kind) end @@ -495,136 +545,142 @@ local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) end end -local function visitTable(node: luau.AstExprTable, visitor: Visitor) - if visitor.visitTable(node) then - visitToken(node.openBrace, visitor) +local function visitExprTable(node: types.AstExprTable, visitor: Visitor) + if visitor.visitExprTable(node) then + visitToken(node.openbrace, visitor) for _, item in node.entries do - visitTableItem(item, visitor) + visitExprTableItem(item, visitor) end - visitToken(node.closeBrace, visitor) + visitToken(node.closebrace, visitor) end end -local function visitIndexName(node: luau.AstExprIndexName, visitor: Visitor) - if visitor.visitIndexName(node) then - visitExpression(node.expression, visitor) +local function visitExprIndexName(node: types.AstExprIndexName, visitor: Visitor) + if visitor.visitExprIndexName(node) then + visitExpr(node.expression, visitor) visitToken(node.accessor, visitor) visitToken(node.index, visitor) end end -local function visitIndexExpr(node: luau.AstExprIndexExpr, visitor: Visitor) - if visitor.visitIndexExpr(node) then - visitExpression(node.expression, visitor) - visitToken(node.openBrackets, visitor) - visitExpression(node.index, visitor) - visitToken(node.closeBrackets, visitor) +local function visitExprIndexExpr(node: types.AstExprIndexExpr, visitor: Visitor) + if visitor.visitExprIndexExpr(node) then + visitExpr(node.expression, visitor) + visitToken(node.openbrackets, visitor) + visitExpr(node.index, visitor) + visitToken(node.closebrackets, visitor) end end -local function visitGroup(node: luau.AstExprGroup, visitor: Visitor) - if visitor.visitGroup(node) then - visitToken(node.openParens, visitor) - visitExpression(node.expression, visitor) - visitToken(node.closeParens, visitor) +local function visitExprGroup(node: types.AstExprGroup, visitor: Visitor) + if visitor.visitExprGroup(node) then + visitToken(node.openparens, visitor) + visitExpr(node.expression, visitor) + visitToken(node.closeparens, visitor) end end -local function visitInterpolatedString(node: luau.AstExprInterpString, visitor: Visitor) - if visitor.visitInterpolatedString(node) then +local function visitExprInterpString(node: types.AstExprInterpString, visitor: Visitor) + if visitor.visitExprInterpString(node) then for i = 1, #node.strings do visitToken(node.strings[i], visitor) if i <= #node.expressions then - visitExpression(node.expressions[i], visitor) + visitExpr(node.expressions[i], visitor) end end end end -local function visitTypeAssertion(node: luau.AstExprTypeAssertion, visitor: Visitor) - if visitor.visitTypeAssertion(node) then - visitExpression(node.operand, visitor) +local function visitExprTypeAssertion(node: types.AstExprTypeAssertion, visitor: Visitor) + if visitor.visitExprTypeAssertion(node) then + visitExpr(node.operand, visitor) visitToken(node.operator, visitor) visitType(node.annotation, visitor) end end -local function visitIfExpression(node: luau.AstExprIfElse, visitor: Visitor) - if visitor.visitIfExpression(node) then - visitToken(node.ifKeyword, visitor) - visitExpression(node.condition, visitor) - visitToken(node.thenKeyword, visitor) - visitExpression(node.consequent, visitor) +local function visitExprIfElse(node: types.AstExprIfElse, visitor: Visitor) + if visitor.visitExprIfElse(node) then + visitToken(node.ifkeyword, visitor) + visitExpr(node.condition, visitor) + visitToken(node.thenkeyword, visitor) + visitExpr(node.thenexpr, visitor) for _, elseifs in node.elseifs do - visitToken(elseifs.elseifKeyword, visitor) - visitExpression(elseifs.condition, visitor) - visitToken(elseifs.thenKeyword, visitor) - visitExpression(elseifs.consequent, visitor) + visitToken(elseifs.elseifkeyword, visitor) + visitExpr(elseifs.condition, visitor) + visitToken(elseifs.thenkeyword, visitor) + visitExpr(elseifs.thenexpr, visitor) end - visitToken(node.elseKeyword, visitor) - visitExpression(node.antecedent, visitor) + visitToken(node.elsekeyword, visitor) + visitExpr(node.elseexpr, visitor) end end -local function visitTypeOrPack(node: luau.AstType | luau.AstTypePack, visitor: Visitor) - if node.tag == "explicit" or node.tag == "generic" or node.tag == "variadic" then +local function visitTypeOrPack(node: types.AstType | types.AstTypePack, visitor: Visitor) + if node.kind == "typepack" then visitTypePack(node, visitor) else visitType(node, visitor) end end -local function visitTypeReference(node: luau.AstTypeReference, visitor: Visitor) +local function visitTypeReference(node: types.AstTypeReference, visitor: Visitor) if visitor.visitTypeReference(node) then if node.prefix then visitToken(node.prefix, visitor) end - if node.prefixPoint then - visitToken(node.prefixPoint, visitor) + if node.prefixpoint then + visitToken(node.prefixpoint, visitor) end visitToken(node.name, visitor) - if node.openParameters then - visitToken(node.openParameters, visitor) + if node.openparameters then + visitToken(node.openparameters, visitor) end if node.parameters then visitPunctuated(node.parameters, visitor, visitTypeOrPack) end - if node.closeParameters then - visitToken(node.closeParameters, visitor) + if node.closeparameters then + visitToken(node.closeparameters, visitor) end end end -local function visitTypeBoolean(node: luau.AstTypeSingletonBool, visitor: Visitor) - if visitor.visitTypeBoolean(node) then +local function visitTypeSingletonBool(node: types.AstTypeSingletonBool, visitor: Visitor) + if visitor.visitTypeSingletonBool(node) then visitToken(node, visitor) end end -local function visitTypeString(node: luau.AstTypeSingletonString, visitor: Visitor) - if visitor.visitTypeString(node) then +local function visitTypeSingletonString(node: types.AstTypeSingletonString, visitor: Visitor) + if visitor.visitTypeSingletonString(node) then visitToken(node, visitor) end end -local function visitTypeTypeof(node: luau.AstTypeTypeof, visitor: Visitor) +local function visitTypeTypeof(node: types.AstTypeTypeof, visitor: Visitor) if visitor.visitTypeTypeof(node) then visitToken(node.typeof, visitor) - visitToken(node.openParens, visitor) - visitExpression(node.expression, visitor) - visitToken(node.closeParens, visitor) + visitToken(node.openparens, visitor) + visitExpr(node.expression, visitor) + visitToken(node.closeparens, visitor) end end -local function visitTypeGroup(node: luau.AstTypeGroup, visitor: Visitor) +local function visitTypeGroup(node: types.AstTypeGroup, visitor: Visitor) if visitor.visitTypeGroup(node) then - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitType(node.type, visitor) - visitToken(node.closeParens, visitor) + visitToken(node.closeparens, visitor) end end -local function visitTypeUnion(node: luau.AstTypeUnion, visitor: Visitor) +local function visitTypeOptional(node: types.AstTypeOptional, visitor: Visitor) + if visitor.visitTypeOptional(node) then + visitToken(node, visitor) + end +end + +local function visitTypeUnion(node: types.AstTypeUnion, visitor: Visitor) if visitor.visitTypeUnion(node) then if node.leading then visitToken(node.leading, visitor) @@ -633,7 +689,7 @@ local function visitTypeUnion(node: luau.AstTypeUnion, visitor: Visitor) end end -local function visitTypeIntersection(node: luau.AstTypeIntersection, visitor: Visitor) +local function visitTypeIntersection(node: types.AstTypeIntersection, visitor: Visitor) if visitor.visitTypeIntersection(node) then if node.leading then visitToken(node.leading, visitor) @@ -642,32 +698,32 @@ local function visitTypeIntersection(node: luau.AstTypeIntersection, visitor: Vi end end -local function visitTypeArray(node: luau.AstTypeArray, visitor: Visitor) +local function visitTypeArray(node: types.AstTypeArray, visitor: Visitor) if visitor.visitTypeArray(node) then - visitToken(node.openBrace, visitor) + visitToken(node.openbrace, visitor) if node.access then visitToken(node.access, visitor) end visitType(node.type, visitor) - visitToken(node.closeBrace, visitor) + visitToken(node.closebrace, visitor) end end -local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) +local function visitTypeTable(node: types.AstTypeTable, visitor: Visitor) if visitor.visitTypeTable(node) then - visitToken(node.openBrace, visitor) + visitToken(node.openbrace, visitor) for _, entry in node.entries do if entry.access then visitToken(entry.access, visitor) end if entry.kind == "indexer" then - visitToken(entry.indexerOpen, visitor) + visitToken(entry.indexeropen, visitor) visitType(entry.key, visitor) - visitToken(entry.indexerClose, visitor) + visitToken(entry.indexerclose, visitor) elseif entry.kind == "stringproperty" then - visitToken(entry.indexerOpen, visitor) - visitTypeString(entry.key, visitor) - visitToken(entry.indexerClose, visitor) + visitToken(entry.indexeropen, visitor) + visitTypeSingletonString(entry.key, visitor) + visitToken(entry.indexerclose, visitor) else visitToken(entry.key, visitor) end @@ -677,11 +733,11 @@ local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) visitToken(entry.separator, visitor) end end - visitToken(node.closeBrace, visitor) + visitToken(node.closebrace, visitor) end end -local function visitTypeFunctionParameter(node: luau.AstTypeFunctionParameter, visitor) +local function visitTypeFunctionParameter(node: types.AstTypeFunctionParameter, visitor) if node.name then visitToken(node.name, visitor) end @@ -691,54 +747,54 @@ local function visitTypeFunctionParameter(node: luau.AstTypeFunctionParameter, v visitType(node.type, visitor) end -local function visitTypeFunction(node: luau.AstTypeFunction, visitor: Visitor) +local function visitTypeFunction(node: types.AstTypeFunction, visitor: Visitor) if visitor.visitTypeFunction(node) then - if node.openGenerics then - visitToken(node.openGenerics, visitor) + if node.opengenerics then + visitToken(node.opengenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericPacks then - visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) + if node.closegenerics then + visitToken(node.closegenerics, visitor) end - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitPunctuated(node.parameters, visitor, visitTypeFunctionParameter) if node.vararg then visitTypePack(node.vararg, visitor) end - visitToken(node.closeParens, visitor) - visitToken(node.returnArrow, visitor) - visitTypePack(node.returnTypes, visitor) + visitToken(node.closeparens, visitor) + visitToken(node.returnarrow, visitor) + visitTypePack(node.returntypes, visitor) end end -local function visitTypePackExplicit(node: luau.AstTypePackExplicit, visitor: Visitor) +local function visitTypePackExplicit(node: types.AstTypePackExplicit, visitor: Visitor) if visitor.visitTypePackExplicit(node) then - if node.openParens then - visitToken(node.openParens, visitor) + if node.openparens then + visitToken(node.openparens, visitor) end visitPunctuated(node.types, visitor, visitType) - if node.tailType then - visitTypePack(node.tailType, visitor) + if node.tailtype then + visitTypePack(node.tailtype, visitor) end - if node.closeParens then - visitToken(node.closeParens, visitor) + if node.closeparens then + visitToken(node.closeparens, visitor) end end end -local function visitTypePackGeneric(node: luau.AstTypePackGeneric, visitor: Visitor) +local function visitTypePackGeneric(node: types.AstTypePackGeneric, visitor: Visitor) if visitor.visitTypePackGeneric(node) then visitToken(node.name, visitor) visitToken(node.ellipsis, visitor) end end -local function visitTypePackVariadic(node: luau.AstTypePackVariadic, visitor: Visitor) +local function visitTypePackVariadic(node: types.AstTypePackVariadic, visitor: Visitor) if visitor.visitTypePackVariadic(node) then if node.ellipsis then visitToken(node.ellipsis, visitor) @@ -747,85 +803,87 @@ local function visitTypePackVariadic(node: luau.AstTypePackVariadic, visitor: Vi end end -function visitExpression(expression: luau.AstExpr, visitor: Visitor) - if visitor.visitExpression(expression) then +function visitExpr(expression: types.AstExpr, visitor: Visitor) + if visitor.visitExpr(expression) then if expression.tag == "nil" then - visitNil(expression, visitor) + visitExprConstantNil(expression, visitor) elseif expression.tag == "boolean" then - visitBoolean(expression, visitor) + visitExprConstantBool(expression, visitor) elseif expression.tag == "number" then - visitNumber(expression, visitor) + visitExprConstantNumber(expression, visitor) elseif expression.tag == "string" then - visitString(expression, visitor) + visitExprConstantString(expression, visitor) elseif expression.tag == "local" then - visitLocalReference(expression, visitor) + visitExprLocal(expression, visitor) elseif expression.tag == "global" then - visitGlobal(expression, visitor) + visitExprGlobal(expression, visitor) elseif expression.tag == "vararg" then - visitVarargs(expression, visitor) + visitExprVarargs(expression, visitor) elseif expression.tag == "call" then - visitCall(expression, visitor) + visitExprCall(expression, visitor) elseif expression.tag == "unary" then - visitUnary(expression, visitor) + visitExprUnary(expression, visitor) elseif expression.tag == "binary" then - visitBinary(expression, visitor) + visitExprBinary(expression, visitor) elseif expression.tag == "function" then - visitAnonymousFunction(expression, visitor) + visitExprAnonymousFunction(expression, visitor) elseif expression.tag == "table" then - visitTable(expression, visitor) + visitExprTable(expression, visitor) elseif expression.tag == "indexname" then - visitIndexName(expression, visitor) + visitExprIndexName(expression, visitor) elseif expression.tag == "index" then - visitIndexExpr(expression, visitor) + visitExprIndexExpr(expression, visitor) elseif expression.tag == "group" then - visitGroup(expression, visitor) + visitExprGroup(expression, visitor) elseif expression.tag == "interpolatedstring" then - visitInterpolatedString(expression, visitor) + visitExprInterpString(expression, visitor) elseif expression.tag == "cast" then - visitTypeAssertion(expression, visitor) + visitExprTypeAssertion(expression, visitor) elseif expression.tag == "conditional" then - visitIfExpression(expression, visitor) + visitExprIfElse(expression, visitor) else exhaustiveMatch(expression.tag) end - visitor.visitExpressionEnd(expression) + visitor.visitExprEnd(expression) end end -function visitStatement(statement: luau.AstStat, visitor: Visitor) +function visitStatement(statement: types.AstStat, visitor: Visitor) if statement.tag == "block" then - visitBlock(statement, visitor) + visitStatBlock(statement, visitor) + elseif statement.tag == "do" then + visitStatDo(statement, visitor) elseif statement.tag == "conditional" then - visitIf(statement, visitor) + visitStatIf(statement, visitor) elseif statement.tag == "expression" then - visitExpression(statement.expression, visitor) + visitStatExpr(statement, visitor) elseif statement.tag == "local" then visitLocalStatement(statement, visitor) elseif statement.tag == "return" then - visitReturn(statement, visitor) + visitStatReturn(statement, visitor) elseif statement.tag == "while" then - visitWhile(statement, visitor) + visitStatWhile(statement, visitor) elseif statement.tag == "break" then - visitToken(statement, visitor) + visitStatBreak(statement, visitor) elseif statement.tag == "continue" then - visitToken(statement, visitor) + visitStatContinue(statement, visitor) elseif statement.tag == "repeat" then - visitRepeat(statement, visitor) + visitStatRepeat(statement, visitor) elseif statement.tag == "for" then - visitFor(statement, visitor) + visitStatFor(statement, visitor) elseif statement.tag == "forin" then - visitForIn(statement, visitor) + visitStatForIn(statement, visitor) elseif statement.tag == "assign" then - visitAssign(statement, visitor) + visitStatAssign(statement, visitor) elseif statement.tag == "compoundassign" then - visitCompoundAssign(statement, visitor) + visitStatCompoundAssign(statement, visitor) elseif statement.tag == "function" then - visitFunction(statement, visitor) + visitStatFunction(statement, visitor) elseif statement.tag == "localfunction" then - visitLocalFunction(statement, visitor) + visitStatLocalFunction(statement, visitor) elseif statement.tag == "typealias" then - visitTypeAlias(statement, visitor) + visitStatTypeAlias(statement, visitor) elseif statement.tag == "typefunction" then visitStatTypeFunction(statement, visitor) else @@ -833,13 +891,13 @@ function visitStatement(statement: luau.AstStat, visitor: Visitor) end end -function visitType(type: luau.AstType, visitor: Visitor) +function visitType(type: types.AstType, visitor: Visitor) if type.tag == "reference" then visitTypeReference(type, visitor) elseif type.tag == "boolean" then - visitTypeBoolean(type, visitor) + visitTypeSingletonBool(type, visitor) elseif type.tag == "string" then - visitTypeString(type, visitor) + visitTypeSingletonString(type, visitor) elseif type.tag == "typeof" then visitTypeTypeof(type, visitor) elseif type.tag == "group" then @@ -849,7 +907,7 @@ function visitType(type: luau.AstType, visitor: Visitor) elseif type.tag == "intersection" then visitTypeIntersection(type, visitor) elseif type.tag == "optional" then - visitToken(type, visitor) + visitTypeOptional(type, visitor) elseif type.tag == "array" then visitTypeArray(type, visitor) elseif type.tag == "table" then @@ -861,7 +919,7 @@ function visitType(type: luau.AstType, visitor: Visitor) end end -function visitTypePack(type: luau.AstTypePack, visitor: Visitor) +function visitTypePack(type: types.AstTypePack, visitor: Visitor) if type.tag == "explicit" then visitTypePackExplicit(type, visitor) elseif type.tag == "generic" then @@ -873,16 +931,107 @@ function visitTypePack(type: luau.AstTypePack, visitor: Visitor) end end -local function createVisitor() - return table.clone(defaultVisitor) +local function create(visit: ((types.AstNode) -> boolean)?): Visitor + if visit then + return { + visitStatBlock = visit, + visitStatBlockEnd = visit, + visitStatDo = visit, + visitStatIf = visit, + visitStatWhile = visit, + visitStatRepeat = visit, + visitStatBreak = visit, + visitStatContinue = visit, + visitStatReturn = visit, + visitStatLocalDeclaration = visit, + visitStatLocalDeclarationEnd = visit, + visitStatFor = visit, + visitStatForIn = visit, + visitStatAssign = visit, + visitStatCompoundAssign = visit, + visitStatFunction = visit, + visitStatLocalFunction = visit, + visitStatTypeAlias = visit, + visitStatTypeFunction = visit, + visitStatExpr = visit, + + visitExpr = visit, + visitExprEnd = visit, + visitExprLocal = visit, + visitExprGlobal = visit, + visitExprCall = visit, + visitExprUnary = visit, + visitExprBinary = visit, + visitExprAnonymousFunction = visit, + visitExprTableItem = visit, + visitExprTable = visit, + visitExprIndexName = visit, + visitExprIndexExpr = visit, + visitExprGroup = visit, + visitExprInterpString = visit, + visitExprTypeAssertion = visit, + visitExprIfElse = visit, + + visitTypeReference = visit, + visitTypeSingletonBool = visit, + visitTypeSingletonString = visit, + visitTypeTypeof = visit, + visitTypeGroup = visit, + visitTypeOptional = visit, + visitTypeUnion = visit, + visitTypeIntersection = visit, + visitTypeArray = visit, + visitTypeTable = visit, + visitTypeFunction = visit, + + visitTypePackExplicit = visit, + visitTypePackGeneric = visit, + visitTypePackVariadic = visit, + + visitToken = visit, + visitExprConstantNil = visit, + visitExprConstantString = visit, + visitExprConstantBool = visit, + visitExprConstantNumber = visit, + visitLocal = visit, + visitExprVarargs = visit, + + visitAttribute = visit, + } + else + return table.clone(defaultVisitor) + end +end + +local function visit(node: types.AstNode, visitor: Visitor) + if node.kind == "stat" then + visitStatement(node, visitor) + elseif node.kind == "expr" then + visitExpr(node, visitor) + elseif node.kind == "type" then + visitType(node, visitor) + elseif node.kind == "typepack" then + visitTypePack(node, visitor) + elseif node.kind == "local" then + visitLocal(node, visitor) + elseif node.kind == "attribute" then + visitAttribute(node, visitor) + elseif node.istoken then + visitToken(node, visitor) + elseif node.istableitem then + visitExprTableItem(node, visitor) + else + exhaustiveMatch(node.kind) + end end -return { - createVisitor = createVisitor, - visitBlock = visitBlock, - visitStatement = visitStatement, - visitExpression = visitExpression, - visitType = visitType, - visitTypePack = visitTypePack, - visitToken = visitToken, -} +visitorlib.create = create +visitorlib.visitblock = visitStatBlock +visitorlib.visitstatement = visitStatement +visitorlib.visitexpression = visitExpr +visitorlib.visittype = visitType +visitorlib.visittypepack = visitTypePack +visitorlib.visittoken = visitToken +visitorlib.visit = visit + +return table.freeze(visitorlib) diff --git a/lute/std/libs/system/init.luau b/lute/std/libs/system/init.luau new file mode 100644 index 000000000..ae9e210c0 --- /dev/null +++ b/lute/std/libs/system/init.luau @@ -0,0 +1,33 @@ +--!strict +-- @std/system +-- stdlib for `@lute/system` +-- Provides re-exports of `@lute/system` and boolean properties for OS detection + +local system = require("@lute/system") +local path = require("@std/path") +local platform = require("@self/platform") + +local systemlib = {} + +function systemlib.tmpdir(): path.path + return path.parse(system.tmpdir()) +end + +-- re-exports +systemlib.os = system.os +systemlib.arch = system.arch + +systemlib.threadcount = system.threadcount +systemlib.hostname = system.hostname +systemlib.totalmemory = system.totalmemory +systemlib.freememory = system.freememory +systemlib.uptime = system.uptime +systemlib.cpus = system.cpus + +-- boolean properties +systemlib.win32 = platform.win32 +systemlib.linux = platform.linux +systemlib.macos = platform.macos +systemlib.unix = platform.unix + +return table.freeze(systemlib) diff --git a/lute/std/libs/system/platform.luau b/lute/std/libs/system/platform.luau new file mode 100644 index 000000000..cefe662a0 --- /dev/null +++ b/lute/std/libs/system/platform.luau @@ -0,0 +1,10 @@ +local system = require("@lute/system") + +local osName = string.lower(system.os) + +local win32 = osName == "windows_nt" +local linux = osName == "linux" +local macos = osName == "darwin" +local unix = macos or linux or osName == "unix" or osName == "posix" + +return table.freeze({ win32 = win32, linux = linux, macos = macos, unix = unix }) diff --git a/lute/std/libs/table.luau b/lute/std/libs/table.luau deleted file mode 100644 index 5627f43c7..000000000 --- a/lute/std/libs/table.luau +++ /dev/null @@ -1,57 +0,0 @@ -export type array = { T } -export type dictionary = { [string]: T } - -local function map(table: { [K]: A }, f: (A) -> B): { [K]: B } - local new = {} - - for k, v in table do - new[k] = f(v) - end - - return new -end - -local function filter(table: { [K]: V }, predicate: (V) -> boolean): { [K]: V } - local new = {} - - for k, v in table do - if predicate(v) then - new[k] = v - end - end - - return new -end - -local function fold(table: { [K]: V }, f: (A, V) -> A, initial: A): A - local acc = initial - - for _, v in table do - acc = f(acc, v) - end - - return acc -end - -return table.freeze({ - -- std extension for table - map = map, - filter = filter, - fold = fold, - - -- re-exports of the table library - clear = table.clear, - clone = table.clone, - concat = table.concat, - create = table.create, - find = table.find, - freeze = table.freeze, - insert = table.insert, - isfrozen = table.isfrozen, - maxn = table.maxn, - move = table.move, - pack = table.pack, - remove = table.remove, - sort = table.sort, - unpack = table.unpack, -}) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau new file mode 100644 index 000000000..6f191c6b6 --- /dev/null +++ b/lute/std/libs/tableext.luau @@ -0,0 +1,96 @@ +--!strict +-- @std/tableext +-- stdlib for an extension of the built-in table library in Luau +-- Provides additional utility functions for table operations + +local tableext = {} + +function tableext.map(table: { [K]: A }, f: (A) -> B): { [K]: B } + local new = {} + + for k, v in table do + new[k] = f(v) + end + + return new +end + +function tableext.filter(table: { [K]: V }, predicate: (V) -> boolean): { [K]: V } + local new = {} + + for k, v in table do + if predicate(v) then + new[k] = v + end + end + + return new +end + +function tableext.any(arr: { T }, predicate: (T) -> boolean): boolean + for _, v in arr do + if predicate(v) then + return true + end + end + return false +end + +function tableext.all(arr: { T }, predicate: (T) -> boolean): boolean + for _, v in arr do + if not predicate(v) then + return false + end + end + return true +end + +function tableext.extend(tbl: { T }, ...: { T }) + local extensions = table.pack(...) + extensions.n = nil :: any + for _, extension in extensions do + for _, entry in extension do + table.insert(tbl, entry) + end + end +end + +function tableext.combine(tbl: { [K]: V }, ...: { [K]: V }): { [K]: V } + local tables = table.pack(...) + tables.n = nil :: any + for _, tocombine in tables do + for key, val in tocombine do + tbl[key] = val + end + end + return tbl +end + +function tableext.keys(tbl: { [K]: V }): { K } + local keys = {} + for k, _ in tbl do + table.insert(keys, k) + end + return keys +end + +function tableext.toset(tbl: { T }): { [T]: true } + local set: { [T]: true } = {} + for _, v in tbl do + set[v] = true + end + return set +end + +function tableext.reverse(tbl: { T }, inplace: boolean?) + local new = if inplace then tbl else {} + local i, j = 1, #tbl + while i <= j do + new[i], new[j] = tbl[j], tbl[i] + i += 1 + j -= 1 + end + return new +end + +return table.freeze(tableext) diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 1c22cbfe2..aede1082e 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -1,12 +1,19 @@ +--!strict +-- @std/task +-- stdlib for `@lute/task` +-- Provides utilities for creating and managing asynchronous tasks + local task = require("@lute/task") +local tasklib = {} + export type task = { success: boolean?, result: any, co: thread, } -local function create(f, ...): task +function tasklib.create(f, ...): task local data = {} data.co = coroutine.create(function(...) @@ -17,10 +24,10 @@ local function create(f, ...): task end) coroutine.resume(data.co, ...) - return data + return data :: task end -local function await(t: task) +function tasklib.await(t: task) if not t.co then error(`await: argument 1 is not a task`) end @@ -36,11 +43,10 @@ local function await(t: task) end end -local function awaitall(...: task) +function tasklib.awaitall(...: task) local tasks = table.pack(...) - tasks.n = nil - for i, v in tasks do + for i, v in ipairs(tasks) do if not v.co then error(`awaitAll: argument {i} is not a task`) end @@ -71,8 +77,4 @@ local function awaitall(...: task) return table.unpack(results) end -return table.freeze({ - create = create, - await = await, - awaitall = awaitall, -}) +return table.freeze(tasklib) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau new file mode 100644 index 000000000..cf3f1939c --- /dev/null +++ b/lute/std/libs/test/assert.luau @@ -0,0 +1,189 @@ +--!strict +-- @std/test/assert +-- Standard assertion library for Luau + +local failure = require("./failure") +local stringext = require("@std/stringext") +local types = require("./types") + +-- types +type failure = types.failure +type asserts = types.asserts + +-- more utils +local assertion = failure.assertion + +-- Helper to format values for error messages +local function formatValue(val: any): string + if type(val) == "nil" then + return "nil" + elseif type(val) == "string" then + return `"{val}"` + elseif type(val) == "table" then + return "table" + else + return tostring(val) + end +end + +-- Structural equality for two tables +local function tablesEqual(t1: unknown, t2: unknown, path: string): (boolean, string?) + if typeof(t1) ~= "table" or typeof(t2) ~= "table" then + if t1 ~= t2 then + return false, `lhs{path} = {formatValue(t1)} but rhs{path} = {formatValue(t2)}` + end + return true, nil + end + + -- Check all keys in t1 + for k, v1 in t1 do + local v2 = (t2 :: any)[k] + local newPath = if path == "" then `[{k}]` else `{path}[{k}]` + + -- Check if key exists in t2 + if v2 == nil then + return false, `lhs{newPath} = {formatValue(v1)} but rhs{newPath} does not exist` + end + + local equal, msg = tablesEqual(v1, v2, newPath) + if not equal then + return false, msg + end + end + + -- Check for keys in t2 that aren't in t1 + for k, v in t2 do + local v1 = (t1 :: any)[k] + if v1 == nil then + local newPath = if path == "" then `[{k}]` else `{path}[{k}]` + return false, `rhs{newPath} = {formatValue(v)} but lhs{newPath} does not exist` + end + end + + return true, nil +end + +local function eq(lhs: T, rhs: T, msg: string?): failure? + if lhs == rhs then + return nil + end + + return assertion(msg or `{lhs} ~= {rhs}`) +end + +local function neq(lhs: T, rhs: T, msg: string?): failure? + if lhs ~= rhs then + return nil + end + + return assertion(msg or `{lhs} == {rhs}`) +end + +local function errors(callback: () -> (), msg: string?): failure? + local success = pcall(callback) + -- if the callback failed, then this assert passes + if not success then + return nil + end + + return assertion(msg or `{callback} did not throw error.`) +end + +-- Table equality assertionsion with deep comparison +local function tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unknown }): failure? + local equal, msg = tablesEqual(lhs, rhs, "") + if equal then + return nil + end + return assertion(msg or "tables are not equal") +end + +local function buffereq(lhs: buffer, rhs: buffer): failure? + if typeof(lhs) ~= "buffer" then + return assertion(`lhs is of type {typeof(lhs)}, not buffer`) + end + + if typeof(rhs) ~= "buffer" then + return assertion(`rhs is of type {typeof(rhs)}, not buffer`) + end + + if buffer.len(lhs) ~= buffer.len(rhs) then + return assertion("buffers are of unequal length") + end + + local len = buffer.len(lhs) + for offset = 0, len - 1 do + local left = buffer.readu8(lhs, offset) + local right = buffer.readu8(rhs, offset) + + if left ~= right then + return assertion(string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right)) + end + end + return nil +end + +local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...): failure? + local success, err = pcall(func, ...) + if success then + return assertion("Function did not error as expected.") + end + + local actualErrorMessage = tostring(err) + -- pcall appends filename and line number of the error, so we check suffix only + if not stringext.hassuffix(actualErrorMessage, expectedErrorMessage) then + return assertion(`Expected suffix of error message "{expectedErrorMessage}", but got "{actualErrorMessage}"`) + end + + return nil +end + +local function strcontains(haystack: string, needle: string, msg: string?, instances: number?): failure? + instances = instances or 1 + + if instances <= 0 then + return assertion("instances must be greater than 0") + end + + if instances == 1 then + if haystack:find(needle, 1, true) == nil then + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) + end + else + if #haystack:split(needle) ~= instances + 1 then + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) + end + end + return nil +end + +local function strnotcontains(haystack: string, needle: string, msg: string?): failure? + if haystack:find(needle, 1, true) ~= nil then + return assertion(if msg then msg else `Expected "{haystack}" to not contain "{needle}".`) + end + + return nil +end + +-- utility for taking an assertion and wrapping it as a check that must complete successfully or will return control flow to the error handler +local function reqassert(req: (T...) -> failure?): (T...) -> failure? + return function(...): failure? + local result: failure? = req(...) + if result ~= nil then + error(result) + end + + return nil + end +end + +return table.freeze({ + eq = reqassert(eq), + neq = reqassert(neq), + errors = reqassert(errors), + tableeq = reqassert(tableeq), + buffereq = reqassert(buffereq), + erroreq = reqassert(erroreq), + strcontains = reqassert(strcontains), + strnotcontains = reqassert(strnotcontains), +}) :: asserts diff --git a/lute/std/libs/test/failure.luau b/lute/std/libs/test/failure.luau new file mode 100644 index 000000000..636dc77bc --- /dev/null +++ b/lute/std/libs/test/failure.luau @@ -0,0 +1,30 @@ +--!strict +-- @std/test/failure +-- utilities for working with test failures +local types = require("./types") + +type failure = types.failure + +local failures = {} +-- Generates the debug information needed to describe assertion failure locations +function failures.assertion(msg: string): failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + local filename, linenumber = debug.info(4, "sl") + local name = debug.info(2, "n") + return { assertion = name, msg = msg, filename = filename, linenumber = linenumber, __tag = "assertion" } +end + +-- Generates the debug information needed to describe assertion failure locations +function failures.lifecycle(hook: hook, err): failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + local filename, linenumber = debug.info(3, "sl") + return { hook = hook, msg = tostring(err), filename = filename, linenumber = linenumber, __tag = "lifecycle" } +end + +-- Generates the debug information needed to describe runtime error failure +function failures.runtimeerror(err): failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + return { msg = tostring(err), stacktrace = debug.traceback(tostring(err)), __tag = "runtime_error" } +end + +return table.freeze(failures) diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau new file mode 100644 index 000000000..eaf5ca4d4 --- /dev/null +++ b/lute/std/libs/test/init.luau @@ -0,0 +1,111 @@ +--!strict +-- @std/test +-- Standard test library for Luau + +local types = require("@self/types") + +local reporter = require("@self/reporter") +local runner = require("@self/runner") +local ps = require("@std/process") + +type Test = types.test +type TestCase = types.testcase +type TestSuite = types.testsuite +type TestEnvironment = types.testenvironment +type TestReporter = types.testreporter + +local TestSuite = {} +TestSuite.__index = TestSuite + +function TestSuite.new(name: string): TestSuite + local self = setmetatable({ + name = name, + cases = {}, + _beforeeach = nil, + _beforeall = nil, + _aftereach = nil, + _afterall = nil, + }, TestSuite) + return self :: TestSuite +end + +function TestSuite:case(name: string, testCase: Test) + table.insert(self.cases, { name = name, case = testCase }) +end + +function TestSuite:beforeeach(hook: () -> ()) + self._beforeeach = hook +end + +function TestSuite:beforeall(hook: () -> ()) + self._beforeall = hook +end + +function TestSuite:aftereach(hook: () -> ()) + self._aftereach = hook +end + +function TestSuite:afterall(hook: () -> ()) + self._afterall = hook +end + +local env: TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, caseindex = {} } + +-- Test library export surface +local test = {} + +-- register an anonymous test case +function test.case(name: string, case: Test) + local testcase = { name = name, case = case } + table.insert(env.anonymous, testcase) + + -- Update caseindex + if not env.caseindex[name] then + env.caseindex[name] = { anonymous = { testcase }, suites = {} } + else + table.insert(env.caseindex[name].anonymous, testcase) + end +end + +-- register a test suite +function test.suite(name: string, registerFn: (TestSuite) -> ()) + local suite = TestSuite.new(name) + registerFn(suite) + table.insert(env.suites, suite) + + -- Update suiteindex + env.suiteindex[name] = suite + + -- Update caseindex for all cases in this suite + for _, testcase in suite.cases do + if not env.caseindex[testcase.name] then + env.caseindex[testcase.name] = { anonymous = {}, suites = {} } + end + if not env.caseindex[testcase.name].suites[name] then + env.caseindex[testcase.name].suites[name] = {} + end + table.insert(env.caseindex[testcase.name].suites[name], testcase) + end +end + +-- get all registered tests without running them (internal) +function test._registered() + return table.freeze({ + anonymous = env.anonymous, + suites = env.suites, + suiteindex = env.suiteindex, + caseindex = env.caseindex, + }) +end + +-- run all the tests +function test.run() + local failed = runner.run(env) + reporter.simple(failed) + if failed.failed ~= 0 then + ps.exit(1) + end + ps.exit(0) +end + +return table.freeze(test) diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau new file mode 100644 index 000000000..8f0a48a10 --- /dev/null +++ b/lute/std/libs/test/reporter.luau @@ -0,0 +1,49 @@ +--!strict +-- @std/test/reporter +-- Standard test reporter library for Luau + +local types = require("./types") +local assert = require("./assert") + +type FailedTest = types.failedtest +type TestRunResult = types.testrunresult +type Assertions = assert.asserts + +local reporter = {} + +local function printFailedTest(failed: FailedTest) + print(`❌ {failed.test}`) + if failed.failure.__tag == "assertion" then + print(`\t{failed.failure.filename}:{failed.failure.linenumber}`) + print(`\t\t{failed.failure.assertion}: {failed.failure.msg}`) + elseif failed.failure.__tag == "lifecycle" then + print(`\t{failed.failure.filename}:{failed.failure.linenumber}`) + print(`\t\t{failed.failure.hook}: {failed.failure.msg}`) + else + print(`\tRuntime error: {failed.failure.msg}`) + print(`Stacktrace:`) + print(failed.failure.stacktrace) + end + print() +end + +function reporter.simple(result: TestRunResult) + print("\n" .. string.rep("=", 50)) + print("TEST RESULTS") + print(string.rep("=", 50)) + + if #result.failures > 0 then + print(`\nFailed Tests ({#result.failures}):\n`) + for _, failed in result.failures do + printFailedTest(failed) + end + end + + print(string.rep("-", 50)) + print(`Total: {result.total}`) + print(`Passed: {result.passed} ✓`) + print(`Failed: {result.failed} ✗`) + print(string.rep("=", 50) .. "\n") +end + +return table.freeze(reporter) diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau new file mode 100644 index 000000000..2d6688934 --- /dev/null +++ b/lute/std/libs/test/runner.luau @@ -0,0 +1,161 @@ +--!strict +-- @std/test/runner +-- Standard test runner library for Luau + +local asserts = require("./assert") +local failure = require("./failure") +local types = require("./types") + +type Test = types.test +type TestCase = types.testcase +type TestSuite = types.testsuite +type TestEnvironment = types.testenvironment +type TestReporter = types.testreporter +type FailedTest = types.failedtest +type TestRunResult = types.testrunresult + +local runner = {} + +function runner.run(env: TestEnvironment): TestRunResult + local failures: { FailedTest } = {} + local total = 0 + local failed = 0 + local passed = 0 + + -- Error handler for beforeall hook + local function beforeallErrorHandler(suite: TestSuite) + return function(err) + -- If beforeall fails, skip all tests in the suite + total += #suite.cases + failed += #suite.cases + for _, tc in suite.cases do + local failedtest: FailedTest = { + test = `{suite.name}.{tc.name}`, + failure = failure.lifecycle("beforeall", err), + } + table.insert(failures, failedtest) + end + return err + end + end + + -- Error handler for beforeeach hook + local function beforeeachErrorHandler(testFullName: string) + return function(err) + failed += 1 + local failedtest: FailedTest = { + test = testFullName, + failure = failure.lifecycle("beforeeach", err), + } + table.insert(failures, failedtest) + return err + end + end + + -- Error handler for aftereach hook + local function afterachErrorHandler(testFullName: string) + return function(err) + local failedtest: FailedTest = { + test = testFullName, + failure = failure.lifecycle("aftereach", err), + } + table.insert(failures, failedtest) + return err + end + end + + -- Error handler for afterall hook + local function afterallErrorHandler(suiteName: string) + return function(err) + failed += 1 + local failedtest: FailedTest = { + test = suiteName, + failure = failure.lifecycle("afterall", err), + } + table.insert(failures, failedtest) + return err + end + end + + -- Run anonymous tests + for _, tc in env.anonymous do + total += 1 + local function handlefailure(err) + local wasruntimefailure = not (typeof(err) == "table" and err.__tag and err.__tag == "assertion") + local failedtest: FailedTest = { + test = tc.name, + failure = if wasruntimefailure then failure.runtimeerror(err) else err, + } + table.insert(failures, failedtest) + failed += 1 + end + + local success = xpcall(tc.case, handlefailure, asserts) + if success then + passed += 1 + end + end + + -- Run tests from suites + for _, suite in env.suites do + -- Run beforeall hook once per suite + if suite._beforeall then + local beforeallSuccess = xpcall(suite._beforeall, beforeallErrorHandler(suite)) + if not beforeallSuccess then + -- If beforeall fails, skip all tests in the suite + continue + end + end + + for _, tc in suite.cases do + total += 1 + local testFullName = `{suite.name}.{tc.name}` + local function handlefailure(err) + local wasruntimefailure = not (typeof(err) == "table" and err.__tag and err.__tag == "assertion") + local failedtest: FailedTest = { + test = testFullName, + failure = if wasruntimefailure then failure.runtimeerror(err) else err, + } + table.insert(failures, failedtest) + end + + -- Run beforeeach hook if it exists + if suite._beforeeach then + local beforeSuccess = xpcall(suite._beforeeach, beforeeachErrorHandler(testFullName)) + if not beforeSuccess then + continue + end + end + + local success = xpcall(tc.case, handlefailure, asserts) + -- Run aftereach hook if it exists (runs even if test failed) + if suite._aftereach then + local afterSuccess = xpcall(suite._aftereach, afterachErrorHandler(testFullName)) + if not afterSuccess then + -- If test passed but aftereach failed, mark as failed + success = false + end + end + -- if the test and the teardown method passed, the test passes + if success then + passed += 1 + else + failed += 1 + end + end + + -- Run afterall hook once per suite (runs even if tests failed) + if suite._afterall then + xpcall(suite._afterall, afterallErrorHandler(suite.name)) + end + end + + return { + failures = failures, + total = total, + failed = failed, + passed = passed, + } +end + +return table.freeze(runner) diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau new file mode 100644 index 000000000..2a00284ab --- /dev/null +++ b/lute/std/libs/test/types.luau @@ -0,0 +1,87 @@ +--!strict +-- @std/test/types +-- Standard test types library for Luau + +-- Test Reporting + +export type hook = "beforeeach" | "beforeall" | "aftereach" | "afterall" +export type failure = + { + assertion: string, -- name of the assertion + msg: string, -- failure message + filename: string, -- filename + linenumber: number, -- linenumber + __tag: "assertion", + } + | { + hook: hook, -- the name of the hook that broke + msg: string, -- failure message + filename: string, -- filename + linenumber: number, -- linenumber + __tag: "lifecycle", + } + | { + msg: string, -- failure message + stacktrace: string, -- stacktrace + __tag: "runtime_error", + } + +export type asserts = { + eq: (T, T, string?) -> failure?, + neq: (T, T, string?) -> failure?, + errors: (() -> (), string?) -> failure?, + tableeq: ({ [unknown]: unknown }, { [unknown]: unknown }) -> failure?, + buffereq: (buffer, buffer) -> failure?, + erroreq: ((A...) -> ...unknown, string, A...) -> failure?, + strcontains: (string, string, string?, number?) -> failure?, + strnotcontains: (string, string, string?) -> failure?, +} + +export type test = (asserts) -> () + +export type testcase = { name: string, case: test } + +export type testsuite = { + name: string, + cases: { testcase }, + _beforeeach: (() -> ())?, + _beforeall: (() -> ())?, + _aftereach: (() -> ())?, + _afterall: (() -> ())?, + case: (self: testsuite, string, test) -> (), + beforeeach: (self: testsuite, () -> ()) -> (), + beforeall: (self: testsuite, () -> ()) -> (), + aftereach: (self: testsuite, () -> ()) -> (), + afterall: (self: testsuite, () -> ()) -> (), +} + +export type failedtest = { + test: string, -- name of the test case that failed + failure: failure, +} + +export type testrunresult = { + failures: { failedtest }, + total: number, + failed: number, + passed: number, +} + +export type testreporter = (testrunresult) -> () + +-- Testing Environment +export type caseindexentry = { + anonymous: { testcase }, + suites: { [string]: { testcase } }, +} + +export type testenvironment = { + anonymous: { testcase }, + suites: { testsuite }, + suiteindex: { [string]: testsuite }, + caseindex: { [string]: caseindexentry }, +} + +export type testrunner = (testenvironment) -> testrunresult + +return {} diff --git a/lute/std/libs/time.luau b/lute/std/libs/time/duration.luau similarity index 71% rename from lute/std/libs/time.luau rename to lute/std/libs/time/duration.luau index bbf1e5a8b..5740b9c93 100644 --- a/lute/std/libs/time.luau +++ b/lute/std/libs/time/duration.luau @@ -1,3 +1,6 @@ +--!strict +-- Submodule of @std/time that provides utility functions for time durations + local NANOS_PER_SEC = 1_000_000_000 local NANOS_PER_MILLI = 1_000_000 local NANOS_PER_MICRO = 1_000 @@ -9,8 +12,8 @@ local HOURS_PER_DAY = 24 local DAYS_PER_WEEK = 7 type durationdata = { - seconds: number, - nanoseconds: number, + _seconds: number, + _nanoseconds: number, } local duration = {} @@ -21,8 +24,8 @@ export type duration = setmetatable function duration.create(seconds: number, nanoseconds: number): duration local self = { - seconds = seconds, - nanoseconds = nanoseconds, + _seconds = seconds, + _nanoseconds = nanoseconds, } return setmetatable(self, duration) @@ -70,36 +73,36 @@ function duration.weeks(weeks: number): duration end function duration.subsecmillis(self: duration): number - return math.floor(self.nanoseconds / NANOS_PER_MILLI) + return math.floor(self._nanoseconds / NANOS_PER_MILLI) end function duration.subsecmicros(self: duration): number - return math.floor(self.nanoseconds / NANOS_PER_MICRO) + return math.floor(self._nanoseconds / NANOS_PER_MICRO) end function duration.subsecnanos(self: duration): number - return self.nanoseconds + return self._nanoseconds end function duration.toseconds(self: duration): number - return self.seconds + self.nanoseconds / NANOS_PER_SEC + return self._seconds + self._nanoseconds / NANOS_PER_SEC end function duration.tomilliseconds(self: duration): number - return self.seconds * MILLIS_PER_SEC + self:subsecmillis() + return self._seconds * MILLIS_PER_SEC + self:subsecmillis() end function duration.tomicroseconds(self: duration): number - return self.seconds * MICROS_PER_SEC + self:subsecmicros() + return self._seconds * MICROS_PER_SEC + self:subsecmicros() end function duration.tonanoseconds(self: duration): number - return self.seconds * NANOS_PER_SEC + self:subsecnanos() + return self._seconds * NANOS_PER_SEC + self:subsecnanos() end function duration.__add(a: duration, b: duration): duration - local seconds = a.seconds + b.seconds - local nanoseconds = a.nanoseconds + b.nanoseconds + local seconds = a._seconds + b._seconds + local nanoseconds = a._nanoseconds + b._nanoseconds if nanoseconds >= NANOS_PER_SEC then seconds += 1 @@ -110,8 +113,8 @@ function duration.__add(a: duration, b: duration): duration end function duration.__sub(a: duration, b: duration): duration - local seconds = a.seconds - b.seconds - local nanoseconds = a.nanoseconds - b.nanoseconds + local seconds = a._seconds - b._seconds + local nanoseconds = a._nanoseconds - b._nanoseconds if nanoseconds < 0 then seconds -= 1 @@ -122,8 +125,8 @@ function duration.__sub(a: duration, b: duration): duration end function duration.__mul(a: duration, b: number): duration - local seconds = a.seconds * b - local nanoseconds = a.nanoseconds * b + local seconds = a._seconds * b + local nanoseconds = a._nanoseconds * b seconds += math.floor(nanoseconds / NANOS_PER_SEC) nanoseconds %= NANOS_PER_SEC @@ -132,8 +135,8 @@ function duration.__mul(a: duration, b: number): duration end function duration.__div(a: duration, b: number): duration - local seconds, extraSeconds = a.seconds / b, a.seconds % b - local nanoseconds, extraNanos = a.nanoseconds / b, a.nanoseconds % b + local seconds, extraSeconds = a._seconds / b, a._seconds % b + local nanoseconds, extraNanos = a._nanoseconds / b, a._nanoseconds % b nanoseconds += (extraSeconds * NANOS_PER_SEC + extraNanos) / b @@ -141,29 +144,27 @@ function duration.__div(a: duration, b: number): duration end function duration.__eq(a: duration, b: duration): boolean - return a.seconds == b.seconds and a.nanoseconds == b.nanoseconds + return a._seconds == b._seconds and a._nanoseconds == b._nanoseconds end function duration.__lt(a: duration, b: duration): boolean - if a.seconds == b.seconds then - return a.nanoseconds < b.nanoseconds + if a._seconds == b._seconds then + return a._nanoseconds < b._nanoseconds end - return a.seconds < b.seconds + return a._seconds < b._seconds end function duration.__le(a: duration, b: duration): boolean - if a.seconds == b.seconds then - return a.nanoseconds <= b.nanoseconds + if a._seconds == b._seconds then + return a._nanoseconds <= b._nanoseconds end - return a.seconds <= b.seconds + return a._seconds <= b._seconds end function duration.__tostring(self: duration): string - return string.format("%d.%09d", self.seconds, self.nanoseconds) + return string.format("%d.%09d", self._seconds, self._nanoseconds) end -return table.freeze({ - duration = duration, -}) +return table.freeze(duration) diff --git a/lute/std/libs/time/init.luau b/lute/std/libs/time/init.luau new file mode 100644 index 000000000..817123243 --- /dev/null +++ b/lute/std/libs/time/init.luau @@ -0,0 +1,12 @@ +--!strict +-- @std/time +-- Provides utility functions for time + +local duration = require("@self/duration") +local time = {} + +export type duration = duration.duration + +time.duration = duration + +return table.freeze(time) diff --git a/lute/std/libs/vector.luau b/lute/std/libs/vector.luau deleted file mode 100644 index fb2b590d5..000000000 --- a/lute/std/libs/vector.luau +++ /dev/null @@ -1,36 +0,0 @@ -local function withx(vec: vector, x: number): vector - return vector.create(x, vec.y, vec.z) -end - -local function withy(vec: vector, y: number): vector - return vector.create(vec.x, y, vec.z) -end - -local function withz(vec: vector, z: number): vector - return vector.create(vec.x, vec.y, z) -end - -return table.freeze({ - -- std extensions for vector - withx = withx, - withy = withy, - withz = withz, - - -- re-exports of the vector library - create = vector.create, - magnitude = vector.magnitude, - normalize = vector.normalize, - cross = vector.cross, - vector = vector.dot, - angle = vector.angle, - floor = vector.floor, - ceil = vector.ceil, - abs = vector.abs, - sign = vector.sign, - clamp = vector.clamp, - max = vector.max, - min = vector.min, - - zero = vector.zero, - one = vector.one, -}) diff --git a/lute/system/include/lute/system.h b/lute/system/include/lute/system.h index b856ca0b8..0bfcde888 100644 --- a/lute/system/include/lute/system.h +++ b/lute/system/include/lute/system.h @@ -2,6 +2,7 @@ #include "lua.h" #include "lualib.h" + #include // open the library as a standard global luau library @@ -21,6 +22,7 @@ int lua_freememory(lua_State* L); int lua_totalmemory(lua_State* L); int lua_hostname(lua_State* L); int lua_uptime(lua_State* L); +int lua_tmpdir(lua_State* L); static const luaL_Reg lib[] = { {"cpus", lua_cpus}, @@ -29,6 +31,7 @@ static const luaL_Reg lib[] = { {"totalmemory", lua_totalmemory}, {"hostname", lua_hostname}, {"uptime", lua_uptime}, + {"tmpdir", lua_tmpdir}, {nullptr, nullptr} }; diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 331a9db48..5d6277ae5 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -1,13 +1,17 @@ #include "lute/system.h" + +#include "lute/uvutils.h" + #include "lua.h" #include "lualib.h" + #include "uv.h" + #include #include #include #include #include -#include namespace libsystem { @@ -88,24 +92,12 @@ int lua_totalmemory(lua_State* L) int lua_hostname(lua_State* L) { - size_t sz = 255; - std::string hostname; - hostname.reserve(sz); - - int res = uv_os_gethostname(hostname.data(), &sz); - if (res == UV_ENOBUFS) - { - hostname.reserve(sz); // libuv updates the size to what's required - res = uv_os_gethostname(hostname.data(), &sz); - } - - if (res != 0) - { - luaL_error(L, "libuv error: %s", uv_strerror(res)); - } - - lua_pushstring(L, hostname.c_str()); + auto result = uvutils::getStringFromUv(uv_os_gethostname); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get hostname: %s", error->toString().c_str()); + std::string* hostname = result.get_if(); + lua_pushlstring(L, hostname->c_str(), hostname->size()); return 1; } @@ -123,6 +115,17 @@ int lua_uptime(lua_State* L) return 1; } + +int lua_tmpdir(lua_State* L) +{ + auto result = uvutils::getStringFromUv(uv_os_tmpdir); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get temporary directory: %s", error->toString().c_str()); + + std::string* tmpDir = result.get_if(); + lua_pushlstring(L, tmpDir->c_str(), tmpDir->size()); + return 1; +} } // namespace libsystem int luaopen_system(lua_State* L) diff --git a/lute/task/include/lute/task.h b/lute/task/include/lute/task.h index 99b60f6f3..5b2b69d22 100644 --- a/lute/task/include/lute/task.h +++ b/lute/task/include/lute/task.h @@ -15,11 +15,13 @@ int lua_defer(lua_State* L); int lua_wait(lua_State* L); int lua_spawn(lua_State* L); int lute_resume(lua_State* L); +int lua_delay(lua_State* L); static const luaL_Reg lib[] = { {"defer", lua_defer}, {"wait", lua_wait}, {"spawn", lua_spawn}, + {"delay", lua_delay}, {"resume", lute_resume}, diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index b4338ca76..23e1c8658 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -1,16 +1,19 @@ #include "lute/task.h" -#include "lua.h" -#include "lualib.h" #include "lute/ref.h" #include "lute/runtime.h" -#include "uv.h" -#include "lute/runtime.h" #include "lute/time.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + #include #include #include + // taken from extern/luau/VM/lcorolib.cpp static const char* const statnames[] = {"running", "suspended", "normal", "dead", "dead"}; @@ -22,11 +25,26 @@ struct WaitData uint64_t startedAtMs; + bool closed = false; bool putDeltaTimeOnStack; -}; + int nargs; + + void close() + { + if (!closed) + { + uv_timer_stop(&uvTimer); + closed = true; + } + } + ~WaitData() + { + close(); + } +}; -static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack) +static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack, int nargs) { WaitData* yield = new WaitData(); uv_timer_init(uv_default_loop(), &yield->uvTimer); @@ -35,6 +53,7 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT yield->startedAtMs = uv_now(uv_default_loop()); yield->uvTimer.data = yield; yield->putDeltaTimeOnStack = putDeltaTimeOnStack; + yield->nargs = nargs; uv_timer_start( &yield->uvTimer, @@ -45,16 +64,18 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT yield->resumptionToken->complete( [yield](lua_State* L) { - int stackReturnAmount = yield->putDeltaTimeOnStack ? 1 : 0; - if (stackReturnAmount) + int stackReturnAmount = yield->putDeltaTimeOnStack ? yield->nargs + 1 : yield->nargs; + + if (yield->putDeltaTimeOnStack) lua_pushnumber(L, static_cast(uv_now(uv_default_loop()) - yield->startedAtMs) / 1000.0); - delete yield; + uv_close(reinterpret_cast(&yield->uvTimer), [](uv_handle_t* handle) { + WaitData* yield = static_cast(handle->data); + delete yield; + }); return stackReturnAmount; } ); - - uv_timer_stop(&yield->uvTimer); }, milliseconds, 0 @@ -71,16 +92,80 @@ int lua_defer(lua_State* L) return lua_yield(L, 0); }; + +int lua_delay(lua_State* L) +{ + int type = lua_type(L, 1); + uint64_t milliseconds = 0; + + // Handle overloads + switch (type) + { + case LUA_TNUMBER: + milliseconds = static_cast(lua_tonumber(L, 1) * 1000); + break; + + case LUA_TUSERDATA: + { + double seconds = getSecondsFromTimespec(getTimespecFromDuration(L, 1)); + milliseconds = static_cast(seconds * 1000); + break; + } + + default: + luaL_errorL(L, "invalid type %s passed into task.delay", lua_typename(L, lua_type(L, 1))); + break; + }; + + // remove the wait time + lua_remove(L, 1); + + lua_State* passedLuaState; + + if (lua_isfunction(L, 1)) + { + lua_State* NL = lua_newthread(L); + lua_pop(L, 1); + + passedLuaState = NL; + + // get the function + lua_xpush(L, NL, 1); + + // remove the function + lua_remove(L, 1); + } + else if (lua_isthread(L, 1)) + { + passedLuaState = lua_tothread(L, 1); + lua_remove(L, 1); + } + else + { + luaL_error(L, "can only pass threads or functions to task.spawn"); + }; + + int nargs = lua_gettop(L); + lua_xmove(L, passedLuaState, nargs); + + yieldLuaStateFor(passedLuaState, milliseconds, false, nargs); + + return 1; +} + int lua_spawn(lua_State* L) { if (lua_isfunction(L, 1)) { lua_State* NL = lua_newthread(L); + // transfer arguments from other lua_State to the called lua_State lua_xpush(L, NL, 1); + // remove the function arg lua_remove(L, 1); + // insert the thread lua_insert(L, 1); } else if (!lua_isthread(L, 1)) @@ -89,6 +174,8 @@ int lua_spawn(lua_State* L) } lute_resume(L); + + // return the thread return 1; } @@ -116,7 +203,7 @@ int lua_wait(lua_State* L) break; }; - yieldLuaStateFor(L, milliseconds, true); + yieldLuaStateFor(L, milliseconds, true, 0); return lua_yield(L, 0); } diff --git a/lute/time/include/lute/time.h b/lute/time/include/lute/time.h index c7b2620bd..92c4d2301 100644 --- a/lute/time/include/lute/time.h +++ b/lute/time/include/lute/time.h @@ -2,7 +2,9 @@ #include "lua.h" #include "lualib.h" + #include "uv.h" + #include // open the library as a standard global luau library diff --git a/lute/time/src/time.cpp b/lute/time/src/time.cpp index 138f5d7e2..c73c3219d 100644 --- a/lute/time/src/time.cpp +++ b/lute/time/src/time.cpp @@ -1,13 +1,16 @@ #include "lute/time.h" + +#include "lute/userdatas.h" + #include "lua.h" #include "lualib.h" -#include "lute/userdatas.h" + #include "uv.h" +#include #include #include #include -#include const int64_t NANOSECONDS_PER_SECOND = 1000000000; const int64_t MICROSECONDS_PER_SECOND = 1000000; diff --git a/lute/vm/include/lute/vm.h b/lute/vm/include/lute/vm.h index ccac21805..7d83bbc72 100644 --- a/lute/vm/include/lute/vm.h +++ b/lute/vm/include/lute/vm.h @@ -1,10 +1,10 @@ #pragma once +#include "lute/spawn.h" + #include "lua.h" #include "lualib.h" -#include "lute/spawn.h" - // open the library as a standard global luau library int luaopen_vm(lua_State* L); // open the library as a table on top of the stack diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 26bd7a369..29bf4f659 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -2,15 +2,16 @@ #include "lute/ref.h" #include "lute/require.h" +#include "lute/requirevfs.h" #include "lute/runtime.h" #include "Luau/Require.h" -#include - #include "lua.h" #include "lualib.h" +#include + struct TargetFunction { std::shared_ptr runtime; @@ -178,14 +179,14 @@ static void* createChildVmRequireContext(lua_State* L) sizeof(RequireCtx), [](void* ptr) { - static_cast(ptr)->~RequireCtx(); + std::destroy_at(static_cast(ptr)); } ); if (!ctx) luaL_error(L, "unable to allocate RequireCtx"); - ctx = new (ctx) RequireCtx{}; + ctx = new (ctx) RequireCtx{std::make_unique()}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. @@ -214,7 +215,7 @@ int lua_spawn(lua_State* L) lua_getinfo(L, 1, "s", &ar); // Require the target module - RequireCtx ctx{}; + RequireCtx ctx{std::make_unique()}; luarequire_pushproxyrequire(child->GL, requireConfigInit, &ctx); lua_pushstring(child->GL, file); lua_pushstring(child->GL, ar.source); @@ -255,7 +256,7 @@ int lua_spawn(lua_State* L) ); // Remove the Ref we have in current VM, now it will not cause the actual lua_unref - target->~TargetFunction(); + std::destroy_at(target); } ); diff --git a/rokit.toml b/rokit.toml index cf1a46d09..fb2317eb8 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] -stylua = "JohnnyMorganz/StyLua@2.0.2" -lute = "luau-lang/lute@0.1.0-nightly.20250606" +stylua = "JohnnyMorganz/StyLua@2.3.0" +lute = "luau-lang/lute@0.1.0-nightly.20260109" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 796571aac..f243eb161 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,13 +4,30 @@ target_sources(Lute.Test PRIVATE src/doctest.h src/main.cpp + # Test fixtures and utilities + src/cliruntimefixture.h + src/lutefixture.h src/luteprojectroot.h + src/testreporter.h + + src/cliruntimefixture.cpp + src/lutefixture.cpp src/luteprojectroot.cpp + src/testreporter.cpp + # Test files + src/compile.test.cpp + src/configresolver.test.cpp + src/lutefs.test.cpp src/modulepath.test.cpp - src/require.test.cpp) + src/moduleresolver.test.cpp + src/packagerequire.test.cpp + src/require.test.cpp + src/staticrequires.test.cpp + src/stdsystem.test.cpp + src/typecheck.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) -target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Require Lute.Runtime Luau.CLI.lib) +target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Luau Lute.Require Lute.Runtime Luau.CLI.lib Luau.Analysis Luau.Compiler Luau.VM) target_compile_options(Lute.Test PRIVATE ${LUTE_OPTIONS}) diff --git a/tests/batteries/base64.test.luau b/tests/batteries/base64.test.luau new file mode 100644 index 000000000..5e5f4be68 --- /dev/null +++ b/tests/batteries/base64.test.luau @@ -0,0 +1,74 @@ +local test = require("@std/test") +local base64 = require("@batteries/base64") + +local function enc(s: string): string + return buffer.tostring(base64.encode(buffer.fromstring(s))) +end +local function dec(s: string): string + return buffer.tostring(base64.decode(buffer.fromstring(s))) +end + +-- RFC 4648 test vectors +local testVectors = { + { "", "" }, + { "f", "Zg==" }, + { "fo", "Zm8=" }, + { "foo", "Zm9v" }, + { "foob", "Zm9vYg==" }, + { "fooba", "Zm9vYmE=" }, + { "foobar", "Zm9vYmFy" }, + { "foobarb", "Zm9vYmFyYg==" }, + { "foobarba", "Zm9vYmFyYmE=" }, + { "foobarbaz", "Zm9vYmFyYmF6" }, +} + +local invalidInputs = { + "SGVsbG8\0", + "SGVsbG8\255", + "Hell@oWorld!", + "Test1#23", + "DataMon$ey", + "SGVsbG8-V29ybGQ_", + "SGVs=bG8=", +} + +local invalidLastBytes = { + "====", -- all padding + "A==A", -- char after padding + "A", -- invalid length + "AB==", -- impossible sequence + "AAB=", -- impossible sequence +} + +test.suite("Base64", function(suite) + for _, vector in ipairs(testVectors) do + local input = vector[1] + local expected_encoded = vector[2] + + suite:case("Encode: '" .. input .. "'", function(assert) + local encoded = enc(input) + assert.eq(expected_encoded, encoded) + end) + + suite:case("Decode: '" .. expected_encoded .. "'", function(assert) + local decoded = dec(expected_encoded) + assert.eq(input, decoded) + end) + end + + for _, invalid in invalidInputs do + suite:case("Invalid Decode: '" .. invalid .. "'", function(assert) + assert.errors(function() + dec(invalid) + end) + end) + end + + for _, invalid in invalidLastBytes do + suite:case("Invalid Decode (last bytes): '" .. invalid .. "'", function(assert) + assert.errors(function() + dec(invalid) + end) + end) + end +end) diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau new file mode 100644 index 000000000..590fe709a --- /dev/null +++ b/tests/batteries/collections/deque.test.luau @@ -0,0 +1,95 @@ +local deque = require("@batteries/collections/deque") +local test = require("@std/test") + +test.suite("deque", function(suite) + suite:case("deque.new with no value creates empty deque", function(assert) + local deq = deque.new() + assert.eq(deq:peekfront(), nil) + assert.eq(deq:peekback(), nil) + assert.eq(#deq, 0) + end) + + suite:case("deque.new with values creates deque with one element", function(assert) + local deq = deque.new(1) + assert.eq(deq:peekfront(), 1) + assert.eq(deq:peekback(), 1) + assert.eq(#deq, 1) + end) + + suite:case("deque:pushfront", function(assert) + local deq = deque.new(2) + deq:pushfront(1) + assert.eq(deq:peekfront(), 1) + assert.eq(deq:peekback(), 2) + assert.eq(#deq, 2) + end) + + suite:case("deque:pushback", function(assert) + local deq = deque.new(2) + deq:pushback(1) + assert.eq(deq:peekfront(), 2) + assert.eq(deq:peekback(), 1) + assert.eq(#deq, 2) + end) + + suite:case("deque:popfront", function(assert) + local deq = deque.new(2) + local popped = deq:popfront() + assert.eq(popped, 2) + assert.eq(#deq, 0) + assert.eq(deq:peekfront(), nil) + assert.eq(deq:peekback(), nil) + end) + + suite:case("deque:popback", function(assert) + local deq = deque.new(2) + local popped = deq:popback() + assert.eq(popped, 2) + assert.eq(#deq, 0) + assert.eq(deq:peekfront(), nil) + assert.eq(deq:peekback(), nil) + end) + + suite:case("deque:peekfront", function(assert) + local deq = deque.new(1) + assert.eq(#deq, 1) + assert.eq(deq:peekfront(), 1) + end) + + suite:case("deque:peekback", function(assert) + local deq = deque.new(1) + assert.eq(#deq, 1) + assert.eq(deq:peekback(), 1) + end) + + suite:case("popping from empty deque", function(assert) + local deq = deque.new() + assert.erroreq(function() + deq:popback() + end, "Popping from empty deque") + assert.erroreq(function() + deq:popfront() + end, "Popping from empty deque") + end) + + suite:case("e2e", function(assert) + local deq = deque.new() + deq:pushback(1) + deq:pushback(2) + deq:pushback(3) + + assert.eq(#deq, 3) + assert.eq(deq:peekfront(), 1) + assert.eq(deq:peekback(), 3) + + assert.eq(deq:popfront(), 1) + assert.eq(#deq, 2) + assert.eq(deq:peekfront(), 2) + assert.eq(deq:peekback(), 3) + + assert.eq(deq:popback(), 3) + assert.eq(#deq, 1) + assert.eq(deq:peekfront(), 2) + assert.eq(deq:peekback(), 2) + end) +end) diff --git a/tests/batteries/difftext.test.luau b/tests/batteries/difftext.test.luau new file mode 100644 index 000000000..37908dab4 --- /dev/null +++ b/tests/batteries/difftext.test.luau @@ -0,0 +1,493 @@ +local difftext = require("@batteries/difftext") +local richterm = require("@batteries/richterm") +local test = require("@std/test") + +local diff = difftext.diff + +-- Each diff operation has structure: { key: "EQUAL" | "ADD" | "DELETE", text: string } +-- Order matters - the sequence of operations defines the transformation + +test.suite("myersdiff - bychar", function(suite) + suite:case("identical strings", function(assert) + local a = "abc" + local b = "abc" + local diffResult = diff(a, b, { byChar = true }) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("single character change in middle", function(assert) + local a = "abc" + local b = "axc" + local diffResult = diff(a, b, { byChar = true }) + -- a(EQUAL), b(DELETE), x(ADD), c(EQUAL) + assert.eq(#diffResult, 4) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "x") + assert.eq(diffResult[4].key, "EQUAL") + assert.eq(diffResult[4].text, "c") + end) + + suite:case("character addition at start", function(assert) + local a = "bc" + local b = "abc" + local diffResult = diff(a, b, { byChar = true }) + -- a(ADD), b(EQUAL), c(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "ADD") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("character addition at end", function(assert) + local a = "ab" + local b = "abc" + local diffResult = diff(a, b, { byChar = true }) + -- a(EQUAL), b(EQUAL), c(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("character deletion at start", function(assert) + local a = "abc" + local b = "bc" + local diffResult = diff(a, b, { byChar = true }) + -- a(DELETE), b(EQUAL), c(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("character deletion at end", function(assert) + local a = "abc" + local b = "ab" + local diffResult = diff(a, b, { byChar = true }) + -- a(EQUAL), b(EQUAL), c(DELETE) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "DELETE") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("multiple consecutive changes", function(assert) + local a = "abc" + local b = "xyz" + local diffResult = diff(a, b, { byChar = true }) + -- a(DELETE), b(DELETE), c(DELETE), x(ADD), y(ADD), z(ADD) + assert.eq(#diffResult, 6) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "DELETE") + assert.eq(diffResult[3].text, "c") + assert.eq(diffResult[4].key, "ADD") + assert.eq(diffResult[4].text, "x") + assert.eq(diffResult[5].key, "ADD") + assert.eq(diffResult[5].text, "y") + assert.eq(diffResult[6].key, "ADD") + assert.eq(diffResult[6].text, "z") + end) + + suite:case("empty to non-empty", function(assert) + local a = "" + local b = "hi" + local diffResult = diff(a, b, { byChar = true }) + -- h(ADD), i(ADD) + assert.eq(#diffResult, 2) + assert.eq(diffResult[1].key, "ADD") + assert.eq(diffResult[1].text, "h") + assert.eq(diffResult[2].key, "ADD") + assert.eq(diffResult[2].text, "i") + end) + + suite:case("non-empty to empty", function(assert) + local a = "hi" + local b = "" + local diffResult = diff(a, b, { byChar = true }) + -- h(DELETE), i(DELETE) + assert.eq(#diffResult, 2) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "h") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "i") + end) +end) + +test.suite("myersdiff - byline", function(suite) + suite:case("identical multiline strings", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- All lines should be EQUAL + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("add line at start", function(assert) + local a = "line2\nline3" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- line1(ADD), line2(EQUAL), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "ADD") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("add line at end", function(assert) + local a = "line1\nline2" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(EQUAL), line3(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("add line in middle", function(assert) + local a = "line1\nline3" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(ADD), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "ADD") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("delete line at start", function(assert) + local a = "line1\nline2\nline3" + local b = "line2\nline3" + local diffResult = diff(a, b) + -- line1(DELETE), line2(EQUAL), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("delete line at end", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline2" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(EQUAL), line3(DELETE) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "DELETE") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("delete line in middle", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(DELETE), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("modify single line", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nmodified\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(DELETE), modified(ADD), line3(EQUAL) + assert.eq(#diffResult, 4) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "modified") + assert.eq(diffResult[4].key, "EQUAL") + assert.eq(diffResult[4].text, "line3") + end) + + suite:case("multiple line changes", function(assert) + local a = "A\nB\nC\nD" + local b = "A\nX\nC\nY\nD" + local diffResult = diff(a, b) + -- A(EQUAL), B(DELETE), X(ADD), C(EQUAL), Y(ADD), D(EQUAL) + assert.eq(#diffResult, 6) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "A") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "B") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "X") + assert.eq(diffResult[4].key, "EQUAL") + assert.eq(diffResult[4].text, "C") + assert.eq(diffResult[5].key, "ADD") + assert.eq(diffResult[5].text, "Y") + assert.eq(diffResult[6].key, "EQUAL") + assert.eq(diffResult[6].text, "D") + end) + + suite:case("empty to multiline", function(assert) + local a = "" + local b = "line1\nline2" + local diffResult = diff(a, b) + -- When splitting "" by "\n", we get {""}, so it's: ""(DELETE), line1(ADD), line2(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "") + assert.eq(diffResult[2].key, "ADD") + assert.eq(diffResult[2].text, "line1") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "line2") + end) + + suite:case("multiline to empty", function(assert) + local a = "line1\nline2" + local b = "" + local diffResult = diff(a, b) + -- line1(DELETE), line2(DELETE), ""(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "") + end) +end) + +test.suite("printdiff", function(suite) + local function composeDetailedRemoval(str: string, changedCharPos: { [number]: true }, sideHeader: string?) + local composed = richterm.brightRed(`- {sideHeader or ""}`) + for i = 1, #str do + if changedCharPos[i] then + composed ..= richterm.bgRed(str:sub(i, i)) + else + composed ..= richterm.brightRed(str:sub(i, i)) + end + end + return composed + end + + local function composeDetailedAddition(str: string, changedCharPos: { [number]: true }, sideHeader: string?) + local composed = richterm.brightGreen(`+ {sideHeader or ""}`) + for i = 1, #str do + if changedCharPos[i] then + composed ..= richterm.bgGreen(str:sub(i, i)) + else + composed ..= richterm.brightGreen(str:sub(i, i)) + end + end + return composed + end + + suite:case("printdiff default - simple addition", function(assert) + local a = "line1\nline2" + local b = "line1\nline2\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 3) + assert.eq(lines[1], " line1") + assert.eq(lines[2], " line2") + assert.eq(lines[3], richterm.brightGreen("+ line3")) + end) + + suite:case("printdiff default - simple deletion", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 3) + assert.eq(lines[1], " line1") + assert.eq(lines[2], richterm.brightRed("- line2")) + assert.eq(lines[3], " line3") + end) + + suite:case("printdiff default - modification", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nmodified\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 4) + assert.eq(lines[1], " line1") + assert.eq(lines[2], richterm.brightRed("- line2")) + assert.eq(lines[3], richterm.brightGreen("+ modified")) + assert.eq(lines[4], " line3") + end) + + suite:case("printdiff default - unchanged lines", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline2\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 3) + assert.eq(lines[1], " line1") + assert.eq(lines[2], " line2") + assert.eq(lines[3], " line3") + end) + + suite:case("printdiff default - multiple changes", function(assert) + local a = "A\nB\nC\nD" + local b = "A\nX\nC\nY\nD" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 6) + assert.eq(lines[1], " A") + assert.eq(lines[2], richterm.brightRed("- B")) + assert.eq(lines[3], richterm.brightGreen("+ X")) + assert.eq(lines[4], " C") + assert.eq(lines[5], richterm.brightGreen("+ Y")) + assert.eq(lines[6], " D") + end) + + suite:case("printdiff default - with line numbers", function(assert) + local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" + local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" + local result = difftext.prettydiff(a, b, { + includeLineNumbers = true, + }) + local lines = result:split("\n") + + assert.eq(#lines, 10) + for i, line in lines do + if i < 10 then + assert.eq(line, ` {i} {i}| line{i}`) + else + assert.eq(line, ` {i} {i}| line{i}`) + end + end + end) + + suite:case("printdiff detailed - single line change", function(assert) + local a = "hello world" + local b = "hello wirld" + local result = difftext.prettydiff(a, b, { detailed = true }) + local lines = result:split("\n") + + -- Detailed mode shows both old and new lines with character-level highlighting + assert.eq(#lines, 2) + assert.eq( + lines[1], + composeDetailedRemoval("hello world", { [8] = true }), + `Not equal:\n{difftext.prettydiff(lines[1], composeDetailedRemoval("hello world", { [8] = true }))}` + ) + assert.eq(lines[2], composeDetailedAddition("hello wirld", { [8] = true })) + end) + + suite:case("printdiff detailed - multiple line changes", function(assert) + local a = "line1\nold line\nline3" + local b = "line1\nnew line\nline3" + local result = difftext.prettydiff(a, b, { detailed = true }) + local lines = result:split("\n") + + local changedChars = { + [1] = true, + [2] = true, + [3] = true, + } + + -- Should have: line1, - old line, + new line, line3 + assert.eq(#lines, 4) + assert.eq(lines[1], " line1") + assert.eq(lines[2], composeDetailedRemoval("old line", changedChars)) + assert.eq(lines[3], composeDetailedAddition("new line", changedChars)) + assert.eq(lines[4], " line3") + end) + + suite:case("printdiff detailed - unchanged lines", function(assert) + local a = "line1\nline2" + local b = "line1\nline2" + local result = difftext.prettydiff(a, b, { detailed = true }) + local lines = result:split("\n") + + assert.eq(#lines, 2) + assert.eq(lines[1], " line1") + assert.eq(lines[2], " line2") + end) + + suite:case("printdiff detailed - with line numbers", function(assert) + local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" + local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nlineten" + local result = difftext.prettydiff(a, b, { + detailed = true, + includeLineNumbers = true, + }) + local lines = result:split("\n") + + assert.eq(#lines, 11) + for i = 1, 9 do + local line = lines[i] + assert.eq(line, ` {i} {i}| line{i}`) + end + assert.eq(lines[10], composeDetailedRemoval("line10", { [5] = true, [6] = true }, "10 | ")) + assert.eq(lines[11], composeDetailedAddition("lineten", { [5] = true, [6] = true, [7] = true }, " 10| ")) + end) + + suite:case("printdiff detailed - sanity check w/ 3 digits of line numbers", function(assert) + local a = "unmodified\n" .. string.rep("line\n", 99) .. "unmodified" + local b = "modified\n" .. string.rep("line\n", 99) .. "modified" + local result = difftext.prettydiff(a, b, { + detailed = true, + includeLineNumbers = true, + }) + local lines = result:split("\n") + + assert.eq(#lines, 103) + assert.eq(lines[1], composeDetailedRemoval("unmodified", { [1] = true, [2] = true }, " 1 | ")) + assert.eq(lines[2], composeDetailedAddition("modified", {}, " 1| ")) + assert.eq(lines[101], " 100 100| line") + assert.eq(lines[102], composeDetailedRemoval("unmodified", { [1] = true, [2] = true }, "101 | ")) + assert.eq(lines[103], composeDetailedAddition("modified", {}, " 101| ")) + end) +end) diff --git a/tests/cli/check.test.luau b/tests/cli/check.test.luau new file mode 100644 index 000000000..ed33c3756 --- /dev/null +++ b/tests/cli/check.test.luau @@ -0,0 +1,16 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) + +test.suite("LuteTypeCheck", function(suite) + suite:case("UsesNewSolver", function(assert) + local testFilePath = path.format(path.join("tests", "src", "staticrequires", "newsolver.luau")) + + local result = process.run({ lutePath, "check", testFilePath }) + assert.eq(result.exitcode, 0) + end) +end) diff --git a/tests/cli/compile.test.luau b/tests/cli/compile.test.luau new file mode 100644 index 000000000..d50631a86 --- /dev/null +++ b/tests/cli/compile.test.luau @@ -0,0 +1,50 @@ +local fs = require("@std/fs") +local process = require("@std/process") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local COMPILER_CONTENTS = [[ +local luau = require("@lute/luau") +local fs = require("@std/fs") + +local args = { ... } +assert(#args == 2, "Expected one argument: path to Luau script to compile") + +local handle = fs.open(args[2], "r") +local contents = fs.read(handle) +fs.close(handle) +luau.compile(contents) +print(`SUCCESS`) +]] + +local COMPILEE_CONTENTS = [[return "Hello world"]] + +local lutePath = process.execpath() +local tmpDir = system.tmpdir() + +test.suite("Lute CLI Compile", function(suite) + suite:case("Run compile", function(check) + -- Setup + -- Create files + local compilerPath = path.join(tmpDir, "compiler.luau") + local compilerFile = fs.open(compilerPath, "w+") + fs.write(compilerFile, COMPILER_CONTENTS) + fs.close(compilerFile) + + local compileePath = path.join(tmpDir, "compilee.luau") + local compileeFile = fs.open(compileePath, "w+") + fs.write(compileeFile, COMPILEE_CONTENTS) + fs.close(compileeFile) + + local result = process.run({ path.format(lutePath), path.format(compilerPath), path.format(compileePath) }) + -- Compilation should work with no memory leaks + check.eq(result.stderr, "") + check.eq(result.stdout, "SUCCESS\n") + check.eq(result.exitcode, 0) + + -- -- Cleanup + fs.remove(compilerPath) + fs.remove(compileePath) + end) +end) diff --git a/tests/cli/discovery/example.spec.luau b/tests/cli/discovery/example.spec.luau new file mode 100644 index 000000000..e1280ef91 --- /dev/null +++ b/tests/cli/discovery/example.spec.luau @@ -0,0 +1,7 @@ +local test = require("@std/test") + +test.suite("ExampleSuite", function(suite) + suite:case("should pass", function(assert) + assert.eq(2 + 2, 4) + end) +end) diff --git a/tests/cli/discovery/smoke.test.luau b/tests/cli/discovery/smoke.test.luau new file mode 100644 index 000000000..eb7a52fdf --- /dev/null +++ b/tests/cli/discovery/smoke.test.luau @@ -0,0 +1,13 @@ +local test = require("@std/test") + +test.suite("SmokeSuite", function(suite) + suite:case("make_fail", function(assert) + -- assert.eq(1, 2) + assert.eq(1, 1) -- comment this and uncomment above to make the test fail + end) + suite:case("make_pass", function(assert) + assert.eq(1, 1) + end) +end) + +test.case("Passing", function(assert) end) diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau new file mode 100644 index 000000000..15843aa2d --- /dev/null +++ b/tests/cli/lib/files.test.luau @@ -0,0 +1,273 @@ +local files = require("@commands/lib/files") +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local tmpdir = system.tmpdir() +local testPath = path.join(tmpdir, "gitignore_test") + +test.suite("cli/lib/files", function(suite) + suite:beforeeach(function() + if fs.exists(testPath) then + fs.removedirectory(testPath, { recursive = true }) + end + end) + + suite:case("parse simple file pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "*.lua\n") + + local shouldIgnorePath = path.join(testPath, "debug.lua") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldNotIgnorePath = path.join(testPath, "readme.luau") + fs.writestringtofile(shouldNotIgnorePath, "") + + local shouldNotIgnoreDirectory = path.join(testPath, "lua") + fs.createdirectory(shouldNotIgnoreDirectory) + local shouldNotIgnoreNestedPath = path.join(shouldNotIgnoreDirectory, "readme.luau") + fs.writestringtofile(shouldNotIgnoreNestedPath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + assert.eq(resultPaths[path.format(shouldNotIgnoreNestedPath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse directory pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "node_modules/\n") + + local dirPath = path.join(testPath, "node_modules") + fs.createdirectory(dirPath) + local shouldIgnoreFile = path.join(dirPath, "package.luau") + fs.writestringtofile(shouldIgnoreFile, "") + + local nestedDirPath = path.join(testPath, "a", "node_modules") + fs.createdirectory(nestedDirPath, { makeparents = true }) + local shouldIgnoreNestedFile = path.join(nestedDirPath, "nested_package.luau") + fs.writestringtofile(shouldIgnoreNestedFile, "") + + local shouldNotIgnoreFile = path.join(testPath, "node_modules.luau") + fs.writestringtofile(shouldNotIgnoreFile, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnoreFile)], nil) + assert.eq(resultPaths[path.format(shouldIgnoreNestedFile)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnoreFile)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse negation pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "*.lua\n!important.lua\n") + + local shouldIgnorePath = path.join(testPath, "debug.lua") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldNotIgnorePath = path.join(testPath, "important.lua") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse leading double asterisk pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "**/temp/foo\n") + + local subdir = path.join(testPath, "src", "temp", "foo") + fs.createdirectory(subdir, { makeparents = true }) + + local shouldIgnorePath = path.join(subdir, "template.luau") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldntIgnorePath = path.join(testPath, "src", "temp", "hello.luau") + fs.writestringtofile(shouldntIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse trailing double asterisk pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "temp/foo/**\n") + + local subdir = path.join(testPath, "temp", "foo", "src") + fs.createdirectory(subdir, { makeparents = true }) + + local shouldIgnorePath = path.join(subdir, "template.luau") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldIgnorePath2 = path.join(testPath, "temp", "foo", "template.luau") + fs.writestringtofile(shouldIgnorePath2, "") + + local shouldntIgnorePath = path.join(testPath, "temp", "hello.luau") + fs.writestringtofile(shouldntIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldIgnorePath2)], nil) + assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse middle double asterisk pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "temp/**/foo\n") + + local subdir = path.join(testPath, "temp", "src", "foo") + fs.createdirectory(subdir, { makeparents = true }) + + local shouldIgnorePath = path.join(subdir, "template.luau") + fs.writestringtofile(shouldIgnorePath, "") + + local subdir2 = path.join(testPath, "temp", "src", "test", "foo") + fs.createdirectory(subdir2, { makeparents = true }) + + local shouldIgnorePath2 = path.join(subdir2, "template.luau") + fs.writestringtofile(shouldIgnorePath2, "") + + local shouldntIgnorePath = path.join(testPath, "temp", "hello.luau") + fs.writestringtofile(shouldntIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldIgnorePath2)], nil) + assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse rooted pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "/build\n") + + fs.createdirectory(path.join(testPath, "build"), { makeparents = true }) + + local shouldIgnorePath = path.join(testPath, "build", "foo.luau") + fs.writestringtofile(shouldIgnorePath, "") + + fs.createdirectory(path.join(testPath, "src", "build"), { makeparents = true }) + + local shouldNotIgnorePath = path.join(testPath, "src", "build", "bar.luau") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[(path.format(shouldIgnorePath))], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse question mark pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "file?.luau\n") + + local shouldIgnorePath1 = path.join(testPath, "file1.luau") + fs.writestringtofile(shouldIgnorePath1, "") + + local shouldIgnorePath2 = path.join(testPath, "fileA.luau") + fs.writestringtofile(shouldIgnorePath2, "") + + local shouldNotIgnorePath = path.join(testPath, "file10.luau") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath1)], nil) + assert.eq(resultPaths[path.format(shouldIgnorePath2)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) +end) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau new file mode 100644 index 000000000..447eade37 --- /dev/null +++ b/tests/cli/lint.test.luau @@ -0,0 +1,1587 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) +local tmpDir = system.tmpdir() +local rulesDir = path.format(path.join(".", "lints")) +local defaultRulesFolder = path.format(path.join("lute", "cli", "commands", "lint", "rules")) +local defaultRulesPaths = { + almost_swapped = path.format(path.join(defaultRulesFolder, "almost_swapped.luau")), + constant_table_comparison = path.format(path.join(defaultRulesFolder, "constant_table_comparison.luau")), + divide_by_zero = path.format(path.join(defaultRulesFolder, "divide_by_zero.luau")), + parenthesized_conditions = path.format(path.join(defaultRulesFolder, "parenthesized_conditions.luau")), +} + +local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" + +test.suite("lute lint", function(suite) + suite:beforeeach(function() + -- A test that creates rulesDir which fails won't cleanup after itself + if fs.exists(rulesDir) then + fs.removedirectory(rulesDir, { recursive = true }) + end + + local modulePath = path.join(tmpDir, "module") + if fs.exists(modulePath) then + fs.removedirectory(modulePath, { recursive = true }) + end + end) + + suite:case("lute lint help message", function(assert) + local result = process.run({ lutePath, "lint", "-h" }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, USAGE) + + result = process.run({ lutePath, "lint", "--h" }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, USAGE) + + result = process.run({ lutePath, "lint", "--help" }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, USAGE) + end) + + suite:case("lute lint no input file", function(assert) + local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) + + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "Error: No input files or string input specified.") + end) + + suite:case("lute lint error code 0 with no violations", function(assert) + local modulePath = path.join(tmpDir, "module") + fs.createdirectory(modulePath) + + local result = process.run({ lutePath, "lint", path.format(modulePath) }) + + assert.eq(result.exitcode, 0) + + assert.eq(result.stdout, "") + + fs.removedirectory(modulePath) + end) + + suite:case("lute lint non verbose", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile(violatorPath, "") + + -- Do + -- Run the linter + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.eq(result.stdout, "") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint verbose", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile(violatorPath, "") + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-v", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "Using default lint rules.") + assert.neq(result.stdout:find("Reading input file '.+violator%.luau'"), nil) + assert.neq(result.stdout:find("Parsing input file '.+violator%.luau'"), nil) + assert.strcontains(result.stdout, "Applying lint rules") + assert.strcontains(result.stdout, "Printing violations from 0 files") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("divide_by_zero", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("almost_swapped", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + +local x = 0 +local y = "hello" +x = y +y = x + +local t = { a = 1, b = 2 } +t.a = t.b +t.b = t.a + +if true then + t["a"] = t["b"] + t["b"] = t["a"] +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.almost_swapped, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + -- We expect 4 warnings, so stdout should be split into 5 parts + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 4) + local expected = [[ +violator.luau:1:1-2:6 ── + │ + 1 │ ╭ a = b + 2 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = "Suggested fix: a, b = b, a" + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:6:1-7:6 ── + │ + 6 │ ╭ x = y + 7 │ │ y = x + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = "Suggested fix: x, y = y, x" + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:10:1-11:10 ── + │ + 10 │ ╭ t.a = t.b + 11 │ │ t.b = t.a + │ ╰─────────^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = "Suggested fix: t.a, t.b = t.b, t.a" + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:14:2-15:17 ── + │ + 14 │ ╭ t["a"] = t["b"] + 15 │ │ t["b"] = t["a"] + │ ╰───────────────────^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = 'Suggested fix: t["a"], t["b"] = t["b"], t["a"]' + + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("almost swapped auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + +local x = 0 +local y = "hello" +x = y +y = x + +local t = { a = 1, b = 2 } +t.a = t.b +t.b = t.a + +if true then + t["a"] = t["b"] + t["b"] = t["a"] +end +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) -- After applying the auto-fix, there are no more violations + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +a, b = b, a + +local x = 0 +local y = "hello" +x, y = y, x + +local t = { a = 1, b = 2 } +t.a, t.b = t.b, t.a + +if true then + t["a"], t["b"] = t["b"], t["a"] +end +]] + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint multiple rules", function(assert) + -- Setup + fs.createdirectory(rulesDir) + -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau + -- Because the default rules require types using a relative path, we copy from examples, which uses an absolute require path + fs.copy(path.join("examples", "lints", "almost_swapped.luau"), path.join(rulesDir, "almost_swapped.luau")) + -- Copy divide_by_zero to rulesDir/divide_by_zero/init.luau + local divideByZeroDir = path.join(rulesDir, "divide_by_zero") + fs.createdirectory(divideByZeroDir) + fs.copy(path.join("examples", "lints", "divide_by_zero.luau"), path.join(divideByZeroDir, "init.luau")) + fs.writestringtofile( + path.join(rulesDir, "bogus.txt"), + [[ +not a luau file +]] + ) + + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + expected = [[ +violator.luau:3:1-4:6 ── + │ + 3 │ ╭ a = b + 4 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + fs.removedirectory(rulesDir, { recursive = true }) + end) + + suite:case("lute lint multiple rules but one errors", function(assert) + -- Setup + fs.createdirectory(rulesDir) + -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau + fs.copy(path.join("examples", "lints", "almost_swapped.luau"), path.join(rulesDir, "almost_swapped.luau")) + -- Create a rule that fails + fs.writestringtofile( + path.join(rulesDir, "erroring_rule.luau"), + [[ +return table.freeze({ + name = "AlwaysErrors", + lint = function(ast, sourcepath) + error("This rule always errors") + end, +}) +]] + ) + + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + local expected = [[ +violator.luau:1:1-2:6 ── + │ + 1 │ ╭ a = b + 2 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + assert.neq( + result.stdout:find( + "Error applying lint rule 'AlwaysErrors': %.[/\\]lints[/\\]erroring_rule%.luau:4: This rule always errors", + 1 + ), + nil + ) + + -- Teardown + fs.remove(violatorPath) + fs.removedirectory(rulesDir, { recursive = true }) + end) + + suite:case("lute lint multiple input files", function(assert) + -- Setup + -- Create multiple files that violate the rule + local violatorPath1 = path.format(path.join(tmpDir, "violator1.luau")) + fs.writestringtofile( + violatorPath1, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + local modulePath = path.join(tmpDir, "module") + fs.createdirectory(modulePath) + local violatorPath2 = path.format(path.join(modulePath, "violator2.lua")) + fs.writestringtofile( + violatorPath2, + [[ +local y = 10 / 0 + +local x = 0 +local y = "hello" +x = y +y = x + ]] + ) + fs.writestringtofile(path.format(path.join(modulePath, "not_luau.txt")), "blablabla") + + -- Do + -- Run the transformer on the transformee + local result = + process.run({ lutePath, "lint", "-r", "examples/lints", "-v", violatorPath1, path.format(modulePath) }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.eq(#result.stdout:split("warning[divide_by_zero]: Division by zero detected."), 3) + local expected = [[ +violator1.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator2.lua:1:11-17 ── + │ + 1 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + assert.eq(#result.stdout:split("warning[almost_swapped]: This looks like a failed attempt to swap."), 3) + expected = [[ +violator1.luau:3:1-4:6 ── + │ + 3 │ ╭ a = b + 4 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator2.lua:5:1-6:6 ── + │ + 5 │ ╭ x = y + 6 │ │ y = x + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + expected = "Skipping non%-Luau file '.+[/\\]module[/\\]not_luau%.txt'" + assert.neq(result.stdout:find(expected, 1, false), nil) + + -- Teardown + fs.remove(violatorPath1) + fs.removedirectory(modulePath, { recursive = true }) + end) + + suite:case("lute lint multiple files with errors recovers", function(assert) + -- Setup + -- Create multiple files that violate the rule + local lintee = path.format(path.join(tmpDir, "lintee.luau")) + fs.writestringtofile(lintee, "bogus") + + local violatorPath = path.format(path.join(tmpDir, "violator1.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", "examples/lints", lintee, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + local expected = [[ +lintee.luau': @std/syntax/parser.luau:12: parsing failed: +(0, 0) - (0, 5): Incomplete statement: expected assignment or a function call +]] + assert.strcontains(result.stdout, expected) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + + -- Teardown + fs.remove(lintee) + fs.remove(violatorPath) + end) + + suite:case("lute lint default rules", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + expected = [[ +violator.luau:3:1-4:6 ── + │ + 3 │ ╭ a = b + 4 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint warning suppression", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(divide_by_zero) +local x = 1 / 0 + +-- lute-lint-ignore(divide_by_zero) +if true then + local y = 10 / 0 +end + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + local expected = [[ +violator.luau:9:1-10:6 ── + │ + 9 │ ╭ a = b + 10 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(divide_by_zero) +local x = 1 / 0 + +if true then + local y = 10 / 0 +end + +local z = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + local expected = [[ +violator.luau:5:12-18 ── + │ + 5 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:8:11-16 ── + │ + 8 │ local z = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement2", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +-- lute-lint-ignore(divide_by_zero) +if true then + local y = 10 / 0 +end + +local z = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:8:11-16 ── + │ + 8 │ local z = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement3", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local function foo() + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:5:9-14 ── + │ + 5 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement4", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +if true then + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:5:9-14 ── + │ + 5 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement5", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +while true do + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:5:9-14 ── + │ + 5 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement6", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local t = {} +for _, _ in t do + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:6:9-14 ── + │ + 6 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression for multi node violation", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(almost_swapped) +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression for multi node violation", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(almost_swapped) +a = b +b = a +a = b + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 1) + + local expected = [[ +violator.luau:3:1-4:6 ── + │ + 3 │ ╭ b = a + 4 │ │ a = b + │ ╰─────^ + │ +]] + + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("nested suppressions", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(divide_by_zero) +local function foo() + local x = 1/0 + local function bar() + -- lute-lint-ignore(almost_swapped) + a = b + b = a + end +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("json output", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-j", violatorPath }) + -- Check + assert.eq(result.exitcode, 0) + + local expected = [[ +{ + "items": [ + { + "items": [ + { + "message": "Division by zero detected.", + "source": "lute lint", + "code": "divide_by_zero", + "severity": 2, + "range": { + "start": { + "character": 10, + "line": 0 + }, + "end": { + "character": 15, + "line": 0 + } + } + } + ], + "uri":]] -- URI has absolute tmpdir path, so we cut off here + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint respects gitignore", function(assert) + -- Setup + -- Create a module directory with a .gitignore that ignores one file + local modulePath = path.join(tmpDir, "module") + fs.createdirectory(modulePath) + local violatorPath1 = path.format(path.join(modulePath, "violator1.luau")) + fs.writestringtofile( + violatorPath1, + [[ +local x = 10 / 0 + ]] + ) + local violatorPath2 = path.format(path.join(modulePath, "violator2.luau")) + fs.writestringtofile( + violatorPath2, + [[ +local y = 10 / 0 + ]] + ) + fs.writestringtofile(path.format(path.join(modulePath, ".gitignore")), "violator2.luau\n") + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", path.format(modulePath) }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator1.luau:1:11-17 ── + │ + 1 │ local x = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator2.lua:1:11-17 ── + │ + 1 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strnotcontains(result.stdout, expected) + + -- Teardown + fs.removedirectory(modulePath, { recursive = true }) + end) + + suite:case("lute lint report directive", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 +-- lute-lint-ignore(divide_by_zero) +if true then + local z = 3 / 0 + -- lute-lint-report(divide_by_zero) + local y = 10 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:6:12-18 ── + │ + 6 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint global ignore directive", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-global-ignore(divide_by_zero) +local x = 1 / 0 +-- lute-lint-report(divide_by_zero) +if true then + local z = 3 / 0 + -- lute-lint-ignore(divide_by_zero) + local y = 10 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + + local expected = [[ +violator.luau:5:12-17 ── + │ + 5 │ local z = 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint string input", function(assert) + local inputString = [[ +a = b +b = a +]] + + -- Do + -- Run the linter on string input + local result = process.run({ lutePath, "lint", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + local expected = [[ + ┌── input:1:1-2:6 ── + │ + 1 │ ╭ a = b + 2 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + end) + + suite:case("lute lint string input verbose", function(assert) + local inputString = "local x = 1 / 0" + + -- Do + -- Run the linter on string input with verbose flag + local result = process.run({ lutePath, "lint", "-v", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "Using default lint rules.") + assert.strcontains(result.stdout, "Parsing input string") + assert.strcontains(result.stdout, "Parsing global lint ignores in input string") + assert.strcontains(result.stdout, "Applying lint rules") + assert.strcontains(result.stdout, "Printing violations from string input") + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + end) + + suite:case("lute lint string input with no violations", function(assert) + local inputString = "local x = 1 + 2" + + -- Do + -- Run the linter on string input + local result = process.run({ lutePath, "lint", "-v", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "No lint violations found.") + end) + + suite:case("lute lint string input json output", function(assert) + local inputString = "local x = 1 / 0" + + -- Do + -- Run the linter on string input with json flag + local result = process.run({ lutePath, "lint", "-j", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 0) + + local expected = [=[ +[ + { + "message": "Division by zero detected.", + "source": "lute lint", + "code": "divide_by_zero", + "severity": 2, + "range": { + "start": { + "character": 10, + "line": 0 + }, + "end": { + "character": 15, + "line": 0 + } + } + } +]]=] + assert.strcontains(result.stdout, expected) + end) + + suite:case("lute lint ignore (table item)", function(assert) + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local y = { + -- lute-lint-ignore(divide_by_zero) + x = 3 / 0 +} + +local z = 5 / 0 +]] + ) + + local result = process.run({ lutePath, "lint", violatorPath }) + assert.eq(result.exitcode, 1) + assert.strnotcontains(result.stdout, `Error linting file '{violatorPath}'`) + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + + fs.remove(violatorPath) + end) + + suite:case("parenthesized_conditions", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +if (3 > 0) then + print("Hello, world!") +elseif (false) then + print("This won't print.") +end + +local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + +while ("hello" == "world") do + print("Nope.") +end + +repeat + print("Still nope.") +until (5 == 5) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.parenthesized_conditions, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", + nil, + 6 + ) + + local expected = [[ +violator.luau:1:4-11 ── + │ + 1 │ if (3 > 0) then + │ ^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:3:8-15 ── + │ + 3 │ elseif (false) then + │ ^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Have to do this here because the path is a lot shorter on Linux, so there's more dashes to match the length of the line + expected = [[ +violator.luau:7:14-21 ──]] + assert.strcontains(result.stdout, expected) + + expected = [[ + │ + 7 │ local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + │ ^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:7:37-43 ──]] + assert.strcontains(result.stdout, expected) + + expected = [[ + │ + 7 │ local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:9:7-27 ── + │ + 9 │ while ("hello" == "world") do + │ ^^^^^^^^^^^^^^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:15:7-15 ── + │ + 15 │ until (5 == 5) + │ ^^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("parenthesized_conditions auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +if (3 > 0) then + print("Hello, world!") +elseif (false) then + print("This won't print.") +end + +local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + +while ("hello" == "world") do + print("Nope.") +end + +repeat + print("Still nope.") +until (5 == 5) +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ + lutePath, + "lint", + "--auto-fix", + "-r", + defaultRulesPaths.parenthesized_conditions, + violatorPath, + }) + + -- Check + assert.eq(result.exitcode, 0) + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +if 3 > 0 then + print("Hello, world!") +elseif false then + print("This won't print.") +end + +local x = if 2 < 1 then 10 elseif true then 3 else 20 + +while "hello" == "world" do + print("Nope.") +end + +repeat + print("Still nope.") +until 5 == 5 +]] + + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("report violations remaining after auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + +local x = 3 / 0 +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) -- After applying the auto-fix, there is still a violation + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +a, b = b, a + +local x = 3 / 0 +]] + assert.eq(fixedContents, expectedFixedContents) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + + assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("constant_table_comparison", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local a = { 1, 2, 3 } == t +local b = t == { 4, 5, 6 } +local c = { 1 } == t +local d = t == ({ 4 } :: { string }) +local e = t ~= { 1, 2 } + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = + process.run({ lutePath, "lint", "-r", defaultRulesPaths.constant_table_comparison, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false.", + nil, + 5 + ) + + local expected = [[ +violator.luau:1:11-27 ── + │ + 1 │ local a = { 1, 2, 3 } == t + │ ^^^^^^^^^^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:2:11-27 ── + │ + 2 │ local b = t == { 4, 5, 6 } + │ ^^^^^^^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:3:11-21 ── + │ + 3 │ local c = { 1 } == t + │ ^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:4:11-37 ── + │ + 4 │ local d = t == ({ 4 } :: { string }) + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:5:11-24 ── + │ + 5 │ local e = t ~= { 1, 2 } + │ ^^^^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("constant_table_comparison auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local a = {} == t +local b = t == {} +local c = t == ({}) +local d = t == ({} :: { string }) +local e = t ~= {} +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ + lutePath, + "lint", + "--auto-fix", + "-r", + defaultRulesPaths.constant_table_comparison, + violatorPath, + }) + + -- Check + assert.eq(result.exitcode, 0) + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +local a = next(t) == nil +local b = next(t) == nil +local c = next(t) == nil +local d = next(t) == nil +local e = next(t) ~= nil +]] + + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) +end) diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadbypath.test.luau new file mode 100644 index 000000000..cde7065d4 --- /dev/null +++ b/tests/cli/loadbypath.test.luau @@ -0,0 +1,58 @@ +local fs = require("@std/fs") +local process = require("@std/process") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local REQUIRER_CONTENTS = [[ +local args = { ... } +assert(#args == 2, "Expected one argument: path to Luau script to require") + +local luau = require("@std/luau") +local result = luau.loadbypath(args[2]) +print(result) +]] + +local REQUIREE_CONTENTS = [[return "Success"]] + +local lutePath = process.execpath() + +local tmpDirStr = system.tmpdir() +local testDir = path.join(tmpDirStr, "lute_require_test") +local testDirStr = path.format(testDir) + +test.suite("Lute CLI Run", function(suite) + suite:beforeall(function() + if not fs.exists(testDirStr) then + fs.createdirectory(testDirStr) + end + end) + + suite:case("loadbypath", function(check) + -- Setup + -- Create files + local requirerPath = path.format(path.join(testDir, "requirer.luau")) + local requirerFile = fs.open(requirerPath, "w+") + fs.write(requirerFile, REQUIRER_CONTENTS) + fs.close(requirerFile) + + local requireePath = path.format(path.join(testDir, "requiree.luau")) + local requireeFile = fs.open(requireePath, "w+") + fs.write(requireeFile, REQUIREE_CONTENTS) + fs.close(requireeFile) + + local result = process.run({ tostring(lutePath), requirerPath, requireePath }) + check.eq(result.exitcode, 0) + check.eq(result.stdout, "Success\n") + + -- Cleanup + fs.remove(requirerPath) + fs.remove(requireePath) + end) + + suite:afterall(function() + if fs.exists(testDirStr) then + fs.removedirectory(testDirStr) + end + end) +end) diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau new file mode 100644 index 000000000..b1ae8babe --- /dev/null +++ b/tests/cli/run.test.luau @@ -0,0 +1,40 @@ +local fs = require("@std/fs") +local pathlib = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local tmpdir = system.tmpdir() + +local rundir = pathlib.join(tmpdir, "lute-run") + +local lutePath = pathlib.format(process.execpath()) + +test.suite("lute-run", function(suite) + suite:beforeeach(function() + if fs.exists(rundir) then + fs.removedirectory(rundir, { recursive = true }) + end + end) + + suite:case("same-named file and directory", function(assert) + -- Setup + fs.createdirectory(rundir) + fs.createdirectory(pathlib.join(rundir, "hello")) + + local helloFilePath = pathlib.join(rundir, "hello.luau") + fs.writestringtofile(helloFilePath, "print('Hello world')") + + -- Execute + local result = process.run({ lutePath, pathlib.format(helloFilePath) }) + + assert.eq(result.exitcode, 1) + assert.neq( + result.stderr:find( + `Error while resolving filepath '.+{if system.win32 then "\\" else "/"}hello%.luau': Unable to tell whether path is a file or directory%. Is there a same%-named file or directory%?` + ), + nil + ) + assert.strnotcontains(result.stdout, "Hello world") + end) +end) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau new file mode 100644 index 000000000..1e088ef4a --- /dev/null +++ b/tests/cli/test.test.luau @@ -0,0 +1,73 @@ +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) + +local expected = [[Anonymous: + Passing +ExampleSuite: + should pass +SmokeSuite: + make_fail + make_pass +]] + +test.suite("LuteTestCommand", function(suite) + suite:case("lute test help message", function(assert) + -- Run lute test with -h flag + local result = process.run({ lutePath, "test", "-h" }) + + -- Check that it exits successfully and prints help + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "Usage:") + end) + + suite:case("lute test runs tests in discovery folder", function(assert) + -- Run lute test on tests/cli/discovery folder + local result = process.run({ lutePath, "test", "tests/cli/discovery" }) + + -- Check that it runs tests and succeeds + assert.eq(result.exitcode, 0) + end) + + suite:case("lute_test_discovery_custom_directory", function(assert) + -- Run lute test with custom directory path + local result = process.run({ lutePath, "test", "--list", "tests/cli/discovery" }) + + -- Check that it discovers test files (.test.luau and .spec.luau) + assert.eq(result.exitcode, 0) + assert.eq(expected, result.stdout) + end) + + suite:case("filter_by_suite_name", function(assert) + -- Run lute test with --suite flag + local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "tests/cli/discovery" }) + + -- Check that it runs only tests in SmokeSuite + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Total: 2", 1, true), nil) + assert.neq(result.stdout:find("Passed: 2", 1, true), nil) + end) + + suite:case("lute test filters by case name", function(assert) + -- Run lute test with --case flag + local result = process.run({ lutePath, "test", "--case", "make_pass", "tests/cli/discovery" }) + + -- Check that it runs only tests named make_pass + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Total: 1", 1, true), nil) + assert.neq(result.stdout:find("Passed: 1", 1, true), nil) + end) + + suite:case("lute test filters by suite and case name", function(assert) + -- Run lute test with both --suite and --case flags + local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "-c", "make_fail", "tests/cli/discovery" }) + + -- Check that it runs only make_fail in SmokeSuite + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Total: 1", 1, true), nil) + assert.neq(result.stdout:find("Passed: 1", 1, true), nil) + end) +end) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau new file mode 100644 index 000000000..f4d37b601 --- /dev/null +++ b/tests/cli/transform.test.luau @@ -0,0 +1,149 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@lute/process") +local system = require("@std/system") +local test = require("@std/test") + +local TRANSFORMEE_CONTENT = [[ +local x = math.sqrt(-1) +local b = x ~= x +]] + +local lutePath = process.execpath() +local tmpDir = system.tmpdir() +local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) +local outputPath = path.format(path.join(tmpDir, "transformee_output.luau")) + +test.suite("lute transform", function(suite) + suite:beforeeach(function() + -- Create a transformee file + local transformeeHandle = fs.open(transformeePath, "w+") + fs.write(transformeeHandle, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + end) + + suite:aftereach(function() + -- Remove the transformee file + fs.remove(transformeePath) + if fs.exists(outputPath) then + fs.remove(outputPath) + end + end) + + suite:case("transform visitor style", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", transformerDest, transformeePath }) + + -- Check + assert.eq(result.exitcode, 0) + + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent:find("x ~= x"), nil) + assert.strcontains(transformeeContent, "math.isnan(x)") + fs.close(transformeeHandle) + + -- Teardown + fs.remove(transformerDest) + end) + + suite:case("transform query style", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "query_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", transformerDest, transformeePath }) + + -- Check + assert.eq(result.exitcode, 0) + + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent:find("x ~= x"), nil) + assert.strcontains(transformeeContent, "math.isnan(x)") + fs.close(transformeeHandle) + + -- Teardown + fs.remove(transformerDest) + end) + + suite:case("transform output directory happy path", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "query_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Create output file + local h = fs.open(outputPath, "w+") + fs.close(h) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", "--output", outputPath, transformerDest, transformeePath }) + -- Check + assert.eq(result.exitcode, 0) + + -- Check that the original file is unchanged + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + + -- Check that the output file has been written to + h = fs.open(outputPath, "r") + local outputContent = fs.read(h) + assert.eq(outputContent:find("x ~= x"), nil) + assert.strcontains(outputContent, "math.isnan(x)") + fs.close(h) + + -- Teardown + fs.remove(transformerDest) + fs.remove(outputPath) + end) + + suite:case("transform output creates file if it doesn't exist", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "query_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Create output directory + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", "--output", outputPath, transformerDest, transformeePath }) + -- Check + assert.eq(result.exitcode, 0) + + -- Check that the original file is unchanged + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + + -- Check that the output file has been created and written to + assert.eq(fs.exists(outputPath), true) + local h = fs.open(outputPath, "r") + local outputContent = fs.read(h) + assert.eq(outputContent:find("x ~= x"), nil) + assert.strcontains(outputContent, "math.isnan(x)") + fs.close(h) + + -- Teardown + fs.remove(transformerDest) + fs.remove(outputPath) + end) +end) diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau new file mode 100644 index 000000000..6dc7c1c33 --- /dev/null +++ b/tests/lute/task.test.luau @@ -0,0 +1,25 @@ +local test = require("@std/test") +local task = require("@lute/task") + +test.suite("LuteTaskSuite", function(suite) + suite:case("simple_task_wait", function(assert) + local startTime = os.clock() + task.wait(0.5) + local endTime = os.clock() + + -- task.wait(0.5) actually waits ~0.48 seconds :) + assert.eq(math.ceil(endTime - startTime) >= 0.5, true) + end) + + suite:case("task_wait_in_loop", function(assert) + local startTime = os.clock() + local elapsed = 0 + while elapsed < 0.5 do + task.wait(0.1) + elapsed = os.clock() - startTime + end + local endTime = os.clock() + + assert.eq(endTime - startTime >= 0.5, true) + end) +end) diff --git a/tests/lute/vm.test.luau b/tests/lute/vm.test.luau new file mode 100644 index 000000000..d74be1ec9 --- /dev/null +++ b/tests/lute/vm.test.luau @@ -0,0 +1,68 @@ +local test = require("@std/test") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") + +test.suite("LuteVmSuite", function(suite) + suite:case("child_vm_has_access_to_builtins", function(assert) + local tmpdir = system.tmpdir() + local child_vm = path.join(tmpdir, "child_vm.luau") + local parent_vm = path.join(tmpdir, "parent_vm.luau") + + do + local handle = fs.open(child_vm, "w+") + assert.eq(fs.exists(child_vm), true) + fs.write( + handle, + [[ + local std_test_exists = require("@std/test") ~= nil + local std_test_consistent = require("@std/test") == require("@std/test") + local lute_vm_exists = require("@lute/vm") ~= nil + local lute_vm_consistent = require("@lute/vm") == require("@lute/vm") + + local exported = {} + + function exported.std_test_exists() + return std_test_exists + end + function exported.std_test_consistent() + return std_test_consistent + end + function exported.lute_vm_exists() + return lute_vm_exists + end + function exported.lute_vm_consistent() + return lute_vm_consistent + end + + return exported + ]] + ) + fs.close(handle) + end + + do + local handle = fs.open(parent_vm, "w+") + assert.eq(fs.exists(parent_vm), true) + fs.write( + handle, + [[ + local vm = require("@lute/vm") + local result = vm.create("./child_vm") + assert(result.std_test_exists()) + assert(result.std_test_consistent()) + assert(result.lute_vm_exists()) + assert(result.lute_vm_consistent()) + ]] + ) + fs.close(handle) + end + + local result = process.run({ path.format(process.execpath()), path.format(parent_vm) }) + assert.eq(result.exitcode, 0) + + fs.remove(child_vm) + fs.remove(parent_vm) + end) +end) diff --git a/tests/astSerializerTests/assignment-1.luau b/tests/parserExamples/assignment-1.luau similarity index 100% rename from tests/astSerializerTests/assignment-1.luau rename to tests/parserExamples/assignment-1.luau diff --git a/tests/astSerializerTests/attributes-1.luau b/tests/parserExamples/attributes-1.luau similarity index 100% rename from tests/astSerializerTests/attributes-1.luau rename to tests/parserExamples/attributes-1.luau diff --git a/tests/astSerializerTests/break-continue-1.luau b/tests/parserExamples/break-continue-1.luau similarity index 100% rename from tests/astSerializerTests/break-continue-1.luau rename to tests/parserExamples/break-continue-1.luau diff --git a/tests/astSerializerTests/compound-assignment-1.luau b/tests/parserExamples/compound-assignment-1.luau similarity index 100% rename from tests/astSerializerTests/compound-assignment-1.luau rename to tests/parserExamples/compound-assignment-1.luau diff --git a/tests/astSerializerTests/compound-types-1.luau b/tests/parserExamples/compound-types-1.luau similarity index 100% rename from tests/astSerializerTests/compound-types-1.luau rename to tests/parserExamples/compound-types-1.luau diff --git a/tests/astSerializerTests/compound-types-2.luau b/tests/parserExamples/compound-types-2.luau similarity index 100% rename from tests/astSerializerTests/compound-types-2.luau rename to tests/parserExamples/compound-types-2.luau diff --git a/tests/astSerializerTests/compound-types-3.luau b/tests/parserExamples/compound-types-3.luau similarity index 100% rename from tests/astSerializerTests/compound-types-3.luau rename to tests/parserExamples/compound-types-3.luau diff --git a/tests/astSerializerTests/function-declaration-1.luau b/tests/parserExamples/function-declaration-1.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-1.luau rename to tests/parserExamples/function-declaration-1.luau diff --git a/tests/astSerializerTests/function-declaration-2.luau b/tests/parserExamples/function-declaration-2.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-2.luau rename to tests/parserExamples/function-declaration-2.luau diff --git a/tests/astSerializerTests/function-declaration-3.luau b/tests/parserExamples/function-declaration-3.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-3.luau rename to tests/parserExamples/function-declaration-3.luau diff --git a/tests/astSerializerTests/function-declaration-4.luau b/tests/parserExamples/function-declaration-4.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-4.luau rename to tests/parserExamples/function-declaration-4.luau diff --git a/tests/astSerializerTests/function-type-1.luau b/tests/parserExamples/function-type-1.luau similarity index 100% rename from tests/astSerializerTests/function-type-1.luau rename to tests/parserExamples/function-type-1.luau diff --git a/tests/astSerializerTests/function-type-2.luau b/tests/parserExamples/function-type-2.luau similarity index 100% rename from tests/astSerializerTests/function-type-2.luau rename to tests/parserExamples/function-type-2.luau diff --git a/tests/astSerializerTests/function-type-3.luau b/tests/parserExamples/function-type-3.luau similarity index 100% rename from tests/astSerializerTests/function-type-3.luau rename to tests/parserExamples/function-type-3.luau diff --git a/tests/astSerializerTests/generic-for-loop-1.luau b/tests/parserExamples/generic-for-loop-1.luau similarity index 100% rename from tests/astSerializerTests/generic-for-loop-1.luau rename to tests/parserExamples/generic-for-loop-1.luau diff --git a/tests/astSerializerTests/if-expression-1.luau b/tests/parserExamples/if-expression-1.luau similarity index 100% rename from tests/astSerializerTests/if-expression-1.luau rename to tests/parserExamples/if-expression-1.luau diff --git a/tests/astSerializerTests/if-expression-2.luau b/tests/parserExamples/if-expression-2.luau similarity index 100% rename from tests/astSerializerTests/if-expression-2.luau rename to tests/parserExamples/if-expression-2.luau diff --git a/tests/astSerializerTests/if-statement-1.luau b/tests/parserExamples/if-statement-1.luau similarity index 100% rename from tests/astSerializerTests/if-statement-1.luau rename to tests/parserExamples/if-statement-1.luau diff --git a/tests/astSerializerTests/interpolated-string-1.luau b/tests/parserExamples/interpolated-string-1.luau similarity index 100% rename from tests/astSerializerTests/interpolated-string-1.luau rename to tests/parserExamples/interpolated-string-1.luau diff --git a/tests/astSerializerTests/interpolated-string-2.luau b/tests/parserExamples/interpolated-string-2.luau similarity index 100% rename from tests/astSerializerTests/interpolated-string-2.luau rename to tests/parserExamples/interpolated-string-2.luau diff --git a/tests/astSerializerTests/local-assignment-1.luau b/tests/parserExamples/local-assignment-1.luau similarity index 100% rename from tests/astSerializerTests/local-assignment-1.luau rename to tests/parserExamples/local-assignment-1.luau diff --git a/tests/astSerializerTests/local-function-declaration-1.luau b/tests/parserExamples/local-function-declaration-1.luau similarity index 100% rename from tests/astSerializerTests/local-function-declaration-1.luau rename to tests/parserExamples/local-function-declaration-1.luau diff --git a/tests/astSerializerTests/numeric-for-loop-1.luau b/tests/parserExamples/numeric-for-loop-1.luau similarity index 100% rename from tests/astSerializerTests/numeric-for-loop-1.luau rename to tests/parserExamples/numeric-for-loop-1.luau diff --git a/tests/astSerializerTests/repeat-until-1.luau b/tests/parserExamples/repeat-until-1.luau similarity index 100% rename from tests/astSerializerTests/repeat-until-1.luau rename to tests/parserExamples/repeat-until-1.luau diff --git a/tests/astSerializerTests/table-1.luau b/tests/parserExamples/table-1.luau similarity index 100% rename from tests/astSerializerTests/table-1.luau rename to tests/parserExamples/table-1.luau diff --git a/tests/astSerializerTests/table-2.luau b/tests/parserExamples/table-2.luau similarity index 100% rename from tests/astSerializerTests/table-2.luau rename to tests/parserExamples/table-2.luau diff --git a/tests/astSerializerTests/type-alias-1.luau b/tests/parserExamples/type-alias-1.luau similarity index 100% rename from tests/astSerializerTests/type-alias-1.luau rename to tests/parserExamples/type-alias-1.luau diff --git a/tests/astSerializerTests/type-alias-2.luau b/tests/parserExamples/type-alias-2.luau similarity index 100% rename from tests/astSerializerTests/type-alias-2.luau rename to tests/parserExamples/type-alias-2.luau diff --git a/tests/astSerializerTests/type-assertion-1.luau b/tests/parserExamples/type-assertion-1.luau similarity index 100% rename from tests/astSerializerTests/type-assertion-1.luau rename to tests/parserExamples/type-assertion-1.luau diff --git a/tests/astSerializerTests/type-function-1.luau b/tests/parserExamples/type-function-1.luau similarity index 100% rename from tests/astSerializerTests/type-function-1.luau rename to tests/parserExamples/type-function-1.luau diff --git a/tests/astSerializerTests/type-singletons-1.luau b/tests/parserExamples/type-singletons-1.luau similarity index 100% rename from tests/astSerializerTests/type-singletons-1.luau rename to tests/parserExamples/type-singletons-1.luau diff --git a/tests/astSerializerTests/type-tables-1.luau b/tests/parserExamples/type-tables-1.luau similarity index 100% rename from tests/astSerializerTests/type-tables-1.luau rename to tests/parserExamples/type-tables-1.luau diff --git a/tests/astSerializerTests/type-tables-2.luau b/tests/parserExamples/type-tables-2.luau similarity index 100% rename from tests/astSerializerTests/type-tables-2.luau rename to tests/parserExamples/type-tables-2.luau diff --git a/tests/astSerializerTests/while-1.luau b/tests/parserExamples/while-1.luau similarity index 100% rename from tests/astSerializerTests/while-1.luau rename to tests/parserExamples/while-1.luau diff --git a/tests/runtime/crypto.test.luau b/tests/runtime/crypto.test.luau new file mode 100644 index 000000000..2ce2d8ba7 --- /dev/null +++ b/tests/runtime/crypto.test.luau @@ -0,0 +1,123 @@ +local test = require("@std/test") + +local crypto = require("@lute/crypto") + +test.suite("CryptoRuntimeTests", function(suite) + suite:case("secretbox_is_a_roundtrip_strings", function(assert) + local message = "Remember, remember the Fifth of November." + local box = crypto.secretbox.seal(message) + + local opened = crypto.secretbox.open(box) + local output = buffer.readstring(opened, 0, buffer.len(opened)) + + assert.eq(output, message) + end) + + suite:case("secretbox_is_a_roundtrip_buffers", function(assert) + local message = "Gunpowder, treason, and plot." + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + local opened = crypto.secretbox.open(box) + + assert.buffereq(opened, buf) + end) + + suite:case("secretbox_separate_keygen", function(assert) + local key = crypto.secretbox.keygen() + + local message = "Remember, remember the Fifth of November." + local box = crypto.secretbox.seal(message, key) + + local opened = crypto.secretbox.open(box) + local output = buffer.readstring(opened, 0, buffer.len(opened)) + + assert.eq(output, message) + end) + + suite:case("secretbox_errors_on_tampering", function(assert) + local message = "Gunpowder, treason, and plot." + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + buffer.writeu32(box.ciphertext, 2, 10_11_2025) + + assert.errors(function() + crypto.secretbox.open(box) + end) + end) + + suite:case("secretbox_no_open_string", function(assert) + local message = "I see no reason why gunpowder treason should ever be forgot." + + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + local other = { + ciphertext = buffer.readstring(box.ciphertext, 0, #message), + nonce = box.nonce, + key = box.key, + } + + assert.errors(function() + crypto.secretbox.open(other) + end) + end) + + suite:case("secretbox_missized_key", function(assert) + local message = "I see no reason why gunpowder treason should ever be forgot." + + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + local other = { + ciphertext = box.ciphertext, + nonce = box.nonce, + key = buffer.create(5), + } + + assert.errors(function() + crypto.secretbox.open(other) + end) + end) + + suite:case("secretbox_on_ill_typed_inputs", function(assert) + local message = "I see no reason why gunpowder treason should ever be forgot." + + assert.errors(function() + crypto.secretbox.seal({ message } :: any) + end) + + assert.errors(function() + crypto.secretbox.seal({ message, message, message } :: any) + end) + + assert.errors(function() + crypto.secretbox.seal({ ciphertext = message, key = message, nonce = message } :: any) + end) + + assert.errors(function() + crypto.secretbox.open(message :: any) + end) + + assert.errors(function() + crypto.secretbox.open({ ciphertext = message, key = buffer.create(), nonce = "woof" } :: any) + end) + + assert.errors(function() + crypto.secretbox.open({ ciphertext = message, key = "woof", nonce = buffer.create() } :: any) + end) + + assert.errors(function() + crypto.secretbox.open({ ciphertext = message } :: any) + end) + + assert.errors(function() + crypto.secretbox.open(nil :: any) + end) + end) +end) diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp new file mode 100644 index 000000000..70b21caf2 --- /dev/null +++ b/tests/src/cliruntimefixture.cpp @@ -0,0 +1,39 @@ +#include "cliruntimefixture.h" + +#include "lute/climain.h" +#include "lute/requiresetup.h" + +#include "Luau/Compiler.h" + +#include "lua.h" +#include "lualib.h" + +static int report(lua_State* L) +{ + const char* str = luaL_tolstring(L, 1, nullptr); + lua_getfield(L, LUA_REGISTRYINDEX, "reporter"); + TestReporter* reporter = static_cast(lua_touserdata(L, -1)); + reporter->reportOutput(str); + return 0; +} + +CliRuntimeFixture::CliRuntimeFixture() + : runtime(std::make_unique()) +{ + L = setupCliState( + *runtime, + [rep = reporter.get()](lua_State* L) + { + lua_pushlightuserdata(L, (void*)rep); + lua_setfield(L, LUA_REGISTRYINDEX, "reporter"); + lua_pushcfunction(L, report, ""); + lua_setglobal(L, "report"); + } + ); +} + +bool CliRuntimeFixture::runCode(const std::string& source) +{ + std::string bytecode = Luau::compile(source, Luau::CompileOptions()); + return runBytecode(*runtime, bytecode, "=stdin", L, 0, nullptr, getReporter()); +} diff --git a/tests/src/cliruntimefixture.h b/tests/src/cliruntimefixture.h new file mode 100644 index 000000000..868ca3479 --- /dev/null +++ b/tests/src/cliruntimefixture.h @@ -0,0 +1,23 @@ +#pragma once + +#include "lute/runtime.h" + +#include "lutefixture.h" + +#include "lua.h" + +#include +#include + +class CliRuntimeFixture : public LuteFixture +{ +public: + CliRuntimeFixture(); + + bool runCode(const std::string& source); + + lua_State* L; + +private: + std::unique_ptr runtime; +}; diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp new file mode 100644 index 000000000..e26f15682 --- /dev/null +++ b/tests/src/compile.test.cpp @@ -0,0 +1,540 @@ +#include "lute/compile.h" + +#include "lute/climain.h" + +#include "Luau/FileUtils.h" + +#include +#include +#include +#include +#include + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_single_file_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create payload and add file + LuteExePayload originalPayload{getReporter()}; + originalPayload.add(testFilePath, testFilePath); + + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + + // Encode + auto encodeResult = originalPayload.encode(); + REQUIRE(encodeResult.has_value()); + REQUIRE(encodeResult->bytesWritten > 0); + REQUIRE(encodeResult->compressedPayloadSizeBytes > 0); + REQUIRE(encodeResult->uncompressedPayloadSizeBytes > 0); + REQUIRE(!encodeResult->payload.empty()); + + // Verify compression is working (compressed should be smaller than uncompressed) + CHECK(encodeResult->compressedPayloadSizeBytes <= encodeResult->uncompressedPayloadSizeBytes); + + // Decode + auto decodeResult = LuteExePayload::decode(encodeResult->payload, getReporter()); + REQUIRE(decodeResult.has_value()); + + // Verify metrics match + CHECK(decodeResult->bytesRead == encodeResult->bytesWritten); + CHECK(decodeResult->compressedPayloadSizeBytes == encodeResult->compressedPayloadSizeBytes); + CHECK(decodeResult->uncompressedPayloadSizeBytes == encodeResult->uncompressedPayloadSizeBytes); + + // Verify entry point + CHECK(decodeResult->payload.entryPointPath == testFilePath); + CHECK(decodeResult->payload.entryPointPath == originalPayload.entryPointPath); + + // Verify bytecode map has one entry + REQUIRE(decodeResult->payload.filePathToBytecode.size() == 1); + + // Verify bytecode matches + auto it = decodeResult->payload.filePathToBytecode.find(testFilePath); + REQUIRE(it != nullptr); + auto originalIt = originalPayload.filePathToBytecode.find(testFilePath); + REQUIRE(originalIt != nullptr); + CHECK(*it == *originalIt); + + // Verify luaurc config was preserved + REQUIRE(decodeResult->payload.luauConfigFiles.size() == 1); + auto configIt = decodeResult->payload.luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_multiple_files_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + std::vector testFiles = { + joinPaths(testDir, "main.luau"), joinPaths(testDir, "utils.luau"), joinPaths(testDir, "lib/helper.luau"), joinPaths(testDir, "shared.luau") + }; + + // Create payload with multiple files + LuteExePayload originalPayload{getReporter()}; + for (const auto& file : testFiles) + { + originalPayload.add(file, file); + } + + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + + // First file should be the entry point + CHECK(originalPayload.entryPointPath == testFiles[0]); + + auto encodeResult = originalPayload.encode(); + REQUIRE(encodeResult.has_value()); + REQUIRE(!encodeResult->payload.empty()); + + auto decodeResult = LuteExePayload::decode(encodeResult->payload, getReporter()); + REQUIRE(decodeResult.has_value()); + + // Verify entry point + CHECK(decodeResult->payload.entryPointPath == testFiles[0]); + + // Verify all files are present + REQUIRE(decodeResult->payload.filePathToBytecode.size() == testFiles.size()); + + for (const auto& file : testFiles) + { + auto decodedIt = decodeResult->payload.filePathToBytecode.find(file); + REQUIRE(decodedIt != nullptr); + + auto originalIt = originalPayload.filePathToBytecode.find(file); + REQUIRE(originalIt != nullptr); + + // Verify bytecode matches + CHECK(*decodedIt == *originalIt); + } + + // Verify luaurc config was preserved + REQUIRE(decodeResult->payload.luauConfigFiles.size() == 1); + auto configIt = decodeResult->payload.luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_invalid_magic_flag") +{ + // Create a payload with invalid magic flag + std::string invalidPayload = "INVALID_"; + invalidPayload.append(100, 'X'); // Add some data + + auto decodeResult = LuteExePayload::decode(invalidPayload, getReporter()); + CHECK(!decodeResult.has_value()); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_too_small_payload") +{ + // Payload smaller than minimum size + std::string tinyPayload = "TINY"; + + auto decodeResult = LuteExePayload::decode(tinyPayload, getReporter()); + CHECK(!decodeResult.has_value()); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_corrupted_metadata") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create valid payload + LuteExePayload originalPayload{getReporter()}; + originalPayload.add(testFilePath, testFilePath); + auto encodeResult = originalPayload.encode(); + REQUIRE(encodeResult.has_value()); + + // Corrupt the payload by modifying bytes near the end (metadata area) + std::string corruptedPayload = encodeResult->payload; + size_t corruptPos = corruptedPayload.size() - 20; + if (corruptPos < corruptedPayload.size()) + { + corruptedPayload[corruptPos] = ~corruptedPayload[corruptPos]; + corruptedPayload[corruptPos + 1] = ~corruptedPayload[corruptPos + 1]; + } + + // Attempt to decode corrupted payload + auto decodeResult = LuteExePayload::decode(corruptedPayload, getReporter()); + // Should either fail or produce different results + if (decodeResult.has_value()) + { + // If it doesn't fail outright, the data should be corrupted + CHECK(decodeResult->payload.entryPointPath != originalPayload.entryPointPath); + } +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_entry_point_is_first_added") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + std::string firstFile = joinPaths(testDir, "main.luau"); + std::string secondFile = joinPaths(testDir, "utils.luau"); + + LuteExePayload payload{getReporter()}; + payload.add(firstFile, firstFile); + payload.add(secondFile, secondFile); + + // Entry point should be the first file added + CHECK(payload.entryPointPath == firstFile); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_nonexistent_file") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string nonExistentFile = joinPaths(luteProjectRoot, "tests/src/this_file_does_not_exist.luau"); + + LuteExePayload payload{getReporter()}; + payload.add(nonExistentFile, nonExistentFile); + + // Encoding should fail because file doesn't exist + auto encodeResult = payload.encode(); + CHECK(!encodeResult.has_value()); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_empty_payload") +{ + // Create payload without adding any files + LuteExePayload emptyPayload{getReporter()}; + CHECK(emptyPayload.entryPointPath.empty()); + + // Encoding an empty payload should fail + auto encodeResult = emptyPayload.encode(); + CHECK(!encodeResult.has_value()); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_compression_effectiveness") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + LuteExePayload payload{getReporter()}; + payload.add(testFilePath, testFilePath); + + auto encodeResult = payload.encode(); + REQUIRE(encodeResult.has_value()); + + // Verify that compression is actually reducing size + // compressed should be smaller than uncompressed (barring any weird edge cases) + CHECK(encodeResult->compressedPayloadSizeBytes <= encodeResult->uncompressedPayloadSizeBytes); + + // Verify the total payload includes overhead for metadata + // Total = compressed data + metadata (sizes, counts, paths, magic flag) + CHECK(encodeResult->bytesWritten >= encodeResult->compressedPayloadSizeBytes); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_bytecode_integrity") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create two separate payloads with the same file + LuteExePayload payload1{getReporter()}; + payload1.add(testFilePath, testFilePath); + auto encode1 = payload1.encode(); + REQUIRE(encode1.has_value()); + + LuteExePayload payload2{getReporter()}; + payload2.add(testFilePath, testFilePath); + auto encode2 = payload2.encode(); + REQUIRE(encode2.has_value()); + + // The bytecode for the same file should be identical + auto it1 = payload1.filePathToBytecode.find(testFilePath); + auto it2 = payload2.filePathToBytecode.find(testFilePath); + REQUIRE(it1 != nullptr); + REQUIRE(it2 != nullptr); + CHECK(*it1 == *it2); + + // The encoded payloads should be identical (deterministic encoding) + CHECK(encode1->payload == encode2->payload); +} + +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_validates_numfiles_metadata") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create and encode a valid payload + LuteExePayload payload{getReporter()}; + payload.add(testFilePath, testFilePath); + auto encodeResult = payload.encode(); + REQUIRE(encodeResult.has_value()); + + // Corrupt the numFiles field (located before entry point path) + // Format: [...][compressed_size][uncompressed_size][num_files][entry_point_path][entry_point_path_length][LUTEBYTE] + std::string corruptedPayload = encodeResult->payload; + + // Find the numFiles field: 8 bytes (magic) + 4 bytes (entry path len) + entry path len + 4 bytes (num_files) + size_t magicSize = 8; // "LUTEBYTE" + + // Read entry point path length from the correct position + size_t entryPathLenPos = corruptedPayload.size() - magicSize - sizeof(uint32_t); + uint32_t entryPathLen; + memcpy(&entryPathLen, corruptedPayload.data() + entryPathLenPos, sizeof(uint32_t)); + + // numFiles is right before the entry point path + size_t numFilesPos = corruptedPayload.size() - magicSize - sizeof(uint32_t) - entryPathLen - sizeof(uint32_t); + + // Change numFiles from 1 to 99 + uint32_t fakeNumFiles = 99; + memcpy(corruptedPayload.data() + numFilesPos, &fakeNumFiles, sizeof(uint32_t)); + + // Decoding should fail due to numFiles mismatch + auto decodeResult = LuteExePayload::decode(corruptedPayload, getReporter()); + CHECK(!decodeResult.has_value()); +} + +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_single_file_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create a temporary dummy executable as the base runtime + std::string dummyExePath = joinPaths(luteProjectRoot, "tests/temp_dummy_exe"); + { + std::ofstream dummyExe(dummyExePath, std::ios::binary); + REQUIRE(dummyExe.is_open()); + // Write some dummy data to simulate an executable + std::string dummyContent = "FAKE_EXECUTABLE_HEADER_DATA"; + dummyExe.write(dummyContent.data(), dummyContent.size()); + dummyExe.close(); + } + + // Create payload with a test file + LuteExePayload originalPayload{getReporter()}; + originalPayload.add(testFilePath, testFilePath); + + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + + // Create LuteExecutable and write it out + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe"); + LuteExecutable executable{dummyExePath, getReporter()}; + + bool createSuccess = executable.create(outputExePath, originalPayload); + REQUIRE(createSuccess); + + // Verify output file was created + std::ifstream checkFile(outputExePath, std::ios::binary); + REQUIRE(checkFile.is_open()); + checkFile.close(); + + // Extract the payload from the created executable + LuteExecutable readExecutable{outputExePath, getReporter()}; + auto extractedPayload = readExecutable.extract(); + REQUIRE(extractedPayload.has_value()); + + // Verify entry point matches + CHECK(extractedPayload->entryPointPath == originalPayload.entryPointPath); + + // Verify the file count matches + REQUIRE(extractedPayload->filePathToBytecode.size() == 1); + + // Verify bytecode matches + auto originalBytecodeIt = originalPayload.filePathToBytecode.find(testFilePath); + auto extractedBytecodeIt = extractedPayload->filePathToBytecode.find(testFilePath); + REQUIRE(originalBytecodeIt != nullptr); + REQUIRE(extractedBytecodeIt != nullptr); + CHECK(*originalBytecodeIt == *extractedBytecodeIt); + + // Verify luaurc config was preserved + REQUIRE(extractedPayload->luauConfigFiles.size() == 1); + auto configIt = extractedPayload->luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); + + // Clean up temporary files + std::remove(dummyExePath.c_str()); + std::remove(outputExePath.c_str()); +} + +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_multiple_files_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + std::vector testFiles = { + joinPaths(testDir, "main.luau"), joinPaths(testDir, "utils.luau"), joinPaths(testDir, "lib/helper.luau"), joinPaths(testDir, "shared.luau") + }; + + // Create a temporary dummy executable + std::string dummyExePath = joinPaths(luteProjectRoot, "tests/temp_dummy_exe_multi"); + { + std::ofstream dummyExe(dummyExePath, std::ios::binary); + REQUIRE(dummyExe.is_open()); + std::string dummyContent = "FAKE_EXECUTABLE_WITH_LONGER_HEADER_TO_TEST_MULTI_FILE"; + dummyExe.write(dummyContent.data(), dummyContent.size()); + dummyExe.close(); + } + + // Create payload with multiple files + LuteExePayload originalPayload{getReporter()}; + for (const auto& file : testFiles) + { + originalPayload.add(file, file); + } + + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + + // First file should be the entry point + CHECK(originalPayload.entryPointPath == testFiles[0]); + + // Create the executable + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_multi"); + LuteExecutable executable{dummyExePath, getReporter()}; + + bool createSuccess = executable.create(outputExePath, originalPayload); + REQUIRE(createSuccess); + + // Extract the payload + LuteExecutable readExecutable{outputExePath, getReporter()}; + auto extractedPayload = readExecutable.extract(); + REQUIRE(extractedPayload.has_value()); + + // Verify entry point + CHECK(extractedPayload->entryPointPath == testFiles[0]); + + // Verify all files are present + REQUIRE(extractedPayload->filePathToBytecode.size() == testFiles.size()); + + // Verify each file's bytecode matches + for (const auto& file : testFiles) + { + auto extractedIt = extractedPayload->filePathToBytecode.find(file); + REQUIRE(extractedIt != nullptr); + + auto originalIt = originalPayload.filePathToBytecode.find(file); + REQUIRE(originalIt != nullptr); + + CHECK(*extractedIt == *originalIt); + } + + // Verify luaurc config was preserved + REQUIRE(extractedPayload->luauConfigFiles.size() == 1); + auto configIt = extractedPayload->luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); + + // Clean up + std::remove(dummyExePath.c_str()); + std::remove(outputExePath.c_str()); +} + +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_extract_from_plain_executable") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + // Create a plain executable without any embedded payload + std::string plainExePath = joinPaths(luteProjectRoot, "tests/temp_plain_exe"); + { + std::ofstream plainExe(plainExePath, std::ios::binary); + REQUIRE(plainExe.is_open()); + std::string content = "PLAIN_EXECUTABLE_NO_PAYLOAD"; + plainExe.write(content.data(), content.size()); + plainExe.close(); + } + + // Attempt to extract - should return nullopt since there's no payload + LuteExecutable executable{plainExePath, getReporter()}; + auto extractedPayload = executable.extract(); + CHECK(!extractedPayload.has_value()); + + // Clean up + std::remove(plainExePath.c_str()); +} + +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_extract_preserves_original_executable") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create dummy executable with specific content + std::string dummyExePath = joinPaths(luteProjectRoot, "tests/temp_dummy_exe_preserve"); + std::string originalExeContent = "ORIGINAL_EXE_CONTENT_12345"; + { + std::ofstream dummyExe(dummyExePath, std::ios::binary); + REQUIRE(dummyExe.is_open()); + dummyExe.write(originalExeContent.data(), originalExeContent.size()); + dummyExe.close(); + } + + // Create payload and executable + LuteExePayload payload{getReporter()}; + payload.add(testFilePath, testFilePath); + + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_preserve"); + LuteExecutable executable{dummyExePath, getReporter()}; + bool createSuccess = executable.create(outputExePath, payload); + REQUIRE(createSuccess); + + // Read the output file and verify the original executable content is at the beginning + std::ifstream outputFile(outputExePath, std::ios::binary); + REQUIRE(outputFile.is_open()); + + std::vector buffer(originalExeContent.size()); + outputFile.read(buffer.data(), buffer.size()); + outputFile.close(); + + std::string extractedPrefix(buffer.begin(), buffer.end()); + CHECK(extractedPrefix == originalExeContent); + + // Clean up + std::remove(dummyExePath.c_str()); + std::remove(outputExePath.c_str()); +} + +TEST_CASE_FIXTURE(LuteFixture, "compile_command_e2e") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + // Use a simple test file that prints something we can verify + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create a temporary output path for the compiled executable + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_compiled_e2e"); +#ifdef _WIN32 + outputExePath += ".exe"; +#endif + + // Build argv for cliMain: ["lute", "compile", , "--output", ] + char executablePlaceholder[] = "lute"; + char compileCommand[] = "compile"; + char outputFlag[] = "--output"; + + std::vector argv = {executablePlaceholder, compileCommand, testFilePath.data(), outputFlag, outputExePath.data()}; + + // Run the compile command + int compileResult = cliMain(argv.size(), argv.data(), getReporter()); + REQUIRE(compileResult == 0); + + // Verify the output file was created + std::ifstream checkFile(outputExePath, std::ios::binary); + REQUIRE(checkFile.is_open()); + checkFile.close(); + + // Now run the compiled executable to verify it works + std::vector runArgv = {outputExePath.data()}; + int runResult = cliMain(runArgv.size(), runArgv.data(), getReporter()); + CHECK(runResult == 0); + + // Clean up + std::remove(outputExePath.c_str()); +} diff --git a/tests/src/configresolver.test.cpp b/tests/src/configresolver.test.cpp new file mode 100644 index 000000000..691a4ed88 --- /dev/null +++ b/tests/src/configresolver.test.cpp @@ -0,0 +1,31 @@ +// Tests for configuration resolver inheritance and ambiguity. +#include "lute/configresolver.h" + +#include "Luau/FileUtils.h" + +#include "doctest.h" +#include "luteprojectroot.h" + +TEST_CASE("configresolver") +{ + std::string root = getLuteProjectRootAbsolute(); + std::string baseDir = joinPaths(root, "tests/src/resolver"); + std::string file = joinPaths(baseDir, "mainmodule.luau"); + + // There is a .luaurc in tests/src/resolver; verify resolver reads it without crashing + Luau::LuteConfigResolver configResolver(Luau::Mode::Nonstrict); + const Luau::Config& cfg = configResolver.getConfig(file, {}); + + // check that mode was set to strict per .luaurc + CHECK(cfg.mode == Luau::Mode::Strict); + + // check the alias root was set + const Luau::Config::AliasInfo* aliasInfo = cfg.aliases.find("root"); + CHECK(aliasInfo != nullptr); + CHECK(aliasInfo->value == "./"); + + // run readConfigRec on parent directory to verify inheritance + const Luau::Config& parentCfg = configResolver.readConfigRec(baseDir); + // mode should be the same as child since no override + CHECK(parentCfg.mode == Luau::Mode::Strict); +} diff --git a/tests/src/lutefixture.cpp b/tests/src/lutefixture.cpp new file mode 100644 index 000000000..3bc95b9dc --- /dev/null +++ b/tests/src/lutefixture.cpp @@ -0,0 +1,11 @@ +#include "lutefixture.h" + +LuteFixture::LuteFixture() + : reporter(std::make_unique()) +{ +} + +TestReporter& LuteFixture::getReporter() +{ + return *reporter; +} diff --git a/tests/src/lutefixture.h b/tests/src/lutefixture.h new file mode 100644 index 000000000..ec7adedad --- /dev/null +++ b/tests/src/lutefixture.h @@ -0,0 +1,17 @@ +#pragma once + +#include "testreporter.h" + +#include + +// Base fixture class for all Lute tests that need a reporter +class LuteFixture +{ +public: + LuteFixture(); + virtual ~LuteFixture() = default; + + TestReporter& getReporter(); + + std::unique_ptr reporter; +}; diff --git a/tests/src/lutefs.test.cpp b/tests/src/lutefs.test.cpp new file mode 100644 index 000000000..46bf3f499 --- /dev/null +++ b/tests/src/lutefs.test.cpp @@ -0,0 +1,57 @@ +#include "lute/fileutils.h" + +#include "Luau/FileUtils.h" + +#include + +#include "cliruntimefixture.h" +#include "doctest.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(CliRuntimeFixture, "fs_open_write_read_close") +{ + std::string testFile = "./tmp/lute_test_file.txt"; + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string localTmpDir = joinPaths(luteProjectRoot, "./tmp"); + + // Clean up any existing file + Lute::removeFile(testFile); + Lute::createDirectories(localTmpDir); + runCode( + R"( + local fs = require("@lute/fs") + local path = ")" + + testFile + R"(" + + -- Open file for writing + local h = fs.open(path, "w+") + + -- Write some data + fs.write(h, "Hello, World!") + + -- Close the file + fs.close(h) + + -- Open file for reading + local hr = fs.open(path, "r") + assert(hr ~= nil, "File handle for reading should not be nil") + + -- Read the data + local content = fs.read(hr) + local correctness = content == "Hello, World!" + + -- Close the read handle + fs.close(hr) + + report(content) + report(correctness) + )" + ); + + CHECK(getReporter().getOutputs()[0] == "Hello, World!"); + CHECK(getReporter().getOutputs()[1] == "true"); + + // Clean up + Lute::removeFile(testFile); + Lute::removeDirectory(localTmpDir); +} diff --git a/tests/src/main.cpp b/tests/src/main.cpp index a3f832e49..b30655e5d 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -1,2 +1,19 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#define DOCTEST_CONFIG_IMPLEMENT +#include "lute/uvstate.h" + #include "doctest.h" + +int main(int argc, char** argv) +{ + UvGlobalState uvState(argc, argv); + + doctest::Context context; + context.applyCommandLine(argc, argv); + + int res = context.run(); + + if (context.shouldExit()) + return res; + + return res; +} diff --git a/tests/src/modulepath.test.cpp b/tests/src/modulepath.test.cpp index 84f63f373..df61f9e61 100644 --- a/tests/src/modulepath.test.cpp +++ b/tests/src/modulepath.test.cpp @@ -1,10 +1,10 @@ -#include "doctest.h" -#include "luteprojectroot.h" - #include "lute/modulepath.h" #include "Luau/FileUtils.h" +#include "doctest.h" +#include "luteprojectroot.h" + TEST_CASE("module_path") { for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp new file mode 100644 index 000000000..ea3f991be --- /dev/null +++ b/tests/src/moduleresolver.test.cpp @@ -0,0 +1,25 @@ +// Simplified tests for module resolver functionality using public APIs. +#include "lute/moduleresolver.h" + +#include "lute/resolverequire.h" + +#include "Luau/Ast.h" +#include "Luau/FileUtils.h" + +#include + +#include "doctest.h" +#include "luteprojectroot.h" + +TEST_CASE("moduleresolver_read_source") +{ + std::string root = getLuteProjectRootAbsolute(); + std::string file = joinPaths(root, "tests/src/resolver/mainmodule.luau"); + Luau::LuteModuleResolver resolver; + auto source = resolver.readSource(file); + REQUIRE(source); + CHECK(source->type == Luau::SourceCode::Module); + CHECK(source->source.find("require") != std::string::npos); +} + +// TODO: add tests for resolveModule diff --git a/tests/src/packagerequire.test.cpp b/tests/src/packagerequire.test.cpp new file mode 100644 index 000000000..22e6f7d3b --- /dev/null +++ b/tests/src/packagerequire.test.cpp @@ -0,0 +1,102 @@ +#include "lute/climain.h" +#include "lute/options.h" +#include "lute/packagerequirevfs.h" +#include "lute/require.h" +#include "lute/runtime.h" +#include "lute/userlandvfs.h" + +#include "Luau/Compiler.h" +#include "Luau/FileUtils.h" +#include "Luau/Require.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") +{ + Runtime runtime; + + lua_State* L = setupState( + runtime, + [](lua_State* L) + { + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string entryRoot = luteProjectRoot + '/' + "tests/src/packages/package_aware_require/packageentry"; + + std::vector directDependencies = { + {"dep", "2.0.0"}, + }; + + std::string packagesRoot = luteProjectRoot + '/' + "tests/src/packages/package_aware_require"; + std::vector> allDependencies; + allDependencies.push_back( + {{"dep", "1.0.0"}, + { + packagesRoot + '/' + "internaldep", + packagesRoot + '/' + "internaldep/init.luau", + {}, + }} + ); + allDependencies.push_back( + {{"dep", "2.0.0"}, + { + packagesRoot + '/' + "dep", + packagesRoot + '/' + "dep/module.luau", + {{"dep", "1.0.0"}}, + }} + ); + + Package::UserlandVfs userlandVfs = Package::UserlandVfs::create(std::move(directDependencies), std::move(allDependencies)); + + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + static_cast(ptr)->~RequireCtx(); + } + ); + + if (!ctx) + luaL_errorL(L, "unable to allocate RequireCtx"); + + ctx = new (ctx) RequireCtx{std::make_unique(std::move(userlandVfs))}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + luaopen_require(L, requireConfigInit, ctx); + } + ); + + std::string path = getLuteProjectRootAbsolute() + "/tests/src/packages/package_aware_require/packageentry/entry.luau"; + std::optional contents = readFile(path); + REQUIRE(contents); + + std::string bytecode = Luau::compile(*contents, copts()); + bool success = runBytecode(runtime, bytecode, "@" + path, L, 0, nullptr, getReporter()); + CHECK(success); +} + +TEST_CASE_FIXTURE(LuteFixture, "pkgrun_with_lockfile") +{ + std::string entry = getLuteProjectRootAbsolute() + "/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau"; + + char executablePlaceholder[] = "lute"; + char subcommand[] = "pkgrun"; + std::vector argv = {executablePlaceholder, subcommand, entry.data()}; + + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + auto rep = getReporter(); +} diff --git a/tests/src/packages/package_aware_require/dep/module.luau b/tests/src/packages/package_aware_require/dep/module.luau new file mode 100644 index 000000000..a18e7b423 --- /dev/null +++ b/tests/src/packages/package_aware_require/dep/module.luau @@ -0,0 +1,3 @@ +local result = require("@dep") +result[#result + 1] = "external" +return result diff --git a/tests/src/packages/package_aware_require/internaldep/init.luau b/tests/src/packages/package_aware_require/internaldep/init.luau new file mode 100644 index 000000000..c351ed5a0 --- /dev/null +++ b/tests/src/packages/package_aware_require/internaldep/init.luau @@ -0,0 +1 @@ +return { "internal" } diff --git a/tests/src/packages/package_aware_require/packageentry/entry.luau b/tests/src/packages/package_aware_require/packageentry/entry.luau new file mode 100644 index 000000000..d95ed26a3 --- /dev/null +++ b/tests/src/packages/package_aware_require/packageentry/entry.luau @@ -0,0 +1,7 @@ +--!strict +local result = require("@dep") :: { string } +assert(type(result) == "table") +assert(#result == 2) + +assert(result[1] == "internal") +assert(result[2] == "external") diff --git a/tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau b/tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau new file mode 100644 index 000000000..22761ff19 --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau @@ -0,0 +1,3 @@ +local result = require("@internaldep") +result[#result + 1] = "external" +return result diff --git a/tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau b/tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau new file mode 100644 index 000000000..c351ed5a0 --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau @@ -0,0 +1 @@ +return { "internal" } diff --git a/tests/src/packages/pkgrun_with_lockfile/loom.lock.luau b/tests/src/packages/pkgrun_with_lockfile/loom.lock.luau new file mode 100644 index 000000000..e2fe02dab --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/loom.lock.luau @@ -0,0 +1,13 @@ +return { + version = 1, + package = { + { + name = "dep", + rev = "v1.2.3", + }, + { + name = "internaldep", + rev = "v4.5.6", + }, + }, +} diff --git a/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau b/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau new file mode 100644 index 000000000..d95ed26a3 --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau @@ -0,0 +1,7 @@ +--!strict +local result = require("@dep") :: { string } +assert(type(result) == "table") +assert(#result == 2) + +assert(result[1] == "internal") +assert(result[2] == "external") diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 16d3af0ea..fc636040d 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -1,32 +1,18 @@ -#include "Luau/FileUtils.h" -#include "doctest.h" - #include "lute/climain.h" -#include "lute/require.h" -#include "lute/runtime.h" +#include "lute/fileutils.h" +#include "lute/uvutils.h" -#include "Luau/Require.h" +#include "Luau/FileUtils.h" #include "lua.h" -#include "lualib.h" -#include "luteprojectroot.h" -#include +#include "uv.h" -class CliRuntimeFixture -{ -public: - CliRuntimeFixture() - : runtime(std::make_unique()) - { - L = setupCliState(*runtime); - } - - lua_State* L; +#include "cliruntimefixture.h" +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" -private: - std::unique_ptr runtime; -}; TEST_CASE_FIXTURE(CliRuntimeFixture, "require_exists") { @@ -34,9 +20,9 @@ TEST_CASE_FIXTURE(CliRuntimeFixture, "require_exists") CHECK(!lua_isnil(L, -1)); } -TEST_CASE("require_modules") +TEST_CASE_FIXTURE(LuteFixture, "require_modules") { - auto doPassingSubcase = [](std::vector& argv, std::string requirePath, std::vector expectedResults) + auto doPassingSubcase = [&](std::vector argv, std::string requirePath, std::vector expectedResults) { std::string pass = "pass"; argv.push_back(pass.data()); @@ -45,10 +31,10 @@ TEST_CASE("require_modules") { argv.push_back(result.data()); } - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); }; - auto doFailingSubcase = [](std::vector& argv, std::string requirePath, std::vector expectedResults) + auto doFailingSubcase = [&](std::vector argv, std::string requirePath, std::vector expectedResults) { std::string fail = "fail"; argv.push_back(fail.data()); @@ -57,7 +43,7 @@ TEST_CASE("require_modules") { argv.push_back(result.data()); } - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); }; char executablePlaceholder[] = "lute"; @@ -86,7 +72,7 @@ TEST_CASE("require_modules") { doPassingSubcase(argv, {"./without_config/luau"}, {"result from init.luau"}); } - + SUBCASE("init_lua") { doPassingSubcase(argv, {"./without_config/lua"}, {"result from init.lua"}); @@ -122,22 +108,44 @@ TEST_CASE("require_modules") SUBCASE("with_module_alias") { - doPassingSubcase(argv, {"./with_config/src/alias_requirer"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/alias_requirer"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/alias_requirer"}, {"result from dependency"}); } SUBCASE("with_directory_alias") { - doPassingSubcase(argv, {"./with_config/src/directory_alias_requirer"}, {"result from subdirectory_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/directory_alias_requirer"}, {"result from subdirectory_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/directory_alias_requirer"}, {"result from subdirectory_dependency"}); } SUBCASE("with_parent_configuration_alias") { - doPassingSubcase(argv, {"./with_config/src/parent_alias_requirer"}, {"result from other_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/parent_alias_requirer"}, {"result from other_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/parent_alias_requirer"}, {"result from other_dependency"}); } SUBCASE("init_does_not_read_sibling_luaurc") { - doPassingSubcase(argv, {"./with_config/src/submodule"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/submodule"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/submodule"}, {"result from dependency"}); + } + + SUBCASE("config_ambiguity") + { + doFailingSubcase( + argv, + {"./config_tests/config_ambiguity/requirer"}, + {R"(error requiring module "@dep": could not resolve alias "dep" (ambiguous configuration file))"} + ); + } + + SUBCASE("config_cannot_be_required") + { + doFailingSubcase( + argv, + {"./config_tests/config_cannot_be_required/requirer"}, + {R"(error requiring module "./.config": could not resolve child component ".config")"} + ); } SUBCASE("lute_modules") @@ -151,3 +159,101 @@ TEST_CASE("require_modules") } } } + +TEST_CASE_FIXTURE(LuteFixture, "require_with_parent_ambiguity") +{ + // This test case cannot be included in the general "require_modules" test + // because ambiguity prevents the test's requirer.luau from navigating to + // this test's entry point. Instead, we manually start the entry point here. + + char executablePlaceholder[] = "lute"; + + // .luaurc + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } + + // .config.luau + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = + joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } +} + +TEST_CASE_FIXTURE(LuteFixture, "require_types") +{ + char executablePlaceholder[] = "lute"; + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/without_config/types/tester.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } +} + +TEST_CASE_FIXTURE(LuteFixture, "require_by_string_semantics_in_cli") +{ + char executablePlaceholder[] = "lute"; + + // Expected to pass + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::vector inputPaths = { + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested"), + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/init.luau"), + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/submodule"), + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/submodule.luau"), + }; + + for (std::string& inputPath : inputPaths) + { + std::vector argv = {executablePlaceholder, inputPath.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } + } + + // Expected to fail + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string inputPath = joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/init"); + std::vector argv = {executablePlaceholder, inputPath.data()}; + CHECK_NE(cliMain(argv.size(), argv.data(), getReporter()), 0); + } +} + +TEST_CASE_FIXTURE(LuteFixture, "require_check_tilde_path") +{ + char executablePlaceholder[] = "lute"; + char subCommand[] = "check"; + // Get home directory + auto result = uvutils::getStringFromUv(uv_os_homedir); + REQUIRE(result.get_if() != nullptr); + std::string homeDir = *result.get_if(); + + // Create test directory and test file + std::string testDir = joinPaths(homeDir, "lute_test_special"); + std::string testFile = joinPaths(testDir, "foo.luau"); + REQUIRE(Lute::createDirectories(testDir)); + + // Write test module file + REQUIRE(Lute::writeFile(testFile, "return { foo = \"bar\" }\n")); + + // Run the test + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string mainPath = joinPaths(luteProjectRoot, "tests/src/require/config_tests/tilde_config/main.luau"); + std::vector argv = {executablePlaceholder, subCommand, mainPath.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } + + // Clean up + Lute::removeFile(testFile); + Lute::removeDirectory(testDir); +} diff --git a/tests/src/require/config_tests/README.md b/tests/src/require/config_tests/README.md new file mode 100644 index 000000000..026899696 --- /dev/null +++ b/tests/src/require/config_tests/README.md @@ -0,0 +1 @@ +The `with_config` and `with_config_luau` directories should be identical, apart from the configuration file type they use. diff --git a/tests/src/require/config_tests/config_ambiguity/.config.luau b/tests/src/require/config_tests/config_ambiguity/.config.luau new file mode 100644 index 000000000..27af9236f --- /dev/null +++ b/tests/src/require/config_tests/config_ambiguity/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + dep = "./dependency" + } + } +} diff --git a/tests/src/require/config_tests/config_ambiguity/.luaurc b/tests/src/require/config_tests/config_ambiguity/.luaurc new file mode 100644 index 000000000..90fe77c24 --- /dev/null +++ b/tests/src/require/config_tests/config_ambiguity/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "dep": "./dependency" + } +} diff --git a/tests/src/require/with_config/src/dependency.luau b/tests/src/require/config_tests/config_ambiguity/dependency.luau similarity index 100% rename from tests/src/require/with_config/src/dependency.luau rename to tests/src/require/config_tests/config_ambiguity/dependency.luau diff --git a/tests/src/require/with_config/src/alias_requirer.luau b/tests/src/require/config_tests/config_ambiguity/requirer.luau similarity index 100% rename from tests/src/require/with_config/src/alias_requirer.luau rename to tests/src/require/config_tests/config_ambiguity/requirer.luau diff --git a/tests/src/require/config_tests/config_cannot_be_required/.config.luau b/tests/src/require/config_tests/config_cannot_be_required/.config.luau new file mode 100644 index 000000000..a3cb35058 --- /dev/null +++ b/tests/src/require/config_tests/config_cannot_be_required/.config.luau @@ -0,0 +1 @@ +return {"result from .config.luau"} diff --git a/tests/src/require/config_tests/config_cannot_be_required/requirer.luau b/tests/src/require/config_tests/config_cannot_be_required/requirer.luau new file mode 100644 index 000000000..38c00431a --- /dev/null +++ b/tests/src/require/config_tests/config_cannot_be_required/requirer.luau @@ -0,0 +1 @@ +return require("./.config") diff --git a/tests/src/require/config_tests/tilde_config/.luaurc b/tests/src/require/config_tests/tilde_config/.luaurc new file mode 100644 index 000000000..690dc6af4 --- /dev/null +++ b/tests/src/require/config_tests/tilde_config/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "tilde": "~/lute_test_special" + } +} diff --git a/tests/src/require/config_tests/tilde_config/main.luau b/tests/src/require/config_tests/tilde_config/main.luau new file mode 100644 index 000000000..b0f6c9f22 --- /dev/null +++ b/tests/src/require/config_tests/tilde_config/main.luau @@ -0,0 +1 @@ +local x = require("@tilde/foo") diff --git a/tests/src/require/with_config/.luaurc b/tests/src/require/config_tests/with_config/.luaurc similarity index 100% rename from tests/src/require/with_config/.luaurc rename to tests/src/require/config_tests/with_config/.luaurc diff --git a/tests/src/require/with_config/src/.luaurc b/tests/src/require/config_tests/with_config/src/.luaurc similarity index 100% rename from tests/src/require/with_config/src/.luaurc rename to tests/src/require/config_tests/with_config/src/.luaurc diff --git a/tests/src/require/with_config/src/submodule/init.luau b/tests/src/require/config_tests/with_config/src/alias_requirer.luau similarity index 100% rename from tests/src/require/with_config/src/submodule/init.luau rename to tests/src/require/config_tests/with_config/src/alias_requirer.luau diff --git a/tests/src/require/config_tests/with_config/src/dependency.luau b/tests/src/require/config_tests/with_config/src/dependency.luau new file mode 100644 index 000000000..b1d7f9c81 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/dependency.luau @@ -0,0 +1 @@ +return { "result from dependency" } diff --git a/tests/src/require/with_config/src/directory_alias_requirer.luau b/tests/src/require/config_tests/with_config/src/directory_alias_requirer.luau similarity index 100% rename from tests/src/require/with_config/src/directory_alias_requirer.luau rename to tests/src/require/config_tests/with_config/src/directory_alias_requirer.luau diff --git a/tests/src/require/with_config/src/other_dependency.luau b/tests/src/require/config_tests/with_config/src/other_dependency.luau similarity index 100% rename from tests/src/require/with_config/src/other_dependency.luau rename to tests/src/require/config_tests/with_config/src/other_dependency.luau diff --git a/tests/src/require/with_config/src/parent_alias_requirer.luau b/tests/src/require/config_tests/with_config/src/parent_alias_requirer.luau similarity index 100% rename from tests/src/require/with_config/src/parent_alias_requirer.luau rename to tests/src/require/config_tests/with_config/src/parent_alias_requirer.luau diff --git a/tests/src/require/config_tests/with_config/src/parent_ambiguity/.luaurc b/tests/src/require/config_tests/with_config/src/parent_ambiguity/.luaurc new file mode 100644 index 000000000..24f8e9ff1 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/parent_ambiguity/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "foo": "./foo", + } +} diff --git a/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder.luau b/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder.luau new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau b/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau new file mode 100644 index 000000000..2f613057d --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau @@ -0,0 +1 @@ +return require("@foo") diff --git a/tests/src/require/config_tests/with_config/src/parent_ambiguity/foo.luau b/tests/src/require/config_tests/with_config/src/parent_ambiguity/foo.luau new file mode 100644 index 000000000..aa00aca1e --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/parent_ambiguity/foo.luau @@ -0,0 +1 @@ +return { "result from foo" } diff --git a/tests/src/require/with_config/src/subdirectory/subdirectory_dependency.luau b/tests/src/require/config_tests/with_config/src/subdirectory/subdirectory_dependency.luau similarity index 100% rename from tests/src/require/with_config/src/subdirectory/subdirectory_dependency.luau rename to tests/src/require/config_tests/with_config/src/subdirectory/subdirectory_dependency.luau diff --git a/tests/src/require/with_config/src/submodule/.luaurc b/tests/src/require/config_tests/with_config/src/submodule/.luaurc similarity index 100% rename from tests/src/require/with_config/src/submodule/.luaurc rename to tests/src/require/config_tests/with_config/src/submodule/.luaurc diff --git a/tests/src/require/config_tests/with_config/src/submodule/init.luau b/tests/src/require/config_tests/with_config/src/submodule/init.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/submodule/init.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/src/require/config_tests/with_config_luau/.config.luau b/tests/src/require/config_tests/with_config_luau/.config.luau new file mode 100644 index 000000000..b64979de2 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/.config.luau @@ -0,0 +1,8 @@ +return { + luau = { + aliases = { + dep = "./this_should_be_overwritten_by_child_luaurc", + otherdep = "./src/other_dependency" + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/.config.luau b/tests/src/require/config_tests/with_config_luau/src/.config.luau new file mode 100644 index 000000000..63d3bbc8e --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/.config.luau @@ -0,0 +1,8 @@ +return { + luau = { + aliases = { + dep = "./dependency", + subdir = "./subdirectory" + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau b/tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/src/require/config_tests/with_config_luau/src/dependency.luau b/tests/src/require/config_tests/with_config_luau/src/dependency.luau new file mode 100644 index 000000000..b1d7f9c81 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/dependency.luau @@ -0,0 +1 @@ +return { "result from dependency" } diff --git a/tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau b/tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau new file mode 100644 index 000000000..43507a5e6 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau @@ -0,0 +1 @@ +return (require("@subdir/subdirectory_dependency")) diff --git a/tests/src/require/config_tests/with_config_luau/src/other_dependency.luau b/tests/src/require/config_tests/with_config_luau/src/other_dependency.luau new file mode 100644 index 000000000..abf2670a4 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/other_dependency.luau @@ -0,0 +1 @@ +return { "result from other_dependency" } diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau b/tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau new file mode 100644 index 000000000..a8e8de094 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau @@ -0,0 +1 @@ +return require("@otherdep") diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau new file mode 100644 index 000000000..bc1ec7f0e --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + foo = "./foo", + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder.luau new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau new file mode 100644 index 000000000..2f613057d --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau @@ -0,0 +1 @@ +return require("@foo") diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau new file mode 100644 index 000000000..aa00aca1e --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau @@ -0,0 +1 @@ +return { "result from foo" } diff --git a/tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau b/tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau new file mode 100644 index 000000000..f555900e8 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau @@ -0,0 +1 @@ +return { "result from subdirectory_dependency" } diff --git a/tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau b/tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau new file mode 100644 index 000000000..eb309fd1f --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + dep = "./this_should_not_be_read_by_init_luau", + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/submodule/init.luau b/tests/src/require/config_tests/with_config_luau/src/submodule/init.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/submodule/init.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/src/require/lute/std.luau b/tests/src/require/lute/std.luau index 0b208e162..1eb4033b3 100644 --- a/tests/src/require/lute/std.luau +++ b/tests/src/require/lute/std.luau @@ -1,11 +1,11 @@ -local table = require("@std/table") +local tableext = require("@std/tableext") local task = require("@std/task") local time = require("@std/time") -local vector = require("@std/vector") +local system = require("@std/system") -assert(type(table) == "table") +assert(type(tableext) == "table") assert(type(task) == "table") assert(type(time) == "table") -assert(type(vector) == "table") +assert(type(system) == "table") return { "successfully required @std modules" } diff --git a/tests/src/require/without_config/types/boolean.luau b/tests/src/require/without_config/types/boolean.luau new file mode 100644 index 000000000..585a20e04 --- /dev/null +++ b/tests/src/require/without_config/types/boolean.luau @@ -0,0 +1 @@ +return false diff --git a/tests/src/require/without_config/types/buffer.luau b/tests/src/require/without_config/types/buffer.luau new file mode 100644 index 000000000..47869c5e5 --- /dev/null +++ b/tests/src/require/without_config/types/buffer.luau @@ -0,0 +1 @@ +return buffer.create(16) diff --git a/tests/src/require/without_config/types/function.luau b/tests/src/require/without_config/types/function.luau new file mode 100644 index 000000000..599ceec9d --- /dev/null +++ b/tests/src/require/without_config/types/function.luau @@ -0,0 +1,3 @@ +return function() + return 1 + 1 +end diff --git a/tests/src/require/without_config/types/nil.luau b/tests/src/require/without_config/types/nil.luau new file mode 100644 index 000000000..15e53c4a8 --- /dev/null +++ b/tests/src/require/without_config/types/nil.luau @@ -0,0 +1 @@ +return nil diff --git a/tests/src/require/without_config/types/number.luau b/tests/src/require/without_config/types/number.luau new file mode 100644 index 000000000..eadc91cf6 --- /dev/null +++ b/tests/src/require/without_config/types/number.luau @@ -0,0 +1 @@ +return 12345 diff --git a/tests/src/require/without_config/types/string.luau b/tests/src/require/without_config/types/string.luau new file mode 100644 index 000000000..bc39fbe82 --- /dev/null +++ b/tests/src/require/without_config/types/string.luau @@ -0,0 +1 @@ +return "foo" diff --git a/tests/src/require/without_config/types/table.luau b/tests/src/require/without_config/types/table.luau new file mode 100644 index 000000000..00aef0c9e --- /dev/null +++ b/tests/src/require/without_config/types/table.luau @@ -0,0 +1 @@ +return { "foo", "bar" } diff --git a/tests/src/require/without_config/types/tester.luau b/tests/src/require/without_config/types/tester.luau new file mode 100644 index 000000000..d51b333d6 --- /dev/null +++ b/tests/src/require/without_config/types/tester.luau @@ -0,0 +1,10 @@ +local _ = require("./boolean") +local _ = require("./buffer") +local _ = require("./function") +local _ = require("./nil") +local _ = require("./number") +local _ = require("./string") +local _ = require("./table") +local _ = require("./thread") +local _ = require("./userdata") +local _ = require("./vector") diff --git a/tests/src/require/without_config/types/thread.luau b/tests/src/require/without_config/types/thread.luau new file mode 100644 index 000000000..1661e039c --- /dev/null +++ b/tests/src/require/without_config/types/thread.luau @@ -0,0 +1,3 @@ +return coroutine.create(function() + return "foo" +end) diff --git a/tests/src/require/without_config/types/userdata.luau b/tests/src/require/without_config/types/userdata.luau new file mode 100644 index 000000000..a11620833 --- /dev/null +++ b/tests/src/require/without_config/types/userdata.luau @@ -0,0 +1 @@ +return newproxy() diff --git a/tests/src/require/without_config/types/vector.luau b/tests/src/require/without_config/types/vector.luau new file mode 100644 index 000000000..14210acb7 --- /dev/null +++ b/tests/src/require/without_config/types/vector.luau @@ -0,0 +1 @@ +return vector.create(1, 2, 3) diff --git a/tests/src/resolver/.luaurc b/tests/src/resolver/.luaurc new file mode 100644 index 000000000..b7a58bb73 --- /dev/null +++ b/tests/src/resolver/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "root": "./" + } +} diff --git a/tests/src/resolver/mainmodule.luau b/tests/src/resolver/mainmodule.luau new file mode 100644 index 000000000..64ddaa13c --- /dev/null +++ b/tests/src/resolver/mainmodule.luau @@ -0,0 +1,27 @@ +local types = require("@root/types") +local tableext = require("@std/tableext") + +local testFilter: { [string]: number } = { + ["a"] = 1, + ["b"] = 2, +} + +local a = {} +type tb = { [string]: number } + +function a._a(a: number) + return function(s: string): types.test + return types(s, a) + end +end + +a._b = tableext.map(testFilter, function(value) + return tostring(value) +end) + +a._metadata = { + key = -1, + id = "nice", +} + +return a diff --git a/tests/src/resolver/types.luau b/tests/src/resolver/types.luau new file mode 100644 index 000000000..68ecda81f --- /dev/null +++ b/tests/src/resolver/types.luau @@ -0,0 +1,7 @@ +export type test = { [string]: number } + +return function(s: string, n: number): test + return { + [s] = n, + } +end diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp new file mode 100644 index 000000000..3bc204988 --- /dev/null +++ b/tests/src/staticrequires.test.cpp @@ -0,0 +1,309 @@ +#include "lute/staticrequires.h" + +#include "Luau/FileUtils.h" + +#include +#include +#include + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_simple_dependencies") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "main.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); + + // Should find: main.luau, utils.luau, lib/helper.luau, shared.luau, dep/option.luau, other/module.luau, other/lib/example.luau + REQUIRE(pairs.size() == 7); + + // Entry point should be first + CHECK(pairs[0].first == "main.luau"); + + std::vector expectedFiles = { + "main.luau", "utils.luau", "lib/helper.luau", "shared.luau", "dep/option.luau", "other/lib/example.luau", "other/module.luau" + }; + + for (const auto& expected : expectedFiles) + { + std::string absolutePath = joinPaths(tracer.getLowestCommonRoot(), expected); + CHECK(tracer.containsAbsolute(absolutePath)); + } + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + + // Check that .luaurc exists in the map + CHECK(luaurcFiles.find(".luaurc") != nullptr); + CHECK(luaurcFiles.find("other/.luaurc") != nullptr); +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "circular_a.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); + + // Should find both circular_a and circular_b without infinite loop + REQUIRE(pairs.size() == 2); + + // Entry point should be first + CHECK(pairs[0].first == "circular_a.luau"); + + // Both files should be in the list using absolute paths + std::string absoluteA = joinPaths(tracer.getLowestCommonRoot(), "circular_a.luau"); + std::string absoluteB = joinPaths(tracer.getLowestCommonRoot(), "circular_b.luau"); + + CHECK(tracer.containsAbsolute(absoluteA)); + CHECK(tracer.containsAbsolute(absoluteB)); + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "utils.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); + + // utils.luau has no requires, should only return itself + REQUIRE(pairs.size() == 1); + CHECK(pairs[0].first == "utils.luau"); + + // Verify .luaurc file was discovered (should find it in the same directory) + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "lib/helper.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); + + // helper.luau requires ../shared, should resolve correctly + REQUIRE(pairs.size() == 2); + + CHECK(pairs[0].first == "lib/helper.luau"); + + std::string absoluteShared = joinPaths(tracer.getLowestCommonRoot(), "shared.luau"); + CHECK(tracer.containsAbsolute(absoluteShared)); + + // Verify .luaurc file was discovered (should find it in parent directory) + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + StaticRequireTracer tracer{getReporter()}; + std::string entryPoint = joinPaths(testDir, "main.luau"); + + tracer.trace(entryPoint); + + // Build absolute paths + std::string mainPath = joinPaths(testDir, "main.luau"); + std::string utilsPath = joinPaths(testDir, "utils.luau"); + std::string helperPath = joinPaths(testDir, "lib/helper.luau"); + std::string sharedPath = joinPaths(testDir, "shared.luau"); + std::string otherExample = joinPaths(testDir, "other/lib/example.luau"); + std::string otherModule = joinPaths(testDir, "other/module.luau"); + std::string dep = joinPaths(testDir, "dep/option.luau"); + + // Verify all expected files were discovered + CHECK(tracer.containsAbsolute(mainPath)); + CHECK(tracer.containsAbsolute(utilsPath)); + CHECK(tracer.containsAbsolute(helperPath)); + CHECK(tracer.containsAbsolute(sharedPath)); + CHECK(tracer.containsAbsolute(otherExample)); + CHECK(tracer.containsAbsolute(otherModule)); + CHECK(tracer.containsAbsolute(dep)); + + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + CHECK(luaurcFiles.find(".luaurc") != nullptr); + CHECK(luaurcFiles.find("other/.luaurc") != nullptr); + + // Verify the graph can be printed without errors (visual inspection of output) + tracer.printRequireGraph(); +} + +TEST_CASE("staticrequiretracer_find_lowest_common_root") +{ + SUBCASE("single_file") + { + std::vector paths = {"/home/user/project/src/main.luau"}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/src"); + } + + SUBCASE("same_directory") + { + std::vector paths = { + "/home/user/project/src/main.luau", "/home/user/project/src/utils.luau", "/home/user/project/src/helper.luau" + }; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/src"); + } + + SUBCASE("nested_directories") + { + std::vector paths = { + "/home/user/project/src/main.luau", "/home/user/project/src/utils.luau", "/home/user/project/src/lib/helper.luau" + }; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/src"); + } + + SUBCASE("different_subdirectories") + { + std::vector paths = {"/home/user/project/tests/src/main.luau", "/home/user/project/tests/lib/helper.luau"}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/tests"); + } + + SUBCASE("no_common_root") + { + std::vector paths = {"/home/user/project1/main.luau", "/var/lib/project2/utils.luau"}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + // Should find at least the root directory "/" + CHECK(!root.empty()); + } + + SUBCASE("empty_vector") + { + std::vector paths = {}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root.empty()); + } +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_bundle_paths_and_contains") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "main.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + // Get the lowest common root + std::string commonRoot = tracer.getLowestCommonRoot(); + CHECK(commonRoot == testDir); + + // Get discovered files as pairs + auto pairs = tracer.getStaticRequirePairs(); + REQUIRE(pairs.size() == 7); + + // Verify bundle paths (without common prefix) + std::vector expectedBundlePaths = { + "main.luau", "utils.luau", "lib/helper.luau", "shared.luau", "dep/option.luau", "other/lib/example.luau", "other/module.luau" + }; + + for (const auto& expected : expectedBundlePaths) + { + bool found = false; + for (const auto& [bundlePath, absolutePath] : pairs) + { + if (bundlePath == expected) + { + found = true; + break; + } + } + CHECK(found); + } + + // Test containsAbsolute() method with absolute paths + std::string mainAbsolute = joinPaths(commonRoot, "main.luau"); + CHECK(tracer.containsAbsolute(mainAbsolute)); + + std::string helperAbsolute = joinPaths(commonRoot, "lib/helper.luau"); + CHECK(tracer.containsAbsolute(helperAbsolute)); + + // Test non-existent module + std::string nonExistentAbsolute = joinPaths(commonRoot, "nonexistent.luau"); + CHECK(!tracer.containsAbsolute(nonExistentAbsolute)); + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + CHECK(luaurcFiles.find(".luaurc") != nullptr); + CHECK(luaurcFiles.find("other/.luaurc") != nullptr); +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_multiple_luaurc_files") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "main.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + // Get discovered files as pairs + auto pairs = tracer.getStaticRequirePairs(); + REQUIRE(pairs.size() == 7); // main, utils, lib/helper, shared, other/module, other/lib/example, dep/option + + // Verify all expected files are discovered including those in the other/ directory + std::vector expectedBundlePaths = { + "main.luau", "utils.luau", "lib/helper.luau", "shared.luau", "other/module.luau", "other/lib/example.luau" + }; + + for (const auto& expected : expectedBundlePaths) + { + bool found = false; + for (const auto& [bundlePath, absolutePath] : pairs) + { + if (bundlePath == expected) + { + found = true; + break; + } + } + CHECK(found); + } + + // Verify both .luaurc files were discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + + // Check that both .luaurc files exist in the map + const std::string* rootLuaurc = luaurcFiles.find(".luaurc"); + REQUIRE(rootLuaurc != nullptr); + + const std::string* otherLuaurc = luaurcFiles.find("other/.luaurc"); + REQUIRE(otherLuaurc != nullptr); +} diff --git a/tests/src/staticrequires/.luaurc b/tests/src/staticrequires/.luaurc new file mode 100644 index 000000000..ceb0531c6 --- /dev/null +++ b/tests/src/staticrequires/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "example": "./dep" + } +} diff --git a/tests/src/staticrequires/circular_a.luau b/tests/src/staticrequires/circular_a.luau new file mode 100644 index 000000000..99b431a0e --- /dev/null +++ b/tests/src/staticrequires/circular_a.luau @@ -0,0 +1,7 @@ +-- Module A that requires B (circular dependency) +local b = require("./circular_b") + +return { + name = "A", + b = b, +} diff --git a/tests/src/staticrequires/circular_b.luau b/tests/src/staticrequires/circular_b.luau new file mode 100644 index 000000000..eb342c930 --- /dev/null +++ b/tests/src/staticrequires/circular_b.luau @@ -0,0 +1,7 @@ +-- Module B that requires A (circular dependency) +local a = require("./circular_a") + +return { + name = "B", + a = a, +} diff --git a/tests/src/staticrequires/dep/option.luau b/tests/src/staticrequires/dep/option.luau new file mode 100644 index 000000000..8df485807 --- /dev/null +++ b/tests/src/staticrequires/dep/option.luau @@ -0,0 +1,3 @@ +local v = { file = "option.luau" } + +return v diff --git a/tests/src/staticrequires/lib/helper.luau b/tests/src/staticrequires/lib/helper.luau new file mode 100644 index 000000000..8134b7e13 --- /dev/null +++ b/tests/src/staticrequires/lib/helper.luau @@ -0,0 +1,10 @@ +-- Helper module that also requires another module +local shared = require("../shared") + +-- print("Helper module") +return { + help = function() + return "helper" + end, + shared = shared, +} diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau new file mode 100644 index 000000000..0a2244f0a --- /dev/null +++ b/tests/src/staticrequires/main.luau @@ -0,0 +1,21 @@ +-- test that you can require in an alias aware fashion +local example = require("@example/option") +-- print(`Required {example.file} from alias @example`) + +-- Entry point that requires other modules +local utils = require("./utils") +local lib = require("./lib/helper") +local otherModule = require("./other/module") +local process = require("@std/process") +local path = require("@std/path") + +-- Use standard library to verify it works in compiled bundles +local cwd = path.format(process.cwd()) +-- print("Main module running from: " .. cwd) + +return { + utils = utils, + lib = lib, + otherModule = otherModule, + cwd = cwd, +} diff --git a/tests/src/staticrequires/newsolver.luau b/tests/src/staticrequires/newsolver.luau new file mode 100644 index 000000000..533dfcc7e --- /dev/null +++ b/tests/src/staticrequires/newsolver.luau @@ -0,0 +1,14 @@ +-- This file should fail the old solver +function add(a, b) + return a + b +end +local vec2 = {} +function vec2.new(x, y) + return setmetatable({ x = x or 0, y = y or 0 }, { + __add = function(v1, v2) + return { x = v1.x + v2.x, y = v1.y + v2.y } + end, + }) +end +add(1, 1) +add(vec2.new(0, 0), vec2.new(1, 1)) diff --git a/tests/src/staticrequires/other/.luaurc b/tests/src/staticrequires/other/.luaurc new file mode 100644 index 000000000..82ecf5199 --- /dev/null +++ b/tests/src/staticrequires/other/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "otherlib": "./lib" + } +} diff --git a/tests/src/staticrequires/other/lib/example.luau b/tests/src/staticrequires/other/lib/example.luau new file mode 100644 index 000000000..b0d5a85f6 --- /dev/null +++ b/tests/src/staticrequires/other/lib/example.luau @@ -0,0 +1,3 @@ +local v = { file = "example.luau" } + +return v diff --git a/tests/src/staticrequires/other/module.luau b/tests/src/staticrequires/other/module.luau new file mode 100644 index 000000000..fdb90ea47 --- /dev/null +++ b/tests/src/staticrequires/other/module.luau @@ -0,0 +1,7 @@ +-- test that we can require using an alias from other/.luaurc +local example = require("@otherlib/example") +-- print(`Required {example.file} from alias @otherlib`) + +return { + example = example, +} diff --git a/tests/src/staticrequires/shared.luau b/tests/src/staticrequires/shared.luau new file mode 100644 index 000000000..fbe99b428 --- /dev/null +++ b/tests/src/staticrequires/shared.luau @@ -0,0 +1,5 @@ +-- Shared module +-- print("Shared module") +return { + version = "1.0", +} diff --git a/tests/src/staticrequires/utils.luau b/tests/src/staticrequires/utils.luau new file mode 100644 index 000000000..a4af5a1cc --- /dev/null +++ b/tests/src/staticrequires/utils.luau @@ -0,0 +1,7 @@ +-- Utilities module +-- print("Utils module") +return { + add = function(a, b) + return a + b + end, +} diff --git a/tests/src/stdsystem.test.cpp b/tests/src/stdsystem.test.cpp new file mode 100644 index 000000000..2787720b3 --- /dev/null +++ b/tests/src/stdsystem.test.cpp @@ -0,0 +1,52 @@ +#include "Luau/StringUtils.h" + +#include + +#include "cliruntimefixture.h" +#include "doctest.h" + +std::string getHostOS() +{ +#if defined(__linux__) + return "Linux"; +#elif defined(__APPLE__) + return "Darwin"; +#elif defined(_WIN32) + return "Windows_NT"; +#else + return "unknown"; +#endif +} + +TEST_CASE_FIXTURE(CliRuntimeFixture, "std_system_os_matches_host_os") +{ + runCode(R"( + local system = require("@std/system") + report(system.os) + )"); + CHECK(getReporter().getOutputs()[0] == getHostOS()); +} + +TEST_CASE_FIXTURE(CliRuntimeFixture, "check_std_system_env_bools") +{ + std::string os = getHostOS(); + + auto checkBool = [this](const std::string& field, bool expected) + { + std::string code = Luau::format( + R"( + local system = require("@std/system") + report(system.%s))", + field.c_str() + ); + runCode(code); + std::string output = this->getReporter().getOutputs()[0]; + CHECK(output == (expected ? "true" : "false")); + this->getReporter().clear(); + }; + + checkBool("win32", os == "Windows_NT"); + checkBool("linux", os == "Linux"); + checkBool("macos", os == "Darwin"); + checkBool("unix", os == "Linux" || os == "Darwin"); +} diff --git a/tests/src/testreporter.cpp b/tests/src/testreporter.cpp new file mode 100644 index 000000000..b4d876011 --- /dev/null +++ b/tests/src/testreporter.cpp @@ -0,0 +1,27 @@ +#include "testreporter.h" + +void TestReporter::reportError(const std::string& message) +{ + errors.push_back(message); +} + +void TestReporter::reportOutput(const std::string& message) +{ + outputs.push_back(message); +} + +const std::vector& TestReporter::getErrors() const +{ + return errors; +} + +const std::vector& TestReporter::getOutputs() const +{ + return outputs; +} + +void TestReporter::clear() +{ + errors.clear(); + outputs.clear(); +} diff --git a/tests/src/testreporter.h b/tests/src/testreporter.h new file mode 100644 index 000000000..5e768e76c --- /dev/null +++ b/tests/src/testreporter.h @@ -0,0 +1,22 @@ +#pragma once + +#include "lute/reporter.h" +#include +#include + +// Test reporter that captures output in vectors for verification +class TestReporter : public LuteReporter +{ +public: + void reportError(const std::string& message) override; + void reportOutput(const std::string& message) override; + + const std::vector& getErrors() const; + const std::vector& getOutputs() const; + + void clear(); + +private: + std::vector errors; + std::vector outputs; +}; diff --git a/tests/src/typecheck.test.cpp b/tests/src/typecheck.test.cpp new file mode 100644 index 000000000..b9e0b3f47 --- /dev/null +++ b/tests/src/typecheck.test.cpp @@ -0,0 +1,16 @@ +#include "lute/tc.h" + +#include "Luau/FileUtils.h" + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(LuteFixture, "typecheck_uses_new_solver") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/newsolver.luau"); + + auto result = typecheck({testFilePath}, getReporter()); + CHECK(result == 0); +} diff --git a/tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json b/tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json new file mode 100644 index 000000000..ae4c7b71f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json @@ -0,0 +1 @@ +[123.456e-789] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_huge_exp.json b/tests/std/JSONParsingTestSuite/i_number_huge_exp.json new file mode 100644 index 000000000..9b5efa236 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_huge_exp.json @@ -0,0 +1 @@ +[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json b/tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json new file mode 100755 index 000000000..3abd58a5c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json @@ -0,0 +1 @@ +[-1e+9999] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json b/tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json new file mode 100755 index 000000000..e10a7eb62 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json @@ -0,0 +1 @@ +[1.5e+9999] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json b/tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json new file mode 100644 index 000000000..3d628a994 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json @@ -0,0 +1 @@ +[-123123e100000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json b/tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json new file mode 100644 index 000000000..54d7d3dcd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json @@ -0,0 +1 @@ +[123123e100000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_real_underflow.json b/tests/std/JSONParsingTestSuite/i_number_real_underflow.json new file mode 100644 index 000000000..c5236eb26 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_real_underflow.json @@ -0,0 +1 @@ +[123e-10000000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json b/tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json new file mode 100644 index 000000000..dfa384619 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json @@ -0,0 +1 @@ +[-123123123123123123123123123123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json b/tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json new file mode 100644 index 000000000..338a8c3c0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json @@ -0,0 +1 @@ +[100000000000000000000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json b/tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json new file mode 100755 index 000000000..e2d9738c2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json @@ -0,0 +1 @@ +[-237462374673276894279832749832423479823246327846] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json b/tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json new file mode 100644 index 000000000..5be7ebaf9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json @@ -0,0 +1 @@ +{"\uDFAA":0} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json b/tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json new file mode 100644 index 000000000..3b9e37c67 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json @@ -0,0 +1 @@ +["\uDADA"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json b/tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json new file mode 100644 index 000000000..487592832 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json @@ -0,0 +1 @@ +["\uD888\u1234"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_UTF-16LE_with_BOM.json b/tests/std/JSONParsingTestSuite/i_string_UTF-16LE_with_BOM.json new file mode 100644 index 000000000..2a79c0629 Binary files /dev/null and b/tests/std/JSONParsingTestSuite/i_string_UTF-16LE_with_BOM.json differ diff --git a/tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json b/tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json new file mode 100755 index 000000000..e2a968a15 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json @@ -0,0 +1 @@ +["日ш"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json b/tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json new file mode 100644 index 000000000..916bff920 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json new file mode 100755 index 000000000..3cb11d229 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json @@ -0,0 +1 @@ +["\uD800\n"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json new file mode 100755 index 000000000..38ec23bb0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json @@ -0,0 +1 @@ +["\uDd1ea"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json new file mode 100755 index 000000000..c9cd6f6c3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json @@ -0,0 +1 @@ +["\uD800\uD800\n"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json b/tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json new file mode 100755 index 000000000..3abbd8d8d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json @@ -0,0 +1 @@ +["\ud800"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json b/tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json new file mode 100755 index 000000000..ffddc04f5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json @@ -0,0 +1 @@ +["\ud800abc"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json b/tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json new file mode 100644 index 000000000..8e45a7eca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json b/tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json new file mode 100755 index 000000000..0d5456cc3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json @@ -0,0 +1 @@ +["\uDd1e\uD834"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json b/tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json new file mode 100644 index 000000000..9389c9823 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json b/tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json new file mode 100644 index 000000000..1dbd397f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json @@ -0,0 +1 @@ +["\uDFAA"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json b/tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json new file mode 100644 index 000000000..729337c0a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json b/tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json new file mode 100644 index 000000000..df90a2916 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json new file mode 100644 index 000000000..c8cee5e0a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json new file mode 100755 index 000000000..9a91da791 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json new file mode 100755 index 000000000..d24fffdd9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json b/tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json new file mode 100644 index 000000000..63c7777fb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_utf16BE_no_BOM.json b/tests/std/JSONParsingTestSuite/i_string_utf16BE_no_BOM.json new file mode 100644 index 000000000..57e5392ff Binary files /dev/null and b/tests/std/JSONParsingTestSuite/i_string_utf16BE_no_BOM.json differ diff --git a/tests/std/JSONParsingTestSuite/i_string_utf16LE_no_BOM.json b/tests/std/JSONParsingTestSuite/i_string_utf16LE_no_BOM.json new file mode 100644 index 000000000..c49c1b25d Binary files /dev/null and b/tests/std/JSONParsingTestSuite/i_string_utf16LE_no_BOM.json differ diff --git a/tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json b/tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json new file mode 100644 index 000000000..711840589 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json b/tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json new file mode 100755 index 000000000..22fdca1b2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json b/tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json new file mode 100644 index 000000000..c14e3f6b1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json @@ -0,0 +1 @@ +[1 true] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json b/tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json new file mode 100644 index 000000000..38a86e2e6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json @@ -0,0 +1 @@ +[a] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json b/tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json new file mode 100644 index 000000000..0d02ad448 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json @@ -0,0 +1 @@ +["": 1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_comma_after_close.json b/tests/std/JSONParsingTestSuite/n_array_comma_after_close.json new file mode 100644 index 000000000..2ccba8d95 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_comma_after_close.json @@ -0,0 +1 @@ +[""], \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_comma_and_number.json b/tests/std/JSONParsingTestSuite/n_array_comma_and_number.json new file mode 100755 index 000000000..d2c84e374 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_comma_and_number.json @@ -0,0 +1 @@ +[,1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_double_comma.json b/tests/std/JSONParsingTestSuite/n_array_double_comma.json new file mode 100755 index 000000000..0431712bc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_double_comma.json @@ -0,0 +1 @@ +[1,,2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json b/tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json new file mode 100644 index 000000000..3f01d3129 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json @@ -0,0 +1 @@ +["x",,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_extra_close.json b/tests/std/JSONParsingTestSuite/n_array_extra_close.json new file mode 100644 index 000000000..c12f9fae1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_extra_close.json @@ -0,0 +1 @@ +["x"]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_extra_comma.json b/tests/std/JSONParsingTestSuite/n_array_extra_comma.json new file mode 100644 index 000000000..5f8ce18e4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_extra_comma.json @@ -0,0 +1 @@ +["",] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_incomplete.json b/tests/std/JSONParsingTestSuite/n_array_incomplete.json new file mode 100644 index 000000000..cc65b0b51 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_incomplete.json @@ -0,0 +1 @@ +["x" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json b/tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json new file mode 100644 index 000000000..c21a8f6cf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json @@ -0,0 +1 @@ +[x \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json b/tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json new file mode 100644 index 000000000..c70b71647 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json @@ -0,0 +1 @@ +[3[4]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json b/tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json new file mode 100644 index 000000000..6099d3441 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json b/tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json new file mode 100755 index 000000000..d4bd7314c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json @@ -0,0 +1 @@ +[1:2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_just_comma.json b/tests/std/JSONParsingTestSuite/n_array_just_comma.json new file mode 100755 index 000000000..9d7077c68 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_just_comma.json @@ -0,0 +1 @@ +[,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_just_minus.json b/tests/std/JSONParsingTestSuite/n_array_just_minus.json new file mode 100755 index 000000000..29501c6ca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_just_minus.json @@ -0,0 +1 @@ +[-] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_missing_value.json b/tests/std/JSONParsingTestSuite/n_array_missing_value.json new file mode 100644 index 000000000..3a6ba86f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_missing_value.json @@ -0,0 +1 @@ +[ , ""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json b/tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json new file mode 100644 index 000000000..646680065 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json @@ -0,0 +1,3 @@ +["a", +4 +,1, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_number_and_comma.json b/tests/std/JSONParsingTestSuite/n_array_number_and_comma.json new file mode 100755 index 000000000..13f6f1d18 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_number_and_comma.json @@ -0,0 +1 @@ +[1,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json b/tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json new file mode 100755 index 000000000..0ac408cb8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json @@ -0,0 +1 @@ +[1,,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json b/tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json new file mode 100755 index 000000000..6cd7cf585 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json @@ -0,0 +1 @@ +[" a"\f] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_star_inside.json b/tests/std/JSONParsingTestSuite/n_array_star_inside.json new file mode 100755 index 000000000..5a5194647 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_star_inside.json @@ -0,0 +1 @@ +[*] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed.json b/tests/std/JSONParsingTestSuite/n_array_unclosed.json new file mode 100644 index 000000000..060733059 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed.json @@ -0,0 +1 @@ +["" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json b/tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json new file mode 100644 index 000000000..6604698ff --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json @@ -0,0 +1 @@ +[1, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json new file mode 100644 index 000000000..4f61de3fb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json @@ -0,0 +1,3 @@ +[1, +1 +,1 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json new file mode 100644 index 000000000..043a87e2d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json @@ -0,0 +1 @@ +[{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_incomplete_false.json b/tests/std/JSONParsingTestSuite/n_incomplete_false.json new file mode 100644 index 000000000..eb18c6a14 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_incomplete_false.json @@ -0,0 +1 @@ +[fals] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_incomplete_null.json b/tests/std/JSONParsingTestSuite/n_incomplete_null.json new file mode 100644 index 000000000..c18ef5385 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_incomplete_null.json @@ -0,0 +1 @@ +[nul] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_incomplete_true.json b/tests/std/JSONParsingTestSuite/n_incomplete_true.json new file mode 100644 index 000000000..f451ac6d2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_incomplete_true.json @@ -0,0 +1 @@ +[tru] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_multidigit_number_then_00.json b/tests/std/JSONParsingTestSuite/n_multidigit_number_then_00.json new file mode 100644 index 000000000..c22507b86 Binary files /dev/null and b/tests/std/JSONParsingTestSuite/n_multidigit_number_then_00.json differ diff --git a/tests/std/JSONParsingTestSuite/n_number_++.json b/tests/std/JSONParsingTestSuite/n_number_++.json new file mode 100644 index 000000000..bdb62aaf4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_++.json @@ -0,0 +1 @@ +[++1234] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_+1.json b/tests/std/JSONParsingTestSuite/n_number_+1.json new file mode 100755 index 000000000..3cbe58c92 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_+1.json @@ -0,0 +1 @@ +[+1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_+Inf.json b/tests/std/JSONParsingTestSuite/n_number_+Inf.json new file mode 100755 index 000000000..871ae14d5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_+Inf.json @@ -0,0 +1 @@ +[+Inf] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-01.json b/tests/std/JSONParsingTestSuite/n_number_-01.json new file mode 100755 index 000000000..0df32bac8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-01.json @@ -0,0 +1 @@ +[-01] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-1.0..json b/tests/std/JSONParsingTestSuite/n_number_-1.0..json new file mode 100755 index 000000000..7cf55a85a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-1.0..json @@ -0,0 +1 @@ +[-1.0.] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-2..json b/tests/std/JSONParsingTestSuite/n_number_-2..json new file mode 100755 index 000000000..9be84365d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-2..json @@ -0,0 +1 @@ +[-2.] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-NaN.json b/tests/std/JSONParsingTestSuite/n_number_-NaN.json new file mode 100755 index 000000000..f61615d40 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-NaN.json @@ -0,0 +1 @@ +[-NaN] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_.-1.json b/tests/std/JSONParsingTestSuite/n_number_.-1.json new file mode 100644 index 000000000..1c9f2dd1b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_.-1.json @@ -0,0 +1 @@ +[.-1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_.2e-3.json b/tests/std/JSONParsingTestSuite/n_number_.2e-3.json new file mode 100755 index 000000000..c6c976f25 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_.2e-3.json @@ -0,0 +1 @@ +[.2e-3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.1.2.json b/tests/std/JSONParsingTestSuite/n_number_0.1.2.json new file mode 100755 index 000000000..c83a25621 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.1.2.json @@ -0,0 +1 @@ +[0.1.2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.3e+.json b/tests/std/JSONParsingTestSuite/n_number_0.3e+.json new file mode 100644 index 000000000..a55a1bfef --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.3e+.json @@ -0,0 +1 @@ +[0.3e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.3e.json b/tests/std/JSONParsingTestSuite/n_number_0.3e.json new file mode 100644 index 000000000..3dd5df4b3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.3e.json @@ -0,0 +1 @@ +[0.3e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.e1.json b/tests/std/JSONParsingTestSuite/n_number_0.e1.json new file mode 100644 index 000000000..c92c71ccb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.e1.json @@ -0,0 +1 @@ +[0.e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json b/tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json new file mode 100644 index 000000000..3ba2c7d6d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json @@ -0,0 +1 @@ +[0E+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0_capital_E.json b/tests/std/JSONParsingTestSuite/n_number_0_capital_E.json new file mode 100755 index 000000000..5301840d1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0_capital_E.json @@ -0,0 +1 @@ +[0E] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0e+.json b/tests/std/JSONParsingTestSuite/n_number_0e+.json new file mode 100644 index 000000000..8ab0bc4b8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0e+.json @@ -0,0 +1 @@ +[0e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0e.json b/tests/std/JSONParsingTestSuite/n_number_0e.json new file mode 100644 index 000000000..47ec421bb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0e.json @@ -0,0 +1 @@ +[0e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1.0e+.json b/tests/std/JSONParsingTestSuite/n_number_1.0e+.json new file mode 100755 index 000000000..cd84b9f69 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1.0e+.json @@ -0,0 +1 @@ +[1.0e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1.0e-.json b/tests/std/JSONParsingTestSuite/n_number_1.0e-.json new file mode 100755 index 000000000..4eb7afa0f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1.0e-.json @@ -0,0 +1 @@ +[1.0e-] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1.0e.json b/tests/std/JSONParsingTestSuite/n_number_1.0e.json new file mode 100755 index 000000000..21753f4c7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1.0e.json @@ -0,0 +1 @@ +[1.0e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1_000.json b/tests/std/JSONParsingTestSuite/n_number_1_000.json new file mode 100755 index 000000000..7b18b66b3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1_000.json @@ -0,0 +1 @@ +[1 000.0] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1eE2.json b/tests/std/JSONParsingTestSuite/n_number_1eE2.json new file mode 100755 index 000000000..4318a341d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1eE2.json @@ -0,0 +1 @@ +[1eE2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_2.e+3.json b/tests/std/JSONParsingTestSuite/n_number_2.e+3.json new file mode 100755 index 000000000..4442f394d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_2.e+3.json @@ -0,0 +1 @@ +[2.e+3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_2.e-3.json b/tests/std/JSONParsingTestSuite/n_number_2.e-3.json new file mode 100755 index 000000000..a65060edf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_2.e-3.json @@ -0,0 +1 @@ +[2.e-3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_2.e3.json b/tests/std/JSONParsingTestSuite/n_number_2.e3.json new file mode 100755 index 000000000..66f7cf701 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_2.e3.json @@ -0,0 +1 @@ +[2.e3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_9.e+.json b/tests/std/JSONParsingTestSuite/n_number_9.e+.json new file mode 100644 index 000000000..732a7b11c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_9.e+.json @@ -0,0 +1 @@ +[9.e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_Inf.json b/tests/std/JSONParsingTestSuite/n_number_Inf.json new file mode 100755 index 000000000..c40c734c3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_Inf.json @@ -0,0 +1 @@ +[Inf] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_NaN.json b/tests/std/JSONParsingTestSuite/n_number_NaN.json new file mode 100755 index 000000000..499231790 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_NaN.json @@ -0,0 +1 @@ +[NaN] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json b/tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json new file mode 100644 index 000000000..b14587e5e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json @@ -0,0 +1 @@ +[1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_expression.json b/tests/std/JSONParsingTestSuite/n_number_expression.json new file mode 100644 index 000000000..76fdbc8a4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_expression.json @@ -0,0 +1 @@ +[1+2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json b/tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json new file mode 100644 index 000000000..3b214880c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json @@ -0,0 +1 @@ +[0x1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json b/tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json new file mode 100644 index 000000000..83e516ab0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json @@ -0,0 +1 @@ +[0x42] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_infinity.json b/tests/std/JSONParsingTestSuite/n_number_infinity.json new file mode 100755 index 000000000..8c2baf783 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_infinity.json @@ -0,0 +1 @@ +[Infinity] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid+-.json b/tests/std/JSONParsingTestSuite/n_number_invalid+-.json new file mode 100644 index 000000000..1cce602b5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid+-.json @@ -0,0 +1 @@ +[0e+-1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json b/tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json new file mode 100644 index 000000000..5fc3c1efb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json @@ -0,0 +1 @@ +[-123.123foo] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json new file mode 100644 index 000000000..3b97e580e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json @@ -0,0 +1 @@ +[123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json new file mode 100644 index 000000000..ea35d723c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json @@ -0,0 +1 @@ +[1e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json new file mode 100644 index 000000000..371226e4c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json @@ -0,0 +1 @@ +[0] diff --git a/tests/std/JSONParsingTestSuite/n_number_minus_infinity.json b/tests/std/JSONParsingTestSuite/n_number_minus_infinity.json new file mode 100755 index 000000000..cf4133d22 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_minus_infinity.json @@ -0,0 +1 @@ +[-Infinity] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json new file mode 100644 index 000000000..a6d8e78e7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json @@ -0,0 +1 @@ +[-foo] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_minus_space_1.json b/tests/std/JSONParsingTestSuite/n_number_minus_space_1.json new file mode 100644 index 000000000..9a5ebedf6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_minus_space_1.json @@ -0,0 +1 @@ +[- 1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json b/tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json new file mode 100644 index 000000000..67af0960a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json @@ -0,0 +1 @@ +[-012] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json b/tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json new file mode 100755 index 000000000..1f2a43496 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json @@ -0,0 +1 @@ +[-.123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json b/tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json new file mode 100644 index 000000000..2aa73119f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json @@ -0,0 +1 @@ +[-1x] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json b/tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json new file mode 100644 index 000000000..9213dfca8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json @@ -0,0 +1 @@ +[1ea] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json b/tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json new file mode 100644 index 000000000..1e52ef964 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json @@ -0,0 +1 @@ +[1e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json b/tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json new file mode 100755 index 000000000..1de287cf8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json @@ -0,0 +1 @@ +[1.] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json b/tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json new file mode 100755 index 000000000..f682dbdce --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json @@ -0,0 +1 @@ +[.123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_with_alpha.json b/tests/std/JSONParsingTestSuite/n_number_with_alpha.json new file mode 100644 index 000000000..1e42d8182 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_with_alpha.json @@ -0,0 +1 @@ +[1.2a-3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json b/tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json new file mode 100644 index 000000000..b79daccb8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json @@ -0,0 +1 @@ +[1.8011670033376514H-308] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json b/tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json new file mode 100755 index 000000000..7106da1f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json @@ -0,0 +1 @@ +[012] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_bad_value.json b/tests/std/JSONParsingTestSuite/n_object_bad_value.json new file mode 100644 index 000000000..a03a8c03b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_bad_value.json @@ -0,0 +1 @@ +["x", truth] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_bracket_key.json b/tests/std/JSONParsingTestSuite/n_object_bracket_key.json new file mode 100644 index 000000000..cc443b483 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_bracket_key.json @@ -0,0 +1 @@ +{[: "x"} diff --git a/tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json b/tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json new file mode 100644 index 000000000..8d5637708 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json @@ -0,0 +1 @@ +{"x", null} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_double_colon.json b/tests/std/JSONParsingTestSuite/n_object_double_colon.json new file mode 100644 index 000000000..80e8c7b89 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_double_colon.json @@ -0,0 +1 @@ +{"x"::"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_emoji.json b/tests/std/JSONParsingTestSuite/n_object_emoji.json new file mode 100644 index 000000000..cb4078eaa --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_emoji.json @@ -0,0 +1 @@ +{🇨🇭} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json b/tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json new file mode 100644 index 000000000..80c42cbad --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json @@ -0,0 +1 @@ +{"a":"a" 123} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json b/tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json new file mode 100755 index 000000000..77c327599 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json @@ -0,0 +1 @@ +{key: 'value'} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json b/tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json new file mode 100644 index 000000000..aa2cb637c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json @@ -0,0 +1 @@ +{"":"0",} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_colon.json b/tests/std/JSONParsingTestSuite/n_object_missing_colon.json new file mode 100644 index 000000000..b98eff62d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_colon.json @@ -0,0 +1 @@ +{"a" b} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_key.json b/tests/std/JSONParsingTestSuite/n_object_missing_key.json new file mode 100755 index 000000000..b4fb0f528 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_key.json @@ -0,0 +1 @@ +{:"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json b/tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json new file mode 100755 index 000000000..e3451384f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json @@ -0,0 +1 @@ +{"a" "b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_value.json b/tests/std/JSONParsingTestSuite/n_object_missing_value.json new file mode 100644 index 000000000..3ef538a60 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_value.json @@ -0,0 +1 @@ +{"a": \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_no-colon.json b/tests/std/JSONParsingTestSuite/n_object_no-colon.json new file mode 100644 index 000000000..f3797b357 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_no-colon.json @@ -0,0 +1 @@ +{"a" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_non_string_key.json b/tests/std/JSONParsingTestSuite/n_object_non_string_key.json new file mode 100755 index 000000000..b9945b34b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_non_string_key.json @@ -0,0 +1 @@ +{1:1} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json b/tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json new file mode 100755 index 000000000..b37fa86c0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json @@ -0,0 +1 @@ +{9999E9999:1} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json b/tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json new file mode 100755 index 000000000..f7d2959d0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json @@ -0,0 +1 @@ +{null:null,null:null} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json b/tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json new file mode 100755 index 000000000..3c9afe8dc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json @@ -0,0 +1 @@ +{"id":0,,,,,} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_single_quote.json b/tests/std/JSONParsingTestSuite/n_object_single_quote.json new file mode 100644 index 000000000..e5cdf976a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_single_quote.json @@ -0,0 +1 @@ +{'a':0} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comma.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comma.json new file mode 100755 index 000000000..a4b025094 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comma.json @@ -0,0 +1 @@ +{"id":0,} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment.json new file mode 100644 index 000000000..a372c6553 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment.json @@ -0,0 +1 @@ +{"a":"b"}/**/ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json new file mode 100644 index 000000000..d557f41ca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json @@ -0,0 +1 @@ +{"a":"b"}/**// \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json new file mode 100644 index 000000000..e335136c0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json @@ -0,0 +1 @@ +{"a":"b"}// \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json new file mode 100644 index 000000000..d892e49f1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json @@ -0,0 +1 @@ +{"a":"b"}/ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json b/tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json new file mode 100755 index 000000000..7c639ae64 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json @@ -0,0 +1 @@ +{"a":"b",,"c":"d"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_unquoted_key.json b/tests/std/JSONParsingTestSuite/n_object_unquoted_key.json new file mode 100644 index 000000000..8ba137293 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_unquoted_key.json @@ -0,0 +1 @@ +{a: "b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_unterminated-value.json b/tests/std/JSONParsingTestSuite/n_object_unterminated-value.json new file mode 100644 index 000000000..7fe699a6a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_unterminated-value.json @@ -0,0 +1 @@ +{"a":"a \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_with_single_string.json b/tests/std/JSONParsingTestSuite/n_object_with_single_string.json new file mode 100644 index 000000000..d63f7fbb7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_with_single_string.json @@ -0,0 +1 @@ +{ "foo" : "bar", "a" } \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json new file mode 100644 index 000000000..787c8f0a8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json @@ -0,0 +1 @@ +{"a":"b"}# \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_single_space.json b/tests/std/JSONParsingTestSuite/n_single_space.json new file mode 100755 index 000000000..0519ecba6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_single_space.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json new file mode 100644 index 000000000..acec66d8f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json @@ -0,0 +1 @@ +["\uD800\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json new file mode 100644 index 000000000..e834b05e9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json @@ -0,0 +1 @@ +["\uD800\u"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json new file mode 100644 index 000000000..a04cd3489 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json @@ -0,0 +1 @@ +["\uD800\u1"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json new file mode 100644 index 000000000..bfbd23409 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json @@ -0,0 +1 @@ +["\uD800\u1x"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json b/tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json new file mode 100644 index 000000000..fd6895693 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json @@ -0,0 +1 @@ +[é] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_backslash_00.json b/tests/std/JSONParsingTestSuite/n_string_backslash_00.json new file mode 100644 index 000000000..b5bf267b5 Binary files /dev/null and b/tests/std/JSONParsingTestSuite/n_string_backslash_00.json differ diff --git a/tests/std/JSONParsingTestSuite/n_string_escape_x.json b/tests/std/JSONParsingTestSuite/n_string_escape_x.json new file mode 100644 index 000000000..fae291938 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escape_x.json @@ -0,0 +1 @@ +["\x00"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json b/tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json new file mode 100755 index 000000000..016fcb47e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json @@ -0,0 +1 @@ +["\\\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json b/tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json new file mode 100644 index 000000000..f35ea382b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json @@ -0,0 +1 @@ +["\ "] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json b/tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json new file mode 100644 index 000000000..a27775421 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json @@ -0,0 +1 @@ +["\🌀"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json new file mode 100755 index 000000000..3415c33ca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json @@ -0,0 +1 @@ +["\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json new file mode 100755 index 000000000..0f2197ea2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json @@ -0,0 +1 @@ +["\u00A"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json new file mode 100755 index 000000000..75504a656 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json @@ -0,0 +1 @@ +["\uD834\uDd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json new file mode 100755 index 000000000..bd9656060 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json @@ -0,0 +1 @@ +["\uD800\uD800\x"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json b/tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json new file mode 100644 index 000000000..0c4300643 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json @@ -0,0 +1 @@ +["\u"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json b/tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json new file mode 100755 index 000000000..d1eb60921 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json @@ -0,0 +1 @@ +["\a"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json b/tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json new file mode 100644 index 000000000..7608cb6ba --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json @@ -0,0 +1 @@ +["\uqqqq"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json b/tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json new file mode 100644 index 000000000..2f757a25b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json @@ -0,0 +1 @@ +["\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json b/tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json new file mode 100755 index 000000000..7b297c636 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json @@ -0,0 +1 @@ +[\u0020"asd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json b/tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json new file mode 100644 index 000000000..01bc70aba --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json @@ -0,0 +1 @@ +[\n] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_single_doublequote.json b/tests/std/JSONParsingTestSuite/n_string_single_doublequote.json new file mode 100755 index 000000000..9d68933c4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_single_doublequote.json @@ -0,0 +1 @@ +" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_single_quote.json b/tests/std/JSONParsingTestSuite/n_string_single_quote.json new file mode 100644 index 000000000..caff239bf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_single_quote.json @@ -0,0 +1 @@ +['single quote'] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json b/tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json new file mode 100755 index 000000000..f2ba8f84a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json @@ -0,0 +1 @@ +abc \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json b/tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json new file mode 100644 index 000000000..db62a46fc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json @@ -0,0 +1 @@ +["\ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_unescaped_ctrl_char.json b/tests/std/JSONParsingTestSuite/n_string_unescaped_ctrl_char.json new file mode 100755 index 000000000..9f2134807 Binary files /dev/null and b/tests/std/JSONParsingTestSuite/n_string_unescaped_ctrl_char.json differ diff --git a/tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json b/tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json new file mode 100644 index 000000000..700d36086 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json @@ -0,0 +1,2 @@ +["new +line"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json b/tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json new file mode 100644 index 000000000..160264a2d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json @@ -0,0 +1 @@ +[" "] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json b/tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json new file mode 100644 index 000000000..17332bb17 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json @@ -0,0 +1 @@ +"\UA66D" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json new file mode 100644 index 000000000..efe3bd272 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json @@ -0,0 +1 @@ +""x \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json b/tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json new file mode 100644 index 000000000..a4823eecc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json b/tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json new file mode 100644 index 000000000..81156a699 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json @@ -0,0 +1 @@ +[⁠] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json b/tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json new file mode 100755 index 000000000..5f282702b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json new file mode 100755 index 000000000..a56fef0b0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json @@ -0,0 +1 @@ +<.> \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json new file mode 100755 index 000000000..617f26254 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json new file mode 100644 index 000000000..5a745e6f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json @@ -0,0 +1 @@ +[1]x \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json b/tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json new file mode 100755 index 000000000..6cfb1398d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json @@ -0,0 +1 @@ +[1]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json b/tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json new file mode 100755 index 000000000..ba6b1788b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json @@ -0,0 +1 @@ +["asd] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json b/tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json new file mode 100644 index 000000000..ef2ab62fe --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json @@ -0,0 +1 @@ +aå \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json b/tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json new file mode 100755 index 000000000..7cd88469a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json @@ -0,0 +1 @@ +[True] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json b/tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json new file mode 100755 index 000000000..d2af0c646 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json @@ -0,0 +1 @@ +1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json b/tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json new file mode 100644 index 000000000..ac61b8200 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json @@ -0,0 +1 @@ +{"x": true, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_double_array.json b/tests/std/JSONParsingTestSuite/n_structure_double_array.json new file mode 100755 index 000000000..058d1626e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_double_array.json @@ -0,0 +1 @@ +[][] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_end_array.json b/tests/std/JSONParsingTestSuite/n_structure_end_array.json new file mode 100644 index 000000000..54caf60b1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_end_array.json @@ -0,0 +1 @@ +] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json b/tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json new file mode 100755 index 000000000..bfcdd514f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json b/tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json new file mode 100644 index 000000000..8b1296cad --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json b/tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json new file mode 100644 index 000000000..8e2f0bef1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_no_data.json b/tests/std/JSONParsingTestSuite/n_structure_no_data.json new file mode 100644 index 000000000..e69de29bb diff --git a/tests/std/JSONParsingTestSuite/n_structure_null-byte-outside-string.json b/tests/std/JSONParsingTestSuite/n_structure_null-byte-outside-string.json new file mode 100644 index 000000000..326db1442 Binary files /dev/null and b/tests/std/JSONParsingTestSuite/n_structure_null-byte-outside-string.json differ diff --git a/tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json new file mode 100644 index 000000000..0746539d2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json @@ -0,0 +1 @@ +2@ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json b/tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json new file mode 100644 index 000000000..aa9ebaec5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json @@ -0,0 +1 @@ +{}} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json b/tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json new file mode 100644 index 000000000..17d045147 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json @@ -0,0 +1 @@ +{"": \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json b/tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json new file mode 100644 index 000000000..ed1b569b7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json @@ -0,0 +1 @@ +{"a":/*comment*/"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json new file mode 100644 index 000000000..9ca2336d7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json @@ -0,0 +1 @@ +{"a": true} "x" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json new file mode 100644 index 000000000..8bebe3af0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json @@ -0,0 +1 @@ +[' \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json new file mode 100644 index 000000000..6295fdc36 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json @@ -0,0 +1 @@ +[, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_object.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_object.json new file mode 100644 index 000000000..e870445b2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_object.json @@ -0,0 +1 @@ +[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"": diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json new file mode 100644 index 000000000..7a63c8c57 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json @@ -0,0 +1 @@ +[{ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json new file mode 100644 index 000000000..9822a6baf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json @@ -0,0 +1 @@ +["a \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_string.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_string.json new file mode 100644 index 000000000..42a619362 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_string.json @@ -0,0 +1 @@ +["a" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object.json b/tests/std/JSONParsingTestSuite/n_structure_open_object.json new file mode 100644 index 000000000..81750b96f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json new file mode 100755 index 000000000..eebc700a1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json @@ -0,0 +1 @@ +{] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json new file mode 100644 index 000000000..47bc9106f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json @@ -0,0 +1 @@ +{, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json new file mode 100644 index 000000000..381ede5de --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json @@ -0,0 +1 @@ +{[ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json new file mode 100644 index 000000000..328c30cd7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json @@ -0,0 +1 @@ +{"a \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json new file mode 100644 index 000000000..9dba17090 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json @@ -0,0 +1 @@ +{'a' \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_open.json b/tests/std/JSONParsingTestSuite/n_structure_open_open.json new file mode 100644 index 000000000..841fd5f86 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_open.json @@ -0,0 +1 @@ +["\{["\{["\{["\{ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_single_eacute.json b/tests/std/JSONParsingTestSuite/n_structure_single_eacute.json new file mode 100644 index 000000000..92a39f398 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_single_eacute.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_single_star.json b/tests/std/JSONParsingTestSuite/n_structure_single_star.json new file mode 100755 index 000000000..f59ec20aa --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_single_star.json @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_trailing_#.json b/tests/std/JSONParsingTestSuite/n_structure_trailing_#.json new file mode 100644 index 000000000..898611087 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_trailing_#.json @@ -0,0 +1 @@ +{"a":"b"}#{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json b/tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json new file mode 100755 index 000000000..df2f0f242 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json @@ -0,0 +1 @@ +[\u000A""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json new file mode 100755 index 000000000..11209515c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json @@ -0,0 +1 @@ +[1 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json new file mode 100644 index 000000000..0d591762c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json @@ -0,0 +1 @@ +[ false, nul \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json new file mode 100644 index 000000000..a2ff8504a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json @@ -0,0 +1 @@ +[ true, fals \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json new file mode 100644 index 000000000..3149e8f5a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json @@ -0,0 +1 @@ +[ false, tru \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json new file mode 100755 index 000000000..694d69dbd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json @@ -0,0 +1 @@ +{"asd":"asd" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json b/tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json new file mode 100644 index 000000000..7284aea33 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json @@ -0,0 +1 @@ +å \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json b/tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json new file mode 100755 index 000000000..81156a699 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json @@ -0,0 +1 @@ +[⁠] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json b/tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json new file mode 100755 index 000000000..a9ea535d1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json b/tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json new file mode 100755 index 000000000..582290798 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json @@ -0,0 +1 @@ +[[] ] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_empty-string.json b/tests/std/JSONParsingTestSuite/y_array_empty-string.json new file mode 100644 index 000000000..93b6be2bc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_empty-string.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_empty.json b/tests/std/JSONParsingTestSuite/y_array_empty.json new file mode 100755 index 000000000..0637a088a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json b/tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json new file mode 100755 index 000000000..eac5f7b46 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json @@ -0,0 +1 @@ +["a"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_false.json b/tests/std/JSONParsingTestSuite/y_array_false.json new file mode 100644 index 000000000..67b2f0760 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_false.json @@ -0,0 +1 @@ +[false] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_heterogeneous.json b/tests/std/JSONParsingTestSuite/y_array_heterogeneous.json new file mode 100755 index 000000000..d3c1e2648 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_heterogeneous.json @@ -0,0 +1 @@ +[null, 1, "1", {}] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_null.json b/tests/std/JSONParsingTestSuite/y_array_null.json new file mode 100644 index 000000000..500db4a86 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_null.json @@ -0,0 +1 @@ +[null] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json b/tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json new file mode 100644 index 000000000..994825500 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json @@ -0,0 +1,2 @@ +[1 +] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_leading_space.json b/tests/std/JSONParsingTestSuite/y_array_with_leading_space.json new file mode 100755 index 000000000..18bfe6422 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_leading_space.json @@ -0,0 +1 @@ + [1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_several_null.json b/tests/std/JSONParsingTestSuite/y_array_with_several_null.json new file mode 100755 index 000000000..99f6c5d1d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_several_null.json @@ -0,0 +1 @@ +[1,null,null,null,2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json b/tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json new file mode 100755 index 000000000..de9e7a944 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json @@ -0,0 +1 @@ +[2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number.json b/tests/std/JSONParsingTestSuite/y_number.json new file mode 100644 index 000000000..e5f5cc334 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number.json @@ -0,0 +1 @@ +[123e65] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_0e+1.json b/tests/std/JSONParsingTestSuite/y_number_0e+1.json new file mode 100755 index 000000000..d1d396706 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_0e+1.json @@ -0,0 +1 @@ +[0e+1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_0e1.json b/tests/std/JSONParsingTestSuite/y_number_0e1.json new file mode 100755 index 000000000..3283a7936 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_0e1.json @@ -0,0 +1 @@ +[0e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_after_space.json b/tests/std/JSONParsingTestSuite/y_number_after_space.json new file mode 100644 index 000000000..623570d96 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_after_space.json @@ -0,0 +1 @@ +[ 4] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json b/tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json new file mode 100755 index 000000000..96555ff78 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json @@ -0,0 +1 @@ +[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001] diff --git a/tests/std/JSONParsingTestSuite/y_number_int_with_exp.json b/tests/std/JSONParsingTestSuite/y_number_int_with_exp.json new file mode 100755 index 000000000..a4ca9e754 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_int_with_exp.json @@ -0,0 +1 @@ +[20e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_minus_zero.json b/tests/std/JSONParsingTestSuite/y_number_minus_zero.json new file mode 100755 index 000000000..37af1312a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_minus_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_negative_int.json b/tests/std/JSONParsingTestSuite/y_number_negative_int.json new file mode 100644 index 000000000..8e30f8bd9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_negative_int.json @@ -0,0 +1 @@ +[-123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_negative_one.json b/tests/std/JSONParsingTestSuite/y_number_negative_one.json new file mode 100644 index 000000000..99d21a2a0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_negative_one.json @@ -0,0 +1 @@ +[-1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_negative_zero.json b/tests/std/JSONParsingTestSuite/y_number_negative_zero.json new file mode 100644 index 000000000..37af1312a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_negative_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_capital_e.json b/tests/std/JSONParsingTestSuite/y_number_real_capital_e.json new file mode 100644 index 000000000..6edbdfcb1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_capital_e.json @@ -0,0 +1 @@ +[1E22] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json new file mode 100644 index 000000000..0a01bd3ef --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json @@ -0,0 +1 @@ +[1E-2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json new file mode 100644 index 000000000..5a8fc0972 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json @@ -0,0 +1 @@ +[1E+2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_exponent.json b/tests/std/JSONParsingTestSuite/y_number_real_exponent.json new file mode 100644 index 000000000..da2522d61 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_exponent.json @@ -0,0 +1 @@ +[123e45] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json b/tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json new file mode 100644 index 000000000..3944a7a45 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json @@ -0,0 +1 @@ +[123.456e78] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json b/tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json new file mode 100644 index 000000000..ca40d3c25 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json @@ -0,0 +1 @@ +[1e-2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json b/tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json new file mode 100644 index 000000000..343601d51 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json @@ -0,0 +1 @@ +[1e+2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_simple_int.json b/tests/std/JSONParsingTestSuite/y_number_simple_int.json new file mode 100644 index 000000000..e47f69afc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_simple_int.json @@ -0,0 +1 @@ +[123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_simple_real.json b/tests/std/JSONParsingTestSuite/y_number_simple_real.json new file mode 100644 index 000000000..b02878e5f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_simple_real.json @@ -0,0 +1 @@ +[123.456789] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object.json b/tests/std/JSONParsingTestSuite/y_object.json new file mode 100755 index 000000000..78262eda3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object.json @@ -0,0 +1 @@ +{"asd":"sdf", "dfg":"fgh"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_basic.json b/tests/std/JSONParsingTestSuite/y_object_basic.json new file mode 100755 index 000000000..646bbe7bb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_basic.json @@ -0,0 +1 @@ +{"asd":"sdf"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_duplicated_key.json b/tests/std/JSONParsingTestSuite/y_object_duplicated_key.json new file mode 100755 index 000000000..bbc2e1ce4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_duplicated_key.json @@ -0,0 +1 @@ +{"a":"b","a":"c"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json b/tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json new file mode 100755 index 000000000..211581c20 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json @@ -0,0 +1 @@ +{"a":"b","a":"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_empty.json b/tests/std/JSONParsingTestSuite/y_object_empty.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_empty_key.json b/tests/std/JSONParsingTestSuite/y_object_empty_key.json new file mode 100755 index 000000000..c0013d3b8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_empty_key.json @@ -0,0 +1 @@ +{"":0} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json b/tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json new file mode 100644 index 000000000..593f0f67f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json @@ -0,0 +1 @@ +{"foo\u0000bar": 42} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json b/tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json new file mode 100644 index 000000000..a0d3531c3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json @@ -0,0 +1 @@ +{ "min": -1.0e+28, "max": 1.0e+28 } \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_long_strings.json b/tests/std/JSONParsingTestSuite/y_object_long_strings.json new file mode 100644 index 000000000..bdc4a0871 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_long_strings.json @@ -0,0 +1 @@ +{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_simple.json b/tests/std/JSONParsingTestSuite/y_object_simple.json new file mode 100644 index 000000000..dacac917f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_simple.json @@ -0,0 +1 @@ +{"a":[]} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_string_unicode.json b/tests/std/JSONParsingTestSuite/y_object_string_unicode.json new file mode 100644 index 000000000..8effdb297 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_string_unicode.json @@ -0,0 +1 @@ +{"title":"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430" } \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_with_newlines.json b/tests/std/JSONParsingTestSuite/y_object_with_newlines.json new file mode 100644 index 000000000..246ec6b34 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_with_newlines.json @@ -0,0 +1,3 @@ +{ +"a": "b" +} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json b/tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json new file mode 100755 index 000000000..9967ddeb8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json @@ -0,0 +1 @@ +["\u0060\u012a\u12AB"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json new file mode 100755 index 000000000..996875cc8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json @@ -0,0 +1 @@ +["\uD801\udc37"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json new file mode 100755 index 000000000..3401021ec --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json @@ -0,0 +1 @@ +["\ud83d\ude39\ud83d\udc8d"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json b/tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json new file mode 100644 index 000000000..7f495532f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json @@ -0,0 +1 @@ +["\"\\\/\b\f\n\r\t"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json b/tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json new file mode 100755 index 000000000..d4439eda7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json @@ -0,0 +1 @@ +["\\u0000"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json b/tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json new file mode 100644 index 000000000..ae03243b6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json @@ -0,0 +1 @@ +["\""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_comments.json b/tests/std/JSONParsingTestSuite/y_string_comments.json new file mode 100644 index 000000000..2260c20c2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_comments.json @@ -0,0 +1 @@ +["a/*b*/c/*d//e"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_double_escape_a.json b/tests/std/JSONParsingTestSuite/y_string_double_escape_a.json new file mode 100644 index 000000000..6715d6f40 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_double_escape_a.json @@ -0,0 +1 @@ +["\\a"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_double_escape_n.json b/tests/std/JSONParsingTestSuite/y_string_double_escape_n.json new file mode 100644 index 000000000..44ca56c4d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_double_escape_n.json @@ -0,0 +1 @@ +["\\n"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json b/tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json new file mode 100644 index 000000000..5b014a9c2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json @@ -0,0 +1 @@ +["\u0012"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json b/tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json new file mode 100755 index 000000000..2ff52e2c5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json @@ -0,0 +1 @@ +["\uFFFF"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_in_array.json b/tests/std/JSONParsingTestSuite/y_string_in_array.json new file mode 100755 index 000000000..21d7ae4cd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_in_array.json @@ -0,0 +1 @@ +["asd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json b/tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json new file mode 100755 index 000000000..9e1887c1e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json @@ -0,0 +1 @@ +[ "asd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json b/tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json new file mode 100644 index 000000000..3919cef76 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json @@ -0,0 +1 @@ +["\uDBFF\uDFFF"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json b/tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json new file mode 100644 index 000000000..2085ab1a1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json @@ -0,0 +1 @@ +["new\u00A0line"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json new file mode 100755 index 000000000..059e4d9dd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json @@ -0,0 +1 @@ +["􏿿"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json new file mode 100755 index 000000000..4c913bd41 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json @@ -0,0 +1 @@ +["￿"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_null_escape.json b/tests/std/JSONParsingTestSuite/y_string_null_escape.json new file mode 100644 index 000000000..c1ad84404 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_null_escape.json @@ -0,0 +1 @@ +["\u0000"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json b/tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json new file mode 100644 index 000000000..157185923 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json @@ -0,0 +1 @@ +["\u002c"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_pi.json b/tests/std/JSONParsingTestSuite/y_string_pi.json new file mode 100644 index 000000000..9df11ae88 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_pi.json @@ -0,0 +1 @@ +["π"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json b/tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json new file mode 100755 index 000000000..10a33a171 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json @@ -0,0 +1 @@ +["𛿿"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_simple_ascii.json b/tests/std/JSONParsingTestSuite/y_string_simple_ascii.json new file mode 100644 index 000000000..8cadf7d05 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_simple_ascii.json @@ -0,0 +1 @@ +["asd "] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_space.json b/tests/std/JSONParsingTestSuite/y_string_space.json new file mode 100644 index 000000000..efd782cc3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_space.json @@ -0,0 +1 @@ +" " \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json b/tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json new file mode 100755 index 000000000..7620b6655 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json @@ -0,0 +1 @@ +["\uD834\uDd1e"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json b/tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json new file mode 100644 index 000000000..108f1d67d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json @@ -0,0 +1 @@ +["\u0821"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json b/tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json new file mode 100644 index 000000000..461503c31 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json @@ -0,0 +1 @@ +["\u0123"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json b/tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json new file mode 100755 index 000000000..897b6021a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json b/tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json new file mode 100755 index 000000000..8cd998c89 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_uEscape.json b/tests/std/JSONParsingTestSuite/y_string_uEscape.json new file mode 100755 index 000000000..f7b41a02f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_uEscape.json @@ -0,0 +1 @@ +["\u0061\u30af\u30EA\u30b9"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json b/tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json new file mode 100644 index 000000000..3a5a220b6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json @@ -0,0 +1 @@ +["new\u000Aline"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json b/tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json new file mode 100755 index 000000000..7d064f498 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode.json b/tests/std/JSONParsingTestSuite/y_string_unicode.json new file mode 100644 index 000000000..3598095b7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode.json @@ -0,0 +1 @@ +["\uA66D"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json b/tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json new file mode 100755 index 000000000..0bb3b51e7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json @@ -0,0 +1 @@ +["\u005C"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_2.json b/tests/std/JSONParsingTestSuite/y_string_unicode_2.json new file mode 100644 index 000000000..a7dcb9768 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_2.json @@ -0,0 +1 @@ +["⍂㈴⍂"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json new file mode 100644 index 000000000..9a8370b96 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json @@ -0,0 +1 @@ +["\uDBFF\uDFFE"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json new file mode 100644 index 000000000..c51f8ae45 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json @@ -0,0 +1 @@ +["\uD83F\uDFFE"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json new file mode 100644 index 000000000..626d5f815 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json @@ -0,0 +1 @@ +["\u200B"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json new file mode 100644 index 000000000..1e23972c6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json @@ -0,0 +1 @@ +["\u2064"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json new file mode 100644 index 000000000..18ef151b4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json @@ -0,0 +1 @@ +["\uFDD0"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json new file mode 100644 index 000000000..13d261fda --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json @@ -0,0 +1 @@ +["\uFFFE"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json b/tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json new file mode 100755 index 000000000..4e6257856 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json @@ -0,0 +1 @@ +["\u0022"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_utf8.json b/tests/std/JSONParsingTestSuite/y_string_utf8.json new file mode 100644 index 000000000..40878435f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_utf8.json @@ -0,0 +1 @@ +["€𝄞"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_with_del_character.json b/tests/std/JSONParsingTestSuite/y_string_with_del_character.json new file mode 100755 index 000000000..8bd24907d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_with_del_character.json @@ -0,0 +1 @@ +["aa"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_false.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_false.json new file mode 100644 index 000000000..02e4a84d6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_false.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_int.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_int.json new file mode 100755 index 000000000..f70d7bba4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_int.json @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json new file mode 100755 index 000000000..b5135a207 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json @@ -0,0 +1 @@ +-0.1 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_null.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_null.json new file mode 100644 index 000000000..ec747fa47 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_null.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_string.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_string.json new file mode 100755 index 000000000..b6e982ca9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_string.json @@ -0,0 +1 @@ +"asd" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_true.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_true.json new file mode 100755 index 000000000..f32a5804e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_true.json @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_string_empty.json b/tests/std/JSONParsingTestSuite/y_structure_string_empty.json new file mode 100644 index 000000000..3cc762b55 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_string_empty.json @@ -0,0 +1 @@ +"" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json b/tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json new file mode 100644 index 000000000..0c3426d4c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json @@ -0,0 +1 @@ +["a"] diff --git a/tests/std/JSONParsingTestSuite/y_structure_true_in_array.json b/tests/std/JSONParsingTestSuite/y_structure_true_in_array.json new file mode 100644 index 000000000..de601e305 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_true_in_array.json @@ -0,0 +1 @@ +[true] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json b/tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json new file mode 100644 index 000000000..2bedf7f2d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json @@ -0,0 +1 @@ + [] \ No newline at end of file diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau new file mode 100644 index 000000000..d9d29d8de --- /dev/null +++ b/tests/std/fs.test.luau @@ -0,0 +1,263 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") +local task = require("@lute/task") + +local tmpdir = system.tmpdir() + +test.suite("FsSuite", function(suite) + suite:case("open_read_write_close_and_stat", function(assert) + local file = path.join(tmpdir, "file.txt") + + local h = fs.open(file, "w+") + assert.eq(fs.exists(file), true) + fs.write(h, "abc") + fs.close(h) + + local m = fs.metadata(file) + assert.eq(m.size, 3) + + local hr = fs.open(file, "r") + assert.eq(fs.read(hr), "abc") + fs.close(hr) + + fs.remove(file) + end) + + suite:case("writestring_and_readfile", function(assert) + local file = path.join(tmpdir, "hello.txt") + fs.writestringtofile(file, "hello lute") + + local contents = fs.readfiletostring(file) + assert.eq(contents, "hello lute") + + fs.remove(file) + end) + + suite:case("exists_and_type_file_and_dir", function(assert) + local file = path.join(tmpdir, "roblox.txt") + fs.writestringtofile(file, "roblox") + + assert.eq(fs.exists(file), true) + assert.eq(fs.type(file), "file") + assert.eq(fs.exists(tmpdir), true) + assert.eq(fs.type(tmpdir), "dir") + + fs.remove(file) + end) + + suite:case("mkdir_listdir_rmdir", function(assert) + local dir = path.join(tmpdir, "subdir") + if not fs.exists(dir) then + fs.createdirectory(dir, { makeparents = true }) + end + + assert.eq(fs.exists(dir), true) + assert.eq(fs.type(dir), "dir") + + local entries = fs.listdirectory(tmpdir) + local found = false + for _, e in entries do + if e.name == "subdir" then + found = true + end + end + assert.eq(found, true) + fs.removedirectory(dir) + end) + + suite:case("link_and_symlink_and_copy", function(assert) + local srcfile = "src.txt" + local src = path.join(tmpdir, srcfile) + fs.writestringtofile(src, "linkcontent") + + local dstlink = path.join(tmpdir, "dstlink") + fs.link(src, dstlink) + assert.eq(fs.readfiletostring(dstlink), "linkcontent") + fs.remove(dstlink) + + local dstcopy = path.join(tmpdir, "dstcopy.txt") + fs.copy(src, dstcopy) + assert.eq(fs.readfiletostring(dstcopy), "linkcontent") + fs.remove(dstcopy) + + local dstsymlink = path.join(tmpdir, "dstsymlink") + fs.symboliclink(srcfile, dstsymlink) + assert.eq(fs.readfiletostring(dstsymlink), "linkcontent") + fs.remove(dstsymlink) + + fs.remove(src) + end) + + suite:case("watch_iterator_on_change", function(assert) + local watchedFileName = "watched.txt" + local watched = path.join(tmpdir, watchedFileName) + + local watcher = fs.watch(tmpdir) + + fs.writestringtofile(watched, "hi") + + -- Note: we had a timeout for this loop before, but it was leading to a flaky test where event = nil, so we're removing it. + -- If this hangs indefinitely at, we'll see this test fail later and revisit. + local start = os.clock() + local event + repeat + event = watcher:next() + if not event then + task.wait(0.01) + end + until event + + print("Watch iterator time taken: " .. (os.clock() - start)) + assert.neq(event, nil) + assert.eq(event.change or event.rename, true) + + watcher:close() + if fs.exists(watched) then + fs.remove(watched) + end + end) + + suite:case("createdirectory_makeparents_true", function(assert) + local nestedDir = path.join(tmpdir, "nested", "directory") + fs.createdirectory(nestedDir, { makeparents = true }) + assert.eq(fs.exists(nestedDir), true) + + fs.removedirectory(nestedDir) + fs.removedirectory(path.join(tmpdir, "nested")) + end) + + suite:case("createdirectory_makeparents_false", function(assert) + local nestedDir = path.join(tmpdir, "this", "should", "not", "work") + local success, err = pcall(function() + fs.createdirectory(nestedDir, { makeparents = false }) + end) + assert.neq(success, true) + assert.neq(err, nil) + end) + + suite:case("createdirectory_no_options", function(assert) + local nestedDir = path.join(tmpdir, "this", "should", "not", "work") + local success, err = pcall(function() + fs.createdirectory(nestedDir) + end) + assert.neq(success, true) + assert.neq(err, nil) + end) + + suite:case("fs_open_optional_mode", function(assert) + local file = path.join(tmpdir, "optional_mode.txt") + + local h1 = fs.open(file, "w+") + fs.write(h1, "created file") + fs.close(h1) + + local h2 = fs.open(file) -- with no mode specified, should default to "r" + local contents = fs.read(h2) + assert.eq(contents, "created file") + fs.close(h2) + + fs.remove(file) + end) + + suite:case("removedirectory_non_recursive", function(assert) + local dir = path.join(tmpdir, "empty_dir") + fs.createdirectory(dir, { makeparents = true }) + assert.eq(fs.exists(dir), true) + + fs.removedirectory(dir) + assert.eq(fs.exists(dir), false) + end) + + suite:case("removedirectory_recursive", function(assert) + local dir = path.join(tmpdir, "recursive_test") + local subdir1 = path.join(dir, "subdir1") + local subdir2 = path.join(dir, "subdir2") + local nestedDir = path.join(subdir1, "nested") + + fs.createdirectory(nestedDir, { makeparents = true }) + fs.createdirectory(subdir2, { makeparents = true }) + + local file1 = path.join(dir, "file1.txt") + local file2 = path.join(subdir1, "file2.txt") + local file3 = path.join(nestedDir, "file3.txt") + + fs.writestringtofile(file1, "content1") + fs.writestringtofile(file2, "content2") + fs.writestringtofile(file3, "content3") + + assert.eq(fs.exists(dir), true) + assert.eq(fs.exists(file1), true) + assert.eq(fs.exists(file2), true) + assert.eq(fs.exists(file3), true) + + fs.removedirectory(dir, { recursive = true }) + + assert.eq(fs.exists(dir), false) + assert.eq(fs.exists(file1), false) + assert.eq(fs.exists(file2), false) + assert.eq(fs.exists(file3), false) + end) + + suite:case("removedirectory_recursive_false_with_contents", function(assert) + local dir = path.join(tmpdir, "non_empty_dir") + fs.createdirectory(dir, { makeparents = true }) + + local file = path.join(dir, "file.txt") + fs.writestringtofile(file, "content") + + local success, err = pcall(function() + fs.removedirectory(dir, { recursive = false }) + end) + + assert.neq(success, true) + assert.neq(err, nil) + + -- Cleanup + fs.remove(file) + fs.removedirectory(dir) + end) + + suite:case("walk_directory_recursive", function(assert) + local baseDir = path.join(tmpdir, "walktest") + local subDir = path.join(baseDir, "subdir") + fs.createdirectory(subDir, { makeparents = true }) + + local file1 = path.join(baseDir, "file1.txt") + local f1 = fs.open(file1, "w+") + fs.close(f1) + + local file2 = path.join(subDir, "file2.txt") + local f2 = fs.open(file2, "w+") + fs.close(f2) + + local expectedPaths = { + tostring(baseDir), + tostring(file1), + tostring(subDir), + tostring(file2), + } + local foundFiles = {} + + local it = fs.walk(baseDir, { recursive = true }) + local p = it() + while p do + table.insert(foundFiles, tostring(p)) + p = it() + end + + for _, expected in expectedPaths do + local found = false + for _, foundPath in foundFiles do + if foundPath == expected then + found = true + break + end + end + assert.eq(found, true) + end + + fs.removedirectory(baseDir, { recursive = true }) + end) +end) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau new file mode 100644 index 000000000..cf3dd7260 --- /dev/null +++ b/tests/std/json.test.luau @@ -0,0 +1,87 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local test = require("@std/test") +local json = require("@std/json") +local stringext = require("@std/stringext") + +local suiteDir = "tests/std/JSONParsingTestSuite" + +test.suite("JSONParsingTestSuite", function(suite) + for _, entry in fs.listdirectory(suiteDir) do + if entry.type ~= "file" then + continue + end + + local name = entry.name + + suite:case(name, function(assert) + local p = path.join(suiteDir, name) + local src = fs.readfiletostring(path.format(p)) + + -- attempt to parse .json and print the error if it fails + local ok, parsed = pcall(function() + return json.deserialize(src) + end) + + if stringext.hasprefix(name, "y_") then + -- valid JSON: must parse + assert.eq(ok, true) + + -- serialize the parsed value and ensure it parses again + local _ok2, _ser = pcall(function() + return json.serialize(parsed) + end) + assert.eq(_ok2, true) + + local _ok3 = pcall(function() + json.deserialize(_ser) + end) + assert.eq(_ok3, true) + elseif stringext.hasprefix(name, "n_") then + -- invalid JSON: must fail to parse + assert.eq(ok, false) + else + -- indeterminate (i_*) or other: don't assert pass/fail, but ensure call completed + assert.eq(type(ok), "boolean") + end + end) + end + + suite:case("json_object_and_asobject", function(assert) + local obj = json.object({ + name = "Alice", + age = 30, + }) + assert.eq(type(obj), "table") + assert.eq(obj.name, "Alice") + assert.eq(obj.age, 30) + end) + + suite:case("json_asobject_test", function(assert) + local myjsonstr = [[ + { + "a": true, + "b": { + "c": true, + "d": [1, 2, 3] + } + } + ]] + + local maybeJson = json.deserialize(myjsonstr) + local obj = json.asobject(maybeJson) + + if obj then + assert.eq(type(obj), "table") + assert.eq(obj.a, true) + assert.eq(type(obj.b), "table") + if json.asobject(obj.b) then + assert.eq(obj.b.c, true) + assert.eq(type(obj.b.d), "table") + for i = 1, #obj.b.d do + assert.eq(type(obj.b.d[i]), "number") + end + end + end + end) +end) diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau new file mode 100644 index 000000000..0d542cdcc --- /dev/null +++ b/tests/std/luau.test.luau @@ -0,0 +1,47 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local tmpDir = system.tmpdir() + +test.suite("LuauTypeOfModuleSuite", function(suite) + suite:case("typeofmodule_returns_as_string_no_require", function(assert) + local testPath = path.join(tmpDir, "typeofmodule_test.luau") + local testFile = fs.open(testPath, "w+") + fs.write( + testFile, + [[ + local function example_string(): string + return "example" + end + + return { + example_string = example_string, + } + ]] + ) + fs.close(testFile) + + local moduleTypeString = luau.typeofmodule(testPath) + local expectedOutputString = [[{ + example_string: () -> string +}]] + + -- Basic check for now until the data structure is created + assert.eq(moduleTypeString, expectedOutputString) + end) + + suite:case("resolverequire", function(assert) + local testPath = path.join(tmpDir, "resolverequire_test.luau") + local testFile = fs.open(testPath, "w+") + + fs.write(testFile, 'print("Hello, World!")') + local resolvedPath = luau.resolverequire("@self", testPath) + fs.close(testFile) + + local expected = path.format(testPath):gsub("\\", "/") + assert.eq(resolvedPath, expected) + end) +end) diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau new file mode 100644 index 000000000..e1c5eacaf --- /dev/null +++ b/tests/std/net.test.luau @@ -0,0 +1,24 @@ +local net = require("@lute/net") +local test = require("@std/test") + +test.suite("NetTestSuite", function(suite) + suite:case("serve_locally_and_request", function(assert) + local server = net.serve({ + handler = function(req: net.ReceivedRequest): net.ServerResponse + return { + status = 200, + body = "Hello, World!", + } + end, + }) + + local response = net.request(`{server.hostname}:{server.port}`, { + method = "GET", + }) + assert.eq(true, response.ok) + assert.eq(200, response.status) + assert.eq("Hello, World!", response.body) + + server.close() + end) +end) diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau new file mode 100644 index 000000000..f5fcd5f61 --- /dev/null +++ b/tests/std/path/path.posix.test.luau @@ -0,0 +1,590 @@ +local system = require("@std/system") +local test = require("@std/test") + +local path = require("@std/path") +local posix = require("@std/path/posix") + +test.suite("PathPosixParseSuite", function(suite) + suite:case("parse_posix_absolute_path", function(assert) + local result = path.posix.parse("/home/user/documents/file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_posix_relative_path", function(assert) + local result = path.posix.parse("documents/file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("parse_empty_string", function(assert) + local result = path.posix.parse("") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("parse_root_path", function(assert) + local result = path.posix.parse("/") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 0) + end) + + suite:case("parse_single_file", function(assert) + local result = path.posix.parse("file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "file.txt") + end) + + suite:case("parse_pathobj_returns_same", function(assert) + local originalPath: posix.path = setmetatable({ + parts = { "folder", "file.txt" }, + absolute = false, + }, posix.pathmt) + local result = path.posix.parse(originalPath) + assert.eq(result, originalPath) + end) +end) + +test.suite("PathPosixBasenameSuite", function(suite) + suite:case("basename_posix_absolute_path", function(assert) + local result = path.posix.basename("/home/user/documents/file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_posix_relative_path", function(assert) + local result = path.posix.basename("documents/file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_single_file", function(assert) + local result = path.posix.basename("file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_directory_no_extension", function(assert) + local result = path.posix.basename("/home/user/documents") + assert.eq(result, "documents") + end) + + suite:case("basename_empty_path", function(assert) + local result = path.posix.basename("") + assert.eq(result, nil) + end) + + suite:case("basename_root_path", function(assert) + local result = path.posix.basename("/") + assert.eq(result, nil) + end) + + suite:case("basename_pathobj", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "folder", "subfolder", "file.txt" }, + absolute = false, + }, posix.pathmt) + local result = path.posix.basename(pathobj) + assert.eq(result, "file.txt") + end) +end) + +test.suite("PathPosixFormatSuite", function(suite) + suite:case("format_posix_absolute_path", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "home", "user", "documents", "file.txt" }, + absolute = true, + }, posix.pathmt) + local result = path.posix.format(pathobj) + assert.eq(result, "/home/user/documents/file.txt") + end) + + suite:case("format_posix_relative_path", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "documents", "file.txt" }, + absolute = false, + }, posix.pathmt) + local result = path.posix.format(pathobj) + assert.eq(result, "documents/file.txt") + end) + + suite:case("format_empty_relative_path", function(assert) + local pathobj: posix.path = setmetatable({ + parts = {}, + absolute = false, + }, posix.pathmt) + local result = path.posix.format(pathobj) + assert.eq(result, ".") + end) + + suite:case("format_root_path", function(assert) + local pathobj: posix.path = setmetatable({ + parts = {}, + absolute = true, + }, posix.pathmt) + local result = path.posix.format(pathobj) + assert.eq(result, "/") + end) + + suite:case("format_string_returns_same", function(assert) + local result = path.posix.format("/home/user/file.txt") + assert.eq(result, "/home/user/file.txt") + end) +end) + +test.suite("PathPosixDirnameSuite", function(suite) + suite:case("dirname_posix_absolute_path", function(assert) + local result = path.posix.dirname("/home/user/documents/file.txt") + assert.eq(result, "/home/user/documents") + end) + + suite:case("dirname_posix_relative_path", function(assert) + local result = path.posix.dirname("documents/file.txt") + assert.eq(result, "documents") + end) + + suite:case("dirname_single_file", function(assert) + local result = path.posix.dirname("file.txt") + assert.eq(result, ".") + end) + + suite:case("dirname_nested_directory", function(assert) + local result = path.posix.dirname("/home/user/documents/subfolder") + assert.eq(result, "/home/user/documents") + end) + + suite:case("dirname_empty_path", function(assert) + local result = path.posix.dirname("") + assert.eq(result, ".") + end) + + suite:case("dirname_root_path", function(assert) + local result = path.posix.dirname("/") + assert.eq(result, "/") + end) + + suite:case("dirname_pathobj", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "folder", "subfolder", "file.txt" }, + absolute = false, + }, posix.pathmt) + local result = path.posix.dirname(pathobj) + assert.eq(result, "folder/subfolder") + end) +end) + +test.suite("PathPosixExtnameSuite", function(suite) + suite:case("extname_simple_extension", function(assert) + local result = path.posix.extname("/home/user/file.txt") + assert.eq(result, ".txt") + end) + + suite:case("extname_multiple_dots", function(assert) + local result = path.posix.extname("/home/user/archive.tar.gz") + assert.eq(result, ".gz") + end) + + suite:case("extname_no_extension", function(assert) + local result = path.posix.extname("/home/user/README") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_extension", function(assert) + local result = path.posix.extname("/home/user/.bashrc") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_real_extension", function(assert) + local result = path.posix.extname("/home/user/.config.json") + assert.eq(result, ".json") + end) + + suite:case("extname_directory_path", function(assert) + local result = path.posix.extname("/home/user/documents") + assert.eq(result, "") + end) + + suite:case("extname_empty_path", function(assert) + local result = path.posix.extname("") + assert.eq(result, "") + end) + + suite:case("extname_root_path", function(assert) + local result = path.posix.extname("/") + assert.eq(result, "") + end) + + suite:case("extname_pathobj", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "folder", "file.pdf" }, + absolute = false, + }, posix.pathmt) + local result = path.posix.extname(pathobj) + assert.eq(result, ".pdf") + end) + + suite:case("extname_dot_only", function(assert) + local result = path.posix.extname("/home/user/file.") + assert.eq(result, ".") + end) + + suite:case("extname_filename_is_dot", function(assert) + local result = path.posix.extname("/home/user/.") + assert.eq(result, "") + end) + + suite:case("extname_filename_is_dotdot", function(assert) + local result = path.posix.extname("/home/user/..") + assert.eq(result, "") + end) +end) + +test.suite("PathPosixIsAbsoluteSuite", function(suite) + suite:case("absolute_posix_absolute_path", function(assert) + local result = path.posix.isabsolute("/home/user/documents/file.txt") + assert.eq(result, true) + end) + + suite:case("absolute_posix_relative_path", function(assert) + local result = path.posix.isabsolute("documents/file.txt") + assert.eq(result, false) + end) + + suite:case("absolute_root_path", function(assert) + local result = path.posix.isabsolute("/") + assert.eq(result, true) + end) + + suite:case("absolute_empty_path", function(assert) + local result = path.posix.isabsolute("") + assert.eq(result, false) + end) + + suite:case("absolute_pathobj_absolute", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "home", "user", "file.txt" }, + absolute = true, + }, posix.pathmt) + local result = path.posix.isabsolute(pathobj) + assert.eq(result, true) + end) + + suite:case("absolute_pathobj_relative", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "folder", "file.txt" }, + absolute = false, + }, posix.pathmt) + local result = path.posix.isabsolute(pathobj) + assert.eq(result, false) + end) +end) + +test.suite("PathPosixNormalizeSuite", function(suite) + suite:case("normalize_removes_current_directory", function(assert) + local result = path.posix.normalize("/home/./user/documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + end) + + suite:case("normalize_resolves_parent_directory", function(assert) + local result = path.posix.normalize("/home/user/../documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "documents") + end) + + suite:case("normalize_multiple_dots", function(assert) + local result = path.posix.normalize("/home/user/./documents/../files/./") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "files") + end) + + suite:case("normalize_relative_path", function(assert) + local result = path.posix.normalize("documents/./subfolder/../file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("normalize_removes_empty_segments", function(assert) + local result = path.posix.normalize("/home//user///documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + end) + + suite:case("normalize_parent_beyond_root", function(assert) + local result = path.posix.normalize("/home/../..") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_empty_path", function(assert) + local result = path.posix.normalize("") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_only_dots", function(assert) + local result = path.posix.normalize("./././.") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_pathobj", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "home", ".", "user", "..", "documents" }, + absolute = true, + }, posix.pathmt) + local result = path.posix.normalize(pathobj) + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "documents") + end) + + suite:case("normalize_parent_directory_only", function(assert) + local result = path.posix.normalize("..") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "..") + end) + + suite:case("normalize_multiple_parent_directories", function(assert) + local result = path.posix.normalize("../../..") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "..") + assert.eq(result.parts[3], "..") + end) + + suite:case("normalize_parent_then_folder", function(assert) + local result = path.posix.normalize("../folder/file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("normalize_multiple_parents_then_folder", function(assert) + local result = path.posix.normalize("../../folder/subfolder") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "..") + assert.eq(result.parts[3], "folder") + assert.eq(result.parts[4], "subfolder") + end) + + suite:case("normalize_folder_then_multiple_parents", function(assert) + local result = path.posix.normalize("folder/subfolder/../../other") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "other") + end) +end) + +test.suite("PathPosixJoinSuite", function(suite) + suite:case("join_absolute_and_relative", function(assert) + local result = path.posix.join("/home/user", "documents", "file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_relative_paths", function(assert) + local result = path.posix.join("documents", "subfolder", "file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "subfolder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("join_empty_parts", function(assert) + local result = path.posix.join("folder", "", "file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "folder") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("join_no_arguments", function(assert) + local result = path.posix.join() + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("join_single_argument", function(assert) + local result = path.posix.join("/home/user") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + end) + + suite:case("join_pathobj_and_string", function(assert) + local pathobj: posix.path = setmetatable({ + parts = { "home", "user" }, + absolute = true, + }, posix.pathmt) + local result = path.posix.join(pathobj, "documents/file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + assert.eq(#pathobj.parts, 2) -- original pathobj should be unchanged + assert.eq(pathobj.parts[1], "home") + assert.eq(pathobj.parts[2], "user") + assert.eq(pathobj.absolute, true) + end) + + suite:case("join_error_on_absolute_addend", function(assert) + local success, _ = pcall(function() + return path.posix.join("/home/user", "/absolute/path") + end) + assert.eq(success, false) + end) +end) + +test.suite("PathPosixResolveSuite", function(suite) + suite:case("resolve_absolute_path", function(assert) + local result = path.posix.resolve("/home/user/documents/file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("resolve_relative_path_from_cwd", function(assert) + -- This test assumes we're in some working directory + local result = path.posix.resolve("documents/file.txt") + -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / + assert.neq(result.absolute, system.win32) + -- The exact parts will depend on the current working directory + -- But we can check that it ends with our relative path + local endIndex = #result.parts + assert.eq(result.parts[endIndex - 1], "documents") + assert.eq(result.parts[endIndex], "file.txt") + end) + + suite:case("resolve_multiple_paths", function(assert) + local result = path.posix.resolve("/home", "user", "../admin", "documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "documents") + end) + + suite:case("resolve_with_absolute_in_middle", function(assert) + local result = path.posix.resolve("relative", "/absolute/path", "more") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "absolute") + assert.eq(result.parts[2], "path") + assert.eq(result.parts[3], "more") + end) + + suite:case("resolve_no_arguments", function(assert) + -- Should return normalized current working directory + local result = path.posix.resolve() + -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / + assert.neq(result.absolute, system.win32) + -- Can't test exact parts since cwd varies + end) + + suite:case("resolve_empty_strings", function(assert) + local result = path.posix.resolve("", "", "/home/user") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + end) + + suite:case("resolve_with_dot_segments", function(assert) + local result = path.posix.resolve("/home/user", "../admin/./documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "documents") + end) +end) + +test.suite("PathPosixToStringSuite", function(suite) + suite:case("toString_posix_absolute_path", function(assert) + local filePath = "/home/user/documents/file.txt" + local pathobj: posix.path = posix.parse(filePath) + local result = tostring(pathobj) + assert.eq(result, filePath) + end) + + suite:case("toString_posix_relative_path", function(assert) + local filePath = "documents/file.txt" + local pathobj: posix.path = posix.parse(filePath) + local result = tostring(pathobj) + assert.eq(result, filePath) + end) +end) + +test.suite("PathPosixRelativeSuite", function(suite) + suite:case("relative_same_path", function(assert) + local from = posix.parse("/home/user/documents") + local to = posix.parse("/home/user/documents") + local result = path.posix.relative(from, to) + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("relative_subdirectory", function(assert) + local from = posix.parse("/home/user") + local to = posix.parse("/home/user/documents/file.txt") + local result = path.posix.relative(from, to) + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("relative_parent_directory", function(assert) + local from = posix.parse("/home/user/documents") + local to = posix.parse("/home/user/file.txt") + local result = path.posix.relative(from, to) + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("relative_different_absolute_values", function(assert) + local from = posix.parse("/home/user/documents") + local to = posix.parse("home/user/file.txt") + assert.erroreq(function() + path.posix.relative(from, to) + end, "Cannot compute relative path between absolute and relative paths") + end) +end) diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau new file mode 100644 index 000000000..543fcdedf --- /dev/null +++ b/tests/std/path/path.win32.test.luau @@ -0,0 +1,819 @@ +local system = require("@std/system") +local test = require("@std/test") +local process = require("@std/process") + +local path = require("@std/path") +local win32 = require("@std/path/win32") + +test.suite("PathWin32ParseSuite", function(suite) + suite:case("parse_windows_absolute_path", function(assert) + local result: win32.path = path.win32.parse("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_windows_unc_path", function(assert) + local result: win32.path = path.win32.parse("\\\\server\\share\\folder\\file.txt") + assert.eq(result.kind, "unc") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "folder") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_empty_string", function(assert) + local result: win32.path = path.win32.parse("") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 0) + end) + + suite:case("parse_single_file", function(assert) + local result: win32.path = path.win32.parse("file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "file.txt") + end) + + suite:case("parse_pathobj_returns_same", function(assert) + local originalPath: win32.path = setmetatable({ + parts = { "folder", "file.txt" }, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.parse(originalPath) + assert.eq(result, originalPath) + end) + + suite:case("parse_mixed_separators", function(assert) + local result: win32.path = path.win32.parse("/home/user\\documents/file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_windows_relative_with_drive", function(assert) + local result: win32.path = path.win32.parse("C:Documents\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + end) +end) + +test.suite("PathWin32BasenameSuite", function(suite) + suite:case("basename_windows_absolute_path", function(assert) + local result = path.win32.basename("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_windows_unc_path", function(assert) + local result = path.win32.basename("\\\\server\\share\\folder\\file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_single_file", function(assert) + local result = path.win32.basename("file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_directory_no_extension", function(assert) + local result = path.win32.basename("C:\\Users\\username\\Documents") + assert.eq(result, "Documents") + end) + + suite:case("basename_empty_path", function(assert) + local result = path.win32.basename("") + assert.eq(result, nil) + end) + + suite:case("basename_root_path", function(assert) + local result = path.win32.basename("C:\\") + assert.eq(result, nil) + end) + + suite:case("basename_pathobj", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "folder", "subfolder", "file.txt" }, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.basename(pathobj) + assert.eq(result, "file.txt") + end) + + suite:case("basename_mixed_separators", function(assert) + local result = path.win32.basename("/home/user\\documents/file.txt") + assert.eq(result, "file.txt") + end) +end) + +test.suite("PathWin32FormatSuite", function(suite) + suite:case("format_windows_absolute_path", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "Users", "username", "Documents", "file.txt" }, + kind = "absolute", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.format(pathobj) + assert.eq(result, "C:\\Users\\username\\Documents\\file.txt") + end) + + suite:case("format_windows_relative_with_drive", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "Documents", "file.txt" }, + kind = "relative", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.format(pathobj) + assert.eq(result, "C:Documents\\file.txt") + end) + + suite:case("format_windows_unc_path", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "server", "share", "folder", "file.txt" }, + kind = "unc", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.format(pathobj) + assert.eq(result, "\\\\server\\share\\folder\\file.txt") + end) + + suite:case("format_empty_relative_path", function(assert) + local pathobj: win32.path = setmetatable({ + parts = {}, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.format(pathobj) + assert.eq(result, ".") + end) + + suite:case("format_empty_relative_path_with_drive", function(assert) + local pathobj: win32.path = setmetatable({ + parts = {}, + kind = "relative", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.format(pathobj) + assert.eq(result, "C:") + end) + + suite:case("format_root_path_with_drive", function(assert) + local pathobj: win32.path = setmetatable({ + parts = {}, + kind = "absolute", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.format(pathobj) + assert.eq(result, "C:\\") + end) + + suite:case("format_string_returns_same", function(assert) + local result = path.win32.format("/home/user/file.txt") + assert.eq(result, "/home/user/file.txt") + end) +end) + +test.suite("PathWin32DirnameSuite", function(suite) + suite:case("dirname_windows_absolute_path", function(assert) + local result = path.win32.dirname("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result, "C:\\Users\\username\\Documents") + end) + + suite:case("dirname_windows_unc_path", function(assert) + local result = path.win32.dirname("\\\\server\\share\\folder\\file.txt") + assert.eq(result, "\\\\server\\share\\folder") + end) + + suite:case("dirname_single_file", function(assert) + local result = path.win32.dirname("file.txt") + assert.eq(result, ".") + end) + + suite:case("dirname_empty_path", function(assert) + local result = path.win32.dirname("") + assert.eq(result, ".") + end) + + suite:case("dirname_windows_root", function(assert) + local result = path.win32.dirname("C:\\") + assert.eq(result, "C:\\") + end) + + suite:case("dirname_pathobj", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "folder", "subfolder", "file.txt" }, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.dirname(pathobj) + assert.eq(result, "folder\\subfolder") + end) + + suite:case("dirname_windows_relative_with_drive", function(assert) + local result = path.win32.dirname("C:Documents\\file.txt") + assert.eq(result, "C:Documents") + end) +end) + +test.suite("PathWin32ExtnameSuite", function(suite) + suite:case("extname_simple_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\file.txt") + assert.eq(result, ".txt") + end) + + suite:case("extname_multiple_dots", function(assert) + local result = path.win32.extname("C:\\Users\\username\\archive.tar.gz") + assert.eq(result, ".gz") + end) + + suite:case("extname_no_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\README") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\.bashrc") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_real_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\.config.json") + assert.eq(result, ".json") + end) + + suite:case("extname_windows_path", function(assert) + local result = path.win32.extname("C:\\Users\\username\\document.docx") + assert.eq(result, ".docx") + end) + + suite:case("extname_directory_path", function(assert) + local result = path.win32.extname("C:\\Users\\username\\documents") + assert.eq(result, "") + end) + + suite:case("extname_empty_path", function(assert) + local result = path.win32.extname("") + assert.eq(result, "") + end) + + suite:case("extname_root_path", function(assert) + local result = path.win32.extname("C:\\") + assert.eq(result, "") + end) + + suite:case("extname_pathobj", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "folder", "file.pdf" }, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.extname(pathobj) + assert.eq(result, ".pdf") + end) + + suite:case("extname_dot_only", function(assert) + local result = path.win32.extname("C:\\Users\\username\\file.") + assert.eq(result, ".") + end) + + suite:case("extname_filename_is_dot", function(assert) + local result = path.win32.extname("C:\\Users\\username\\.") + assert.eq(result, "") + end) + + suite:case("extname_filename_is_dotdot", function(assert) + local result = path.win32.extname("C:\\Users\\username\\..") + assert.eq(result, "") + end) +end) + +test.suite("PathWin32GetDriveSuite", function(suite) + suite:case("getdrive_absolute_path_string", function(assert) + local result = path.win32.drive("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_relative_path_with_drive", function(assert) + local result = path.win32.drive("D:Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "D") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_path_object", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "Users", "username", "Documents" }, + kind = "absolute", + drive = "E" :: string?, + }, win32.pathmt) + local result = path.win32.drive(pathobj) + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "E") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_root_path", function(assert) + local result = path.win32.drive("F:\\") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "F") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_error_on_empty_path", function(assert) + assert.errors(function() + path.win32.drive("") + end) + end) +end) + +test.suite("PathWin32IsAbsoluteSuite", function(suite) + suite:case("isAbsolute_windows_absolute_path", function(assert) + local result = path.win32.isabsolute("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result, true) + end) + + suite:case("isAbsolute_windows_relative_path", function(assert) + local result = path.win32.isabsolute("Documents\\file.txt") + assert.eq(result, false) + end) + + suite:case("isAbsolute_windows_relative_with_drive", function(assert) + local result = path.win32.isabsolute("C:Documents\\file.txt") + assert.eq(result, false) + end) + + suite:case("isAbsolute_windows_unc_path", function(assert) + local result = path.win32.isabsolute("\\\\server\\share\\folder\\file.txt") + assert.eq(result, true) + end) + + suite:case("isAbsolute_root_path", function(assert) + local result = path.win32.isabsolute("C:\\") + assert.eq(result, true) + end) + + suite:case("isAbsolute_empty_path", function(assert) + local result = path.win32.isabsolute("") + assert.eq(result, false) + end) + + suite:case("isAbsolute_pathobj_absolute", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "home", "user", "file.txt" }, + kind = "absolute", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.isabsolute(pathobj) + assert.eq(result, true) + end) + + suite:case("isAbsolute_pathobj_relative", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "folder", "file.txt" }, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) + local result = path.win32.isabsolute(pathobj) + assert.eq(result, false) + end) +end) + +test.suite("PathWin32NormalizeSuite", function(suite) + suite:case("normalize_removes_current_directory", function(assert) + local result = path.win32.normalize("C:\\Users\\.\\username\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("normalize_resolves_parent_directory", function(assert) + local result = path.win32.normalize("C:\\Users\\username\\..\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "Documents") + end) + + suite:case("normalize_multiple_dots", function(assert) + local result = path.win32.normalize("C:\\Users\\username\\.\\Documents\\..\\files\\.\\") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "files") + end) + + suite:case("normalize_relative_path", function(assert) + local result = path.win32.normalize("Documents\\.\\subfolder\\..\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("normalize_removes_empty_segments", function(assert) + local result = path.win32.normalize("C:\\Users\\\\username\\\\\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("normalize_parent_beyond_root", function(assert) + local result = path.win32.normalize("C:\\Users\\..\\..\\") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_unc_path", function(assert) + local result = path.win32.normalize("\\\\server\\share\\.\\folder\\..\\files") + assert.eq(result.kind, "unc") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "files") + end) + + suite:case("normalize_relative_with_drive", function(assert) + local result = path.win32.normalize("C:Documents\\.\\subfolder\\..\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("normalize_parent_directory_only", function(assert) + local result = path.win32.normalize("..") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "..") + end) + + suite:case("normalize_multiple_parent_directories", function(assert) + local result = path.win32.normalize("..\\..\\..") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "..") + assert.eq(result.parts[3], "..") + end) + + suite:case("normalize_parent_then_folder", function(assert) + local result = path.win32.normalize("..\\folder\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("normalize_folder_then_multiple_parents", function(assert) + local result = path.win32.normalize("folder\\subfolder\\..\\..\\other") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "other") + end) + + suite:case("normalize_pathobj", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "Users", ".", "username", "..", "Documents" }, + kind = "absolute", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.normalize(pathobj) + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "Documents") + end) + + suite:case("normalize_relative_with_drive_and_parent", function(assert) + local result = path.win32.normalize("C:..\\folder\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder") + assert.eq(result.parts[3], "file.txt") + end) +end) + +test.suite("PathWin32JoinSuite", function(suite) + suite:case("join_absolute_and_relative", function(assert) + local result = path.win32.join("C:\\Users\\username", "Documents", "file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_relative_paths", function(assert) + local result = path.win32.join("Documents", "subfolder", "file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "subfolder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("join_drive_relative_paths", function(assert) + local result = path.win32.join("C:Documents", "subfolder", "file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "subfolder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("join_unc_and_relative", function(assert) + local result = path.win32.join("\\\\server\\share", "folder", "file.txt") + assert.eq(result.kind, "unc") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "folder") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_no_arguments", function(assert) + local result = path.win32.join() + assert.eq(result.kind, "relative") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 0) + end) + + suite:case("join_pathobj_and_string", function(assert) + local pathobj: win32.path = setmetatable({ + parts = { "Users", "username" }, + kind = "absolute", + drive = "C" :: string?, + }, win32.pathmt) + local result = path.win32.join(pathobj, "Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + assert.eq(#pathobj.parts, 2) -- original pathobj should be unchanged + assert.eq(pathobj.parts[1], "Users") + assert.eq(pathobj.parts[2], "username") + assert.eq(pathobj.kind, "absolute") + assert.eq(pathobj.drive, "C") + end) + + suite:case("join_error_on_absolute_addend", function(assert) + local success, _ = pcall(function() + return path.win32.join("C:\\Users", "D:\\absolute\\path") + end) + assert.eq(success, false) + end) + + suite:case("join_error_on_unc_addend", function(assert) + local success, _ = pcall(function() + return path.win32.join("C:\\Users", "\\\\server\\share") + end) + assert.eq(success, false) + end) + + suite:case("join_error_on_incompatible_drives", function(assert) + local success, _ = pcall(function() + return path.win32.join("C:Documents", "D:other") + end) + assert.eq(success, false) + end) + + suite:case("join_compatible_drives", function(assert) + local result = path.win32.join("C:Documents", "C:subfolder") + assert.eq(result.kind, "relative") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "subfolder") + end) +end) + +test.suite("PathWin32ResolveSuite", function(suite) + suite:case("resolve_absolute_path", function(assert) + local result = path.win32.resolve("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("resolve_relative_path_from_cwd", function(assert) + -- This test assumes we're in some working directory + local result = path.win32.resolve("Documents\\file.txt") + -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters + assert.eq(result.kind, if system.win32 then "absolute" else "relative") + -- The exact parts will depend on the current working directory + -- But we can check that it ends with our relative path + -- In an ideal world, we'd mock `process.cwd` + local endIndex = #result.parts + assert.eq(result.parts[endIndex - 1], "Documents") + assert.eq(result.parts[endIndex], "file.txt") + end) + + suite:case("resolve_multiple_paths", function(assert) + local result = path.win32.resolve("C:\\Users", "username", "..\\admin", "Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("resolve_with_absolute_in_middle", function(assert) + local result = path.win32.resolve("relative", "D:\\absolute\\path", "more") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "D") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "absolute") + assert.eq(result.parts[2], "path") + assert.eq(result.parts[3], "more") + end) + + suite:case("resolve_unc_path", function(assert) + local result = path.win32.resolve("\\\\server\\share\\folder") + assert.eq(result.kind, "unc") + assert.eq(result.drive, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "folder") + end) + + suite:case("resolve_no_arguments", function(assert) + -- Should return normalized current working directory + local result = path.win32.resolve() + -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters + assert.eq(result.kind, if system.win32 then "absolute" else "relative") + -- Can't test exact parts since cwd varies + end) + + suite:case("resolve_drive_relative_path", function(assert) + -- Get the drive letter from the current working directory + local cwdPath = path.win32.parse(process.cwd() :: win32.pathlike) + local drive = if system.win32 then path.win32.drive(cwdPath).drive :: string else "C" + + local testPath = drive .. ":Documents\\file.txt" + local result = path.win32.resolve(testPath) + -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters + assert.eq(result.kind, if system.win32 then "absolute" else "relative") + assert.eq(result.drive, drive) + -- The exact parts depend on cwd, but should be absolute + local endIndex = #result.parts + assert.eq(result.parts[endIndex - 1], "Documents") + assert.eq(result.parts[endIndex], "file.txt") + end) + + suite:case("resolve_with_dot_segments", function(assert) + local result = path.win32.resolve("C:\\Users\\username", "..\\admin\\.\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("resolve_empty_strings", function(assert) + local result = path.win32.resolve("", "", "C:\\Users\\username") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + end) +end) + +test.suite("PathWin32ToStringSuite", function(suite) + suite:case("toString_windows_absolute_path", function(assert) + local filePath = "C:\\Users\\username\\Documents\\file.txt" + local pathObj: win32.path = win32.parse(filePath) + local result = tostring(pathObj) + assert.eq(result, filePath) + end) + + suite:case("toString_windows_relative_path", function(assert) + local filePath = "C:\\Users\\username\\Documents\\file.txt" + local pathObj: win32.path = path.win32.parse(filePath) + local result = tostring(pathObj) + assert.eq(result, filePath) + end) +end) + +test.suite("PathWin32RelativeSuite", function(suite) + suite:case("relative_same_path", function(assert) + local from = win32.parse("C:\\Users\\username\\Documents") + local to = win32.parse("C:\\Users\\username\\Documents") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 0) + assert.eq(result.drive, nil) + end) + + suite:case("relative_subdirectory", function(assert) + local from = win32.parse("C:\\Users\\username") + local to = win32.parse("C:\\Users\\username\\Documents\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + assert.eq(result.drive, nil) + end) + + suite:case("relative_parent_directory", function(assert) + local from = win32.parse("C:\\Users\\username\\Documents") + local to = win32.parse("C:\\Users\\username\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "file.txt") + assert.eq(result.drive, nil) + end) + + suite:case("relative_different_kinds", function(assert) + local from = win32.parse("C:\\Users\\username\\Documents") + local to = win32.parse("Documents\\file.txt") + assert.erroreq(function() + path.win32.relative(from, to) + end, "Cannot compute relative path between different kinds of paths") + end) + + suite:case("relative_different_drives", function(assert) + local from = win32.parse("C:\\Users\\username") + local to = win32.parse("D:\\Documents\\file.txt") + assert.erroreq(function() + path.win32.relative(from, to) + end, "Cannot compute relative path between different drives") + end) + + suite:case("relative_unc_paths", function(assert) + local from = win32.parse("\\\\server\\share\\folder1") + local to = win32.parse("\\\\server\\share\\folder2\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder2") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("relative_with_drive_letter_same_drive", function(assert) + local from = win32.parse("C:Documents") + local to = win32.parse("C:Documents\\subfolder\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "subfolder") + assert.eq(result.parts[2], "file.txt") + end) +end) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau new file mode 100644 index 000000000..62f1d73c8 --- /dev/null +++ b/tests/std/process.test.luau @@ -0,0 +1,137 @@ +local pathlib = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") +local windowsPath = require("@std/path/win32") + +test.suite("ProcessSuite", function(suite) + suite:case("homedir_and_cwd_and_execpath", function(assert) + local hd = process.homedir() + assert.neq(pathlib.format(hd), "") + + local cwd = process.cwd() + assert.neq(pathlib.format(cwd), "") + + local ex = process.execpath() + assert.neq(pathlib.format(ex), "") + end) + + suite:case("run_echo_array_via_run", function(assert) + local r = process.run({ "echo", "hello-from-lute" }) + assert.eq(r.stdout, "hello-from-lute\n") + end) + + suite:case("run_echo_array_via_system", function(assert) + local r = process.system("echo hello-from-lute") + assert.eq(r.stdout, "hello-from-lute\n") + end) + + suite:case("run_system_and_cwd", function(check) + local rootdir: string = "/" + local root: pathlib.path? = nil + if system.win32 then + root = pathlib.win32.drive(process.cwd() :: windowsPath.path) + rootdir = pathlib.format(root) + end + local r = process.run({ "pwd" }, { cwd = rootdir }) + if system.win32 then + assert(root ~= nil and (root :: windowsPath.path).drive ~= nil) + check.tableeq(r, { -- LUAUFIX: ProcessResult (a: A, ...: B...): A return a end") + assert.eq(anonFuncExpr.tag, "function") + assert.eq(anonFuncExpr.kind, "expr") + + funcExpr = anonFuncExpr :: syntax.AstExprAnonymousFunction + assert.eq(#funcExpr.attributes, 0) + assert.eq(funcExpr.functionkeyword.text, "function") + + funcBody = funcExpr.body + assert.neq(funcBody.opengenerics, nil) + assert.neq(funcBody.generics, nil) + + assert.eq(#funcBody.generics :: syntax.Punctuated, 1) + local gen = (funcBody.generics :: syntax.Punctuated)[1].node + assert.eq(gen.name.text, "A") + assert.eq(gen.equals, nil) + assert.eq(gen.default, nil) + + assert.neq(funcBody.genericpacks, nil) + assert.eq(#funcBody.genericpacks :: syntax.Punctuated, 1) + local genPack = (funcBody.genericpacks :: syntax.Punctuated)[1].node + assert.eq(genPack.name.text, "B") + assert.eq(genPack.ellipsis.text, "...") + assert.eq(genPack.equals, nil) + assert.eq(genPack.default, nil) + + assert.neq(funcBody.closegenerics, nil) + assert.eq((funcBody.closegenerics :: syntax.Token<">">).text, ">") + + assert.eq(funcBody.openparens.text, "(") + assert.eq(#funcBody.parameters, 1) + + local param = funcBody.parameters[1].node + assert.eq(param.name.text, "a") + assert.neq(param.colon, nil) + assert.eq((param.colon :: syntax.Token<":">).text, ":") + assert.neq(param.annotation, nil) + assert.eq((param.annotation :: syntax.AstType).tag, "reference") + + assert.neq(funcBody.vararg, nil) + assert.eq((funcBody.vararg :: syntax.Token<"...">).text, "...") + assert.neq(funcBody.varargcolon, nil) + assert.eq((funcBody.varargcolon :: syntax.Token<":">).text, ":") + assert.neq(funcBody.varargannotation, nil) + assert.eq((funcBody.varargannotation :: syntax.AstTypePack).tag, "generic") + + assert.eq(funcBody.closeparens.text, ")") + assert.neq(funcBody.returnspecifier, nil) + assert.eq((funcBody.returnspecifier :: syntax.Token<":">).text, ":") + assert.neq(funcBody.returnannotation, nil) + assert.eq((funcBody.returnannotation :: syntax.AstTypePack).tag, "explicit") + end) + + suite:case("parseExprTable", function(assert) + local emptyTableExpr = parser.parseexpr("{}") + assert.eq(emptyTableExpr.tag, "table") + assert.eq(emptyTableExpr.kind, "expr") + local tableExpr = emptyTableExpr :: syntax.AstExprTable + assert.eq(tableExpr.openbrace.text, "{") + assert.eq(tableExpr.closebrace.text, "}") + assert.eq(#tableExpr.entries, 0) + + local mixedTable = parser.parseexpr("{1, foo = bar; [2] = baz}") + assert.eq(mixedTable.tag, "table") + assert.eq(mixedTable.kind, "expr") + local mixedTableExpr = mixedTable :: syntax.AstExprTable + assert.eq(#mixedTableExpr.entries, 3) + + local entry1 = mixedTableExpr.entries[1] + assert.eq(entry1.kind, "list") + assert.eq(entry1.value.tag, "number") + assert.neq(entry1.separator, nil) + assert.eq((entry1.separator :: syntax.Token<"," | ";">).text, ",") + + local entry2: any = mixedTableExpr.entries[2] + assert.eq(entry2.kind, "record") + assert.eq(entry2.key.text, "foo") + assert.eq(entry2.equals.text, "=") + assert.eq(entry2.value.tag, "global") + assert.neq(entry2.separator, nil) + assert.eq((entry2.separator :: syntax.Token<"," | ";">).text, ";") + + local entry3: any = mixedTableExpr.entries[3] + assert.eq(entry3.kind, "general") + assert.eq(entry3.indexeropen.text, "[") + assert.eq(entry3.key.tag, "number") + assert.eq(entry3.indexerclose.text, "]") + assert.eq(entry3.equals.text, "=") + assert.eq(entry3.value.tag, "global") + assert.eq(entry3.separator, nil) + end) + + suite:case("parseExprUnary", function(assert) + local notExpr = parser.parseexpr("not x") + assert.eq(notExpr.tag, "unary") + assert.eq(notExpr.kind, "expr") + local expr = notExpr :: syntax.AstExprUnary + assert.eq(expr.operator.text, "not") + assert.eq(expr.operand.tag, "global") + + local negExpr = parser.parseexpr("-42") + assert.eq(negExpr.tag, "unary") + assert.eq(negExpr.kind, "expr") + expr = negExpr :: syntax.AstExprUnary + assert.eq(expr.operator.text, "-") + assert.eq(expr.operand.tag, "number") + + local lenExpr = parser.parseexpr("#str") + assert.eq(lenExpr.tag, "unary") + assert.eq(lenExpr.kind, "expr") + expr = lenExpr :: syntax.AstExprUnary + assert.eq(expr.operator.text, "#") + assert.eq(expr.operand.tag, "global") + end) + + suite:case("parseExprBinary", function(assert) + local addExpr = parser.parseexpr("1 + 2") + assert.eq(addExpr.tag, "binary") + assert.eq(addExpr.kind, "expr") + local expr = addExpr :: syntax.AstExprBinary + assert.eq(expr.lhsoperand.tag, "number") + assert.eq(expr.operator.text, "+") + assert.eq(expr.rhsoperand.tag, "number") + end) + + suite:case("parseExprInterpString", function(assert) + local interpExpr = parser.parseexpr("`Hello, {name}! Today is {dayOfWeek}.`") + assert.eq(interpExpr.tag, "interpolatedstring") + assert.eq(interpExpr.kind, "expr") + local expr = interpExpr :: syntax.AstExprInterpString + assert.eq(#expr.strings, 3) + + assert.eq(expr.strings[1].text, "Hello, ") + assert.eq(expr.strings[2].text, "! Today is ") + assert.eq(expr.strings[3].text, ".") + + assert.eq(#expr.expressions, 2) + assert.eq(expr.expressions[1].tag, "global") + assert.eq(expr.expressions[2].tag, "global") + end) + + suite:case("parseExprTypeAssertion", function(assert) + local castExpr = parser.parseexpr("x :: string") + assert.eq(castExpr.tag, "cast") + assert.eq(castExpr.kind, "expr") + local expr = castExpr :: syntax.AstExprTypeAssertion + assert.eq(expr.operand.tag, "global") + assert.eq(expr.operator.text, "::") + assert.eq(expr.annotation.tag, "reference") + end) + + -- table items serialized with location field + suite:case("ExprTableItem serialized with location field", function(assert) + local tableExpr = parser.parseexpr([[ +{ +1, +foo = bar, +[2] = baz +} + ]]) + assert.eq(tableExpr.tag, "table") + assert.eq(tableExpr.kind, "expr") + tableExpr = tableExpr :: syntax.AstExprTable + assert.eq(#tableExpr.entries, 3) + assert.eq(tableExpr.entries[1].location.beginline, 2) + assert.eq(tableExpr.entries[1].location.begincolumn, 1) + assert.eq(tableExpr.entries[1].location.endline, 2) + assert.eq(tableExpr.entries[1].location.endcolumn, 2) + assert.eq(tableExpr.entries[2].location.beginline, 3) + assert.eq(tableExpr.entries[2].location.begincolumn, 1) + assert.eq(tableExpr.entries[2].location.endline, 3) + assert.eq(tableExpr.entries[2].location.endcolumn, 10) + assert.eq(tableExpr.entries[3].location.beginline, 4) + assert.eq(tableExpr.entries[3].location.begincolumn, 1) + assert.eq(tableExpr.entries[3].location.endline, 4) + assert.eq(tableExpr.entries[3].location.endcolumn, 10) + end) +end) + +local function checkIsBlock(node: any, assert: asserts.asserts) + assert.eq(node.kind, "stat") + assert.eq(node.tag, "block") +end + +test.suite("parse", function(suite) + suite:case("parseEmptyProgram", function(assert) + local block = parser.parseblock("") + checkIsBlock(block, assert) + assert.eq(#block.statements, 0) + end) + + suite:case("parseDoStatement", function(assert) + local block = parser.parseblock("do print('Hello, World!') end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local doStat = block.statements[1] :: syntax.AstStatDo + assert.eq(doStat.tag, "do") + assert.eq(doStat.dokeyword.text, "do") + assert.eq(#doStat.body, 1) + assert.eq(doStat.endkeyword.text, "end") + end) + + suite:case("parseIfStatement", function(assert) + local block = parser.parseblock( + "if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end" + ) + assert.eq(#block.statements, 1) + + local ifStat = block.statements[1] :: syntax.AstStatIf + assert.eq(ifStat.tag, "conditional") + assert.eq(ifStat.ifkeyword.text, "if") + assert.eq(ifStat.condition.tag, "binary") + assert.eq(ifStat.thenkeyword.text, "then") + checkIsBlock(ifStat.thenblock, assert) + + local thenBlock = ifStat.thenblock + assert.eq(#thenBlock.statements, 1) + + assert.eq(#ifStat.elseifs, 1) + local elseIf = ifStat.elseifs[1] + assert.eq(elseIf.elseifkeyword.text, "elseif") + assert.eq(elseIf.condition.tag, "binary") + assert.eq(elseIf.thenkeyword.text, "then") + checkIsBlock(elseIf.thenblock, assert) + + assert.neq(ifStat.elsekeyword, nil) + assert.eq((ifStat.elsekeyword :: syntax.Token<"else">).text, "else") + assert.neq(ifStat.elseblock, nil) + checkIsBlock(ifStat.elseblock, assert) + assert.eq(ifStat.endkeyword.text, "end") + + block = parser.parseblock("if condition then doSomething() end") + assert.eq(#block.statements, 1) + local simpleIfStat = block.statements[1] :: syntax.AstStatIf + assert.eq(simpleIfStat.tag, "conditional") + assert.eq(simpleIfStat.ifkeyword.text, "if") + assert.eq(simpleIfStat.condition.tag, "global") + assert.eq(simpleIfStat.thenkeyword.text, "then") + checkIsBlock(simpleIfStat.thenblock, assert) + assert.eq(#simpleIfStat.thenblock.statements, 1) + assert.eq(#simpleIfStat.elseifs, 0) + assert.eq(simpleIfStat.elsekeyword, nil) + assert.eq(simpleIfStat.elseblock, nil) + assert.eq(simpleIfStat.endkeyword.text, "end") + end) + + suite:case("parseStatWhile", function(assert) + local block = parser.parseblock("while i <= 10 do print(i); i = i + 1 end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local whileStat = block.statements[1] :: syntax.AstStatWhile + assert.eq(whileStat.tag, "while") + assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.condition.tag, "binary") + assert.eq(whileStat.dokeyword.text, "do") + checkIsBlock(whileStat.body, assert) + assert.eq(#whileStat.body.statements, 2) + assert.eq(whileStat.endkeyword.text, "end") + + block = parser.parseblock("while true do break end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + whileStat = block.statements[1] :: syntax.AstStatWhile + assert.eq(whileStat.tag, "while") + assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.condition.tag, "boolean") + assert.eq(whileStat.dokeyword.text, "do") + checkIsBlock(whileStat.body, assert) + assert.eq(#whileStat.body.statements, 1) + + local breakStat = whileStat.body.statements[1] :: syntax.AstStat + assert.eq(breakStat.kind, "stat") + breakStat = breakStat :: syntax.AstStatBreak + assert.eq(breakStat.tag, "break") + assert.eq(breakStat.text, "break") + + assert.eq(whileStat.endkeyword.text, "end") + + block = parser.parseblock("while true do continue end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + whileStat = block.statements[1] :: syntax.AstStatWhile + assert.eq(whileStat.tag, "while") + assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.condition.tag, "boolean") + assert.eq(whileStat.dokeyword.text, "do") + checkIsBlock(whileStat.body, assert) + assert.eq(#whileStat.body.statements, 1) + + local continueStat = whileStat.body.statements[1] :: syntax.AstStat + assert.eq(continueStat.kind, "stat") + continueStat = continueStat :: syntax.AstStatContinue + assert.eq(continueStat.tag, "continue") + assert.eq(continueStat.text, "continue") + + assert.eq(whileStat.endkeyword.text, "end") + end) + + suite:case("parseStatRepeat", function(assert) + local block = parser.parseblock("repeat i = i + 1; print(i) until i > 5") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local repeatStat = block.statements[1] :: syntax.AstStatRepeat + assert.eq(repeatStat.tag, "repeat") + assert.eq(repeatStat.repeatkeyword.text, "repeat") + checkIsBlock(repeatStat.body, assert) + assert.eq(#repeatStat.body.statements, 2) + assert.eq(repeatStat.untilkeyword.text, "until") + assert.eq(repeatStat.condition.tag, "binary") + end) + + suite:case("parseStatLocal", function(assert) + local block = parser.parseblock("local b, c = 1, 2") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + assert.eq(block.statements[1].kind, "stat") + + local l = block.statements[1] :: syntax.AstStatLocal + assert.eq(l.tag, "local") + assert.eq(l.localkeyword.text, "local") + assert.eq(#l.variables, 2) + assert.eq(l.variables[1].node.name.text, "b") + assert.neq(l.variables[1].separator, nil) + assert.eq((l.variables[1].separator :: syntax.Token<",">).text, ",") + assert.eq(l.variables[2].node.name.text, "c") + assert.eq(l.variables[2].separator, nil) + assert.neq(l.equals, nil) + assert.eq((l.equals :: syntax.Token<"=">).text, "=") + assert.eq(#l.values, 2) + assert.eq(l.values[1].node.tag, "number") + assert.neq(l.values[1].separator, nil) + assert.eq((l.values[1].separator :: syntax.Token<",">).text, ",") + assert.eq(l.values[2].node.tag, "number") + assert.eq(l.values[2].separator, nil) + + block = parser.parseblock("local x") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + assert.eq(block.statements[1].kind, "stat") + + l = block.statements[1] :: syntax.AstStatLocal + assert.eq(l.tag, "local") + assert.eq(l.localkeyword.text, "local") + assert.eq(#l.variables, 1) + assert.eq(l.variables[1].node.name.text, "x") + assert.eq(l.variables[1].separator, nil) + assert.eq(l.equals, nil) + assert.eq(#l.values, 0) + end) + + suite:case("parseStatFor", function(assert) + local block = parser.parseblock("for i = 1, 10, 2 do print(i) end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local forStat = block.statements[1] :: syntax.AstStatFor + assert.eq(forStat.tag, "for") + assert.eq(forStat.forkeyword.text, "for") + assert.eq(forStat.variable.name.text, "i") + assert.eq(forStat.equals.text, "=") + assert.eq(forStat.from.tag, "number") + assert.eq(forStat.tocomma.text, ",") + assert.eq(forStat.to.tag, "number") + assert.neq(forStat.stepcomma, nil) + assert.eq((forStat.stepcomma :: syntax.Token<",">).text, ",") + assert.neq(forStat.step, nil) + assert.eq(((forStat.step :: any) :: syntax.AstExpr).tag, "number") + assert.eq(forStat.dokeyword.text, "do") + checkIsBlock(forStat.body, assert) + assert.eq(#forStat.body.statements, 1) + assert.eq(forStat.endkeyword.text, "end") + + block = parser.parseblock("for j = 0, 5 do print(j) end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + forStat = block.statements[1] :: syntax.AstStatFor + assert.eq(forStat.tag, "for") + assert.eq(forStat.forkeyword.text, "for") + assert.eq(forStat.variable.name.text, "j") + assert.eq(forStat.equals.text, "=") + assert.eq(forStat.from.tag, "number") + assert.eq(forStat.tocomma.text, ",") + assert.eq(forStat.to.tag, "number") + assert.eq(forStat.stepcomma, nil) + assert.eq(forStat.step, nil) + assert.eq(forStat.dokeyword.text, "do") + checkIsBlock(forStat.body, assert) + assert.eq(#forStat.body.statements, 1) + assert.eq(forStat.endkeyword.text, "end") + end) + + suite:case("parseForInLoop", function(assert) + local block = parser.parseblock("for key, value in arr do print(key, value) end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local forInStat = block.statements[1] :: syntax.AstStatForIn + assert.eq(forInStat.tag, "forin") + assert.eq(forInStat.forkeyword.text, "for") + assert.eq(#forInStat.variables, 2) + assert.eq(forInStat.variables[1].node.name.text, "key") + assert.eq(forInStat.variables[2].node.name.text, "value") + assert.eq(forInStat.inkeyword.text, "in") + assert.eq(#forInStat.values, 1) + assert.eq(forInStat.values[1].node.tag, "global") + assert.eq(forInStat.dokeyword.text, "do") + checkIsBlock(forInStat.body, assert) + assert.eq(#forInStat.body.statements, 1) + assert.eq(forInStat.endkeyword.text, "end") + end) + + suite:case("parseStatAssign", function(assert) + local block = parser.parseblock("x = 1") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local assign = block.statements[1] :: syntax.AstStatAssign + assert.eq(assign.tag, "assign") + assert.eq(#assign.variables, 1) + assert.eq(assign.variables[1].node.tag, "global") + assert.eq(assign.equals.text, "=") + assert.eq(#assign.values, 1) + assert.eq(assign.values[1].node.tag, "number") + end) + + suite:case("parseStatCompoundAssign", function(assert) + local block = parser.parseblock("x += 5") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local compound = block.statements[1] :: syntax.AstStatCompoundAssign + assert.eq(compound.tag, "compoundassign") + assert.eq(compound.variable.tag, "global") + assert.eq(compound.operand.text, "+=") + assert.eq(compound.value.tag, "number") + end) + + suite:case("parseStatFunction", function(assert) + local block = parser.parseblock("@checked @native @deprecated function add(a, b) return a + b end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local funcStat = block.statements[1] :: syntax.AstStatFunction + assert.eq(funcStat.tag, "function") + assert.eq(#funcStat.attributes, 3) + assert.eq((funcStat.attributes[1] :: syntax.AstAttribute).text, "@checked") + assert.eq((funcStat.attributes[2] :: syntax.AstAttribute).text, "@native") + assert.eq((funcStat.attributes[3] :: syntax.AstAttribute).text, "@deprecated") + assert.eq(funcStat.functionkeyword.text, "function") + assert.eq(funcStat.name.tag, "global") + assert.eq((funcStat.name :: syntax.AstExprGlobal).name.text, "add") + -- body parsing tested in parseExprAnonymousFunction + end) + + suite:case("parseStatLocalFunction", function(assert) + local block = parser.parseblock("local function multiply(x, y) return x * y end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction + assert.eq(localFuncStat.tag, "localfunction") + assert.eq(#localFuncStat.attributes, 0) + assert.eq(localFuncStat.localkeyword.text, "local") + assert.eq(localFuncStat.functionkeyword.text, "function") + assert.eq(localFuncStat.name.name.text, "multiply") + end) + + suite:case("parseStatTypeAlias", function(assert) + local block = parser.parseblock("type Vector3 = {x: number, y: number, z: number}") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.tag, "typealias") + assert.eq(typeAlias.export, nil) + assert.eq(typeAlias.typetoken.text, "type") + assert.eq(typeAlias.name.text, "Vector3") + assert.eq(typeAlias.opengenerics, nil) + assert.eq(typeAlias.generics, nil) + assert.eq(typeAlias.closegenerics, nil) + assert.eq(typeAlias.equals.text, "=") + assert.eq(typeAlias.type.kind, "type") + assert.eq(typeAlias.type.tag, "table") + + block = parser.parseblock("export type Point = {x: T, y: T, f: (U...) -> ()}") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.neq(typeAlias.export, nil) + assert.eq((typeAlias.export :: syntax.Token<"export">).text, "export") + assert.neq(typeAlias.opengenerics, nil) + assert.eq((typeAlias.opengenerics :: syntax.Token<"<">).text, "<") + assert.neq(typeAlias.generics, nil) + assert.eq(#typeAlias.generics :: syntax.Punctuated, 1) + assert.neq(typeAlias.genericpacks, nil) + assert.eq(#typeAlias.genericpacks :: syntax.Punctuated, 1) + assert.neq(typeAlias.closegenerics, nil) + assert.eq((typeAlias.closegenerics :: syntax.Token<">">).text, ">") + end) + + suite:case("parseStatTypeFunction", function(assert) + local block = parser.parseblock("type function foo() return types.number end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local typeFunc = block.statements[1] :: syntax.AstStatTypeFunction + assert.eq(typeFunc.tag, "typefunction") + assert.eq(typeFunc.export, nil) + assert.eq(typeFunc.type.text, "type") + assert.eq(typeFunc.functionkeyword.text, "function") + assert.eq(typeFunc.name.text, "foo") + + block = parser.parseblock("export type function foo() return types.number end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + typeFunc = block.statements[1] :: syntax.AstStatTypeFunction + assert.neq(typeFunc.export, nil) + assert.eq((typeFunc.export :: syntax.Token<"export">).text, "export") + end) +end) + +test.suite("parse types", function(suite) + -- AstGenericType and AstGenericTypePack are tested in parseExprAnonymousFunction + suite:case("testTypeReference", function(assert) + local block = parser.parseblock("type MyString = string") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "reference") + + local typeRef = typeAlias.type :: syntax.AstTypeReference + assert.eq(typeRef.prefix, nil) + assert.eq(typeRef.prefixpoint, nil) + assert.eq(typeRef.name.text, "string") + assert.eq(typeRef.openparameters, nil) + assert.eq(typeRef.parameters, nil) + assert.eq(typeRef.closeparameters, nil) + + block = parser.parseblock("type MyTable = MyModule.Table") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "reference") + + typeRef = typeAlias.type :: syntax.AstTypeReference + assert.neq(typeRef.prefix, nil) + assert.eq((typeRef.prefix :: syntax.Token).text, "MyModule") + assert.neq(typeRef.prefixpoint, nil) + assert.eq((typeRef.prefixpoint :: syntax.Token<".">).text, ".") + assert.eq(typeRef.name.text, "Table") + assert.neq(typeRef.openparameters, nil) + assert.eq((typeRef.openparameters :: syntax.Token<"<">).text, "<") + assert.neq(typeRef.parameters, nil) + assert.eq(#typeRef.parameters :: syntax.Punctuated, 2) + assert.eq((typeRef.parameters :: syntax.Punctuated)[1].node.tag, "reference") + assert.eq((typeRef.parameters :: syntax.Punctuated)[2].node.tag, "reference") + assert.neq(typeRef.closeparameters, nil) + assert.eq((typeRef.closeparameters :: syntax.Token<">">).text, ">") + end) + + suite:case("testTypeSingletonBoolean", function(assert) + local block = parser.parseblock("type TrueType = true") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "boolean") + + local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool + assert.eq(singletonBool.text, "true") + assert.eq(singletonBool.value, true) + + block = parser.parseblock("type FalseType = false") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "boolean") + + singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool + assert.eq(singletonBool.text, "false") + assert.eq(singletonBool.value, false) + end) + + suite:case("testTypeSingletonString", function(assert) + local block = parser.parseblock("type HelloType = 'hello'") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "string") + + local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString + assert.eq(singletonStr.text, "hello") + assert.eq(singletonStr.quotestyle, "single") + + block = parser.parseblock('type WorldType = "world"') + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "string") + + singletonStr = typeAlias.type :: syntax.AstTypeSingletonString + assert.eq(singletonStr.text, "world") + assert.eq(singletonStr.quotestyle, "double") + end) + + suite:case("testTypeTypeof", function(assert) + local block = parser.parseblock("type T = typeof(someVariable)") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "typeof") + + local typeofType = typeAlias.type :: syntax.AstTypeTypeof + assert.eq(typeofType.typeof.text, "typeof") + assert.eq(typeofType.openparens.text, "(") + assert.eq(typeofType.expression.tag, "global") + assert.eq(typeofType.closeparens.text, ")") + end) + + suite:case("testTypeGroup", function(assert) + local block = parser.parseblock("type T = (string)") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "group") + + local groupType = typeAlias.type :: syntax.AstTypeGroup + assert.eq(groupType.openparens.text, "(") + assert.eq(groupType.type.tag, "reference") + assert.eq(groupType.closeparens.text, ")") + end) + + suite:case("testTypeUnion", function(assert) + local block = parser.parseblock("type T = string | number | boolean") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "union") + + local unionType = typeAlias.type :: syntax.AstTypeUnion + assert.eq(unionType.leading, nil) + assert.eq(#unionType.types, 3) + assert.eq(unionType.types[1].node.tag, "reference") + assert.eq(unionType.types[2].node.tag, "reference") + assert.eq(unionType.types[3].node.tag, "reference") + + block = parser.parseblock("type T = | number?") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "union") + + unionType = typeAlias.type :: syntax.AstTypeUnion + assert.neq(unionType.leading, nil) + assert.eq((unionType.leading :: syntax.Token<"|">).text, "|") + assert.eq(#unionType.types, 2) + assert.eq(unionType.types[1].node.tag, "reference") + assert.eq(unionType.types[2].node.tag, "optional") + assert.eq((unionType.types[2].node :: syntax.AstTypeOptional).text, "?") + end) + + suite:case("testTypeUnionWithOptionalPart", function(assert) + local block = parser.parseblock("type T = number? | string") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "union") + + local unionType = typeAlias.type :: syntax.AstTypeUnion + assert.eq(unionType.leading, nil) + assert.eq(#unionType.types, 3) + + local pair1 = unionType.types[1] + assert.eq(pair1.node.tag, "reference") + assert.eq(pair1.separator, nil) + + local pair2 = unionType.types[2] + assert.eq(pair2.node.tag, "optional") + assert.neq(pair2.separator, nil) + assert.eq((pair2.separator :: syntax.Token<"|">).text, "|") + + local pair3 = unionType.types[3] + assert.eq(pair3.node.tag, "reference") + assert.eq(pair3.separator, nil) + end) + + suite:case("testTypeIntersection", function(assert) + local block = parser.parseblock("type T = A & B & C") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "intersection") + + local intersectionType = typeAlias.type :: syntax.AstTypeIntersection + assert.eq(intersectionType.leading, nil) + assert.eq(#intersectionType.types, 3) + assert.eq(intersectionType.types[1].node.tag, "reference") + assert.eq(intersectionType.types[2].node.tag, "reference") + assert.eq(intersectionType.types[3].node.tag, "reference") + + block = parser.parseblock("type T = & B") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "intersection") + + intersectionType = typeAlias.type :: syntax.AstTypeIntersection + assert.neq(intersectionType.leading, nil) + assert.eq((intersectionType.leading :: syntax.Token<"&">).text, "&") + assert.eq(#intersectionType.types, 1) + assert.eq(intersectionType.types[1].node.tag, "reference") + end) + + suite:case("testTypeArray", function(assert) + local block = parser.parseblock("type T = { number }") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "array") + + local arrayType = typeAlias.type :: syntax.AstTypeArray + assert.eq(arrayType.openbrace.text, "{") + assert.eq(arrayType.access, nil) + assert.eq(arrayType.type.tag, "reference") + assert.eq(arrayType.closebrace.text, "}") + + block = parser.parseblock("type T = { read string }") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "array") + + arrayType = typeAlias.type :: syntax.AstTypeArray + assert.neq(arrayType.access, nil) + assert.eq((arrayType.access :: syntax.Token<"read" | "write">).text, "read") + + block = parser.parseblock("type T = { write number }") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "array") + + arrayType = typeAlias.type :: syntax.AstTypeArray + assert.neq(arrayType.access, nil) + assert.eq((arrayType.access :: syntax.Token<"read" | "write">).text, "write") + end) + + suite:case("parseTypeTable", function(assert) + local block = parser.parseblock("type T = {}") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + + local tableType = typeAlias.type :: syntax.AstTypeTable + assert.eq(tableType.openbrace.text, "{") + assert.eq(#tableType.entries, 0) + assert.eq(tableType.closebrace.text, "}") + + block = parser.parseblock("type T = {[number] : string}") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + + tableType = typeAlias.type :: syntax.AstTypeTable + assert.eq(#tableType.entries, 1) + local tableTypeItem: any = tableType.entries[1] + assert.eq(tableTypeItem.kind, "indexer") + assert.eq(tableTypeItem.access, nil) + assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq(tableTypeItem.key.tag, "reference") + assert.eq(tableTypeItem.indexerclose.text, "]") + assert.eq(tableTypeItem.colon.text, ":") + assert.eq(tableTypeItem.value.tag, "reference") + assert.eq(tableTypeItem.separator, nil) + + block = parser.parseblock("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + tableType = typeAlias.type :: syntax.AstTypeTable + assert.eq(#tableType.entries, 3) + tableTypeItem = tableType.entries[1] + assert.eq(tableTypeItem.kind, "stringproperty") + assert.eq(tableTypeItem.access, nil) + assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).tag, "string") + assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).text, "hello") + assert.eq(tableTypeItem.indexerclose.text, "]") + assert.eq(tableTypeItem.colon.text, ":") + assert.eq(tableTypeItem.value.tag, "string") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ",") + + tableTypeItem = tableType.entries[2] + assert.eq(tableTypeItem.kind, "stringproperty") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "read") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ";") + + tableTypeItem = tableType.entries[3] + assert.eq(tableTypeItem.kind, "stringproperty") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "write") + assert.eq(tableTypeItem.separator, nil) + + block = parser.parseblock("type T = { hello: 'world'; read foo: number, write bar: boolean }") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + tableType = typeAlias.type :: syntax.AstTypeTable + assert.eq(#tableType.entries, 3) + tableTypeItem = tableType.entries[1] + assert.eq(tableTypeItem.kind, "property") + assert.eq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.key :: syntax.Token).text, "hello") + assert.eq(tableTypeItem.colon.text, ":") + assert.eq(tableTypeItem.value.tag, "string") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ";") + + tableTypeItem = tableType.entries[2] + assert.eq(tableTypeItem.kind, "property") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "read") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ",") + + tableTypeItem = tableType.entries[3] + assert.eq(tableTypeItem.kind, "property") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "write") + assert.eq(tableTypeItem.separator, nil) + end) + + suite:case("parseTypeFunction", function(assert) + local block = parser.parseblock("type T = (n : number, string) -> boolean") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "function") + + local funcType = typeAlias.type :: syntax.AstTypeFunction + assert.eq(funcType.opengenerics, nil) + assert.eq(funcType.generics, nil) + assert.eq(funcType.genericpacks, nil) + assert.eq(funcType.closegenerics, nil) + assert.eq(funcType.openparens.text, "(") + assert.eq(#funcType.parameters, 2) + + local param = funcType.parameters[1].node + assert.neq(param.name, nil) + assert.eq((param.name :: syntax.Token).text, "n") + assert.neq(param.colon, nil) + assert.eq((param.colon :: syntax.Token<":">).text, ":") + assert.eq(param.type.tag, "reference") + + param = funcType.parameters[2].node + assert.eq(param.name, nil) + assert.eq(param.colon, nil) + assert.eq(param.type.tag, "reference") + + assert.eq(funcType.vararg, nil) + assert.eq(funcType.closeparens.text, ")") + assert.eq(funcType.returnarrow.text, "->") + assert.eq(funcType.returntypes.kind, "typepack") + + block = parser.parseblock("type T = (item: T, ...U) -> ()") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "function") + + funcType = typeAlias.type :: syntax.AstTypeFunction + assert.neq(funcType.opengenerics, nil) + assert.eq((funcType.opengenerics :: syntax.Token<"<">).text, "<") + assert.neq(funcType.generics, nil) + assert.eq(#funcType.generics :: syntax.Punctuated, 1) + assert.neq(funcType.genericpacks, nil) + assert.eq(#funcType.genericpacks :: syntax.Punctuated, 1) + assert.eq((funcType.genericpacks :: syntax.Punctuated)[1].node.tag, "generic") + + local genericTypePack = (funcType.genericpacks :: syntax.Punctuated)[1].node + assert.eq(genericTypePack.name.text, "U") + assert.eq(genericTypePack.ellipsis.text, "...") + + assert.neq(funcType.closegenerics, nil) + assert.eq((funcType.closegenerics :: syntax.Token<">">).text, ">") + assert.neq(funcType.vararg, nil) + assert.eq((funcType.vararg :: syntax.AstTypePack).kind, "typepack") + + block = parser.parseblock("type T = () -> ...number") + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "function") + + funcType = typeAlias.type :: syntax.AstTypeFunction + assert.eq(funcType.returntypes.kind, "typepack") + assert.eq(funcType.returntypes.tag, "variadic") + local variadicType: syntax.AstTypePackVariadic = funcType.returntypes :: any + assert.neq(variadicType.ellipsis, nil) + assert.eq((variadicType.ellipsis :: syntax.Token<"...">).text, "...") + assert.eq(variadicType.type.tag, "reference") + end) +end) diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau new file mode 100644 index 000000000..1a86b7b24 --- /dev/null +++ b/tests/std/syntax/printer.test.luau @@ -0,0 +1,896 @@ +local assertLib = require("@std/test/assert") +local printer = require("@std/syntax/printer") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntaxTypes = require("@std/syntax/types") +local syntaxUtils = require("@std/syntax/utils") +local test = require("@std/test") + +local function compareStrings(result: string, expected: string) + for i = 1, math.max(#result, #expected) do + local rChar = result:sub(i, i):byte() + local eChar = expected:sub(i, i):byte() + if rChar ~= eChar then + print(`Difference at index {i}: result='{rChar}', expected='{eChar}'`) + end + end +end + +local function checkReplacement( + source: string, + expected: string, + transformFn: (syntaxTypes.AstNode) -> syntaxTypes.replacements?, + assert: assertLib.asserts +) + local ast = syntax.parse(source) + local replacements = transformFn(ast.root) + + local result = printer.printfile(ast, replacements) + + assert.eq(result, expected) +end + +test.suite("syntax_printer", function(suite) + suite:case("string_replacement", function(assert) + local source = [[ + local name = "World" +]] + + local expected = " local name = 1 + 2\n" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return if s.text == "World" then "1 + 2" else nil + end) + end, assert) + end) + + suite:case("expr_replacement", function(assert) + local source = [[ + local name = "World" +]] + + local expected = " local name = 1 + 2\n" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return if s.text == "World" then syntax.parseexpr("1 + 2") else nil + end) + end, assert) + end) + + suite:case("stat_replacement", function(assert) + local source = [[ + local name = "World" +]] + + local expected = " local name = 1 + 2\n" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function(s) + return syntax.parseblock("local name = 1 + 2") + end) + end, assert) + end) + + suite:case("type_replacement", function(assert) + local source = [[local name: number = "World"]] + local expected = [[local name: string = "World"]] + + local stat = syntax.parseblock("local x: string") + local ann = (stat.statements[1] :: syntaxTypes.AstStatLocal).variables[1].node.annotation + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isStatLocal) + :map(function(assign) + return assign.variables[1].node.annotation + end) + :replace(function(t) + return ann + end) + end, assert) + end) + + suite:case("type_pack_replacement", function(assert) + local source = [[type t = (...number) -> ()]] + local expected = [[type t = (...number) -> string, ...string)]] + + local stat = syntax.parseblock("type t = () -> (string, ...string)") + local tp = ((stat.statements[1] :: syntaxTypes.AstStatTypeAlias).type :: syntaxTypes.AstTypeFunction).returntypes + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isStatTypeAlias) + :map(function(ta) + if ta.type.tag == "function" then + return ta.type.returntypes + end + end) + :replace(function(t) + return tp + end) + end, assert) + end) + + suite:case("expr_table_item_replacement", function(assert) + local source = "local tbl = { a = 1, }" + local expected = "local tbl = { b = 2, }" + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isExprTable) + :map(function(node) + return node.entries[1] + end) + :replace(function(entry) + return "b = 2," + end) + end, assert) + end) + + suite:case("exprgroup_trivia", function(assert) + local source = [[local x = +(1 + 2) -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprGroup):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprnil_trivia", function(assert) + local source = [[local x = +nil -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprConstantNil):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprbool_trivia", function(assert) + local source = [[local x = +true -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprConstantBool):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprnumber_trivia", function(assert) + local source = [[local x = +235 -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprConstantNumber):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprlocal_trivia", function(assert) + local source = [[local y = 3 + local x = +y -- after]] + local expected = "local y = 3\n\t\tlocal x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprLocal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprglobal_trivia", function(assert) + local source = [[local x = +y -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprGlobal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprvarargs_trivia", function(assert) + local source = [[local x = +... -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprVarargs):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprcall_trivia", function(assert) + local source = [[local x = +print() -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprCall):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprindexname_trivia", function(assert) + local source = [[local x = +hello.world -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprIndexName):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprindexexpr_trivia", function(assert) + local source = [[local x = +hello['world'] -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprIndexExpr):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("expranonymousfunction_trivia", function(assert) + local source = [[local x = +function() return end -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprAnonymousFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprtable_trivia", function(assert) + local source = [[local x = +{} -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprTable):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprunary_trivia", function(assert) + local source = [[local x = +#t -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprUnary):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprbinary_trivia", function(assert) + local source = [[local x = +3 + 2 -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprBinary):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprinterpstring_trivia", function(assert) + local source = [[local x = +`hello {"world"}` -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprInterpString):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprtypeassertion_trivia", function(assert) + local source = [[local x = +y :: number -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprTypeAssertion):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprifelse_trivia", function(assert) + local source = [[local x = +if true then 'hello' else 'world' -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExprIfElse):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("expr_trivia", function(assert) + local source = [[local x, y = +if true then 'hello' else 'world', z :: string -- after]] + local expected = "local x, y =\nreplaced, replaced -- after" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isExpr):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statblock_trivia", function(assert) + local source = [[-- hello +local y = 1 +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatBlock):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statif_trivia", function(assert) + local source = [[-- hello +if true then + return 1 +else + return 2 +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatIf):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statwhile_trivia", function(assert) + local source = [[-- hello +while true do + print("hello") +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatWhile):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statrepeat_trivia", function(assert) + local source = [[-- hello +repeat + print("hello") +until true +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatRepeat):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statbreak_trivia", function(assert) + local source = [[-- hello +while true do + break +end +-- world]] + local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatBreak):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statcontinue_trivia", function(assert) + local source = [[-- hello +while true do + continue +end +-- world]] + local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatContinue):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statreturn_trivia", function(assert) + local source = [[-- hello +function foo() return 1, 2 end +-- world]] + local expected = "-- hello\nfunction foo() replaced end\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatReturn):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statexpr_trivia", function(assert) + local source = [[-- hello +foobar() +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatExpr):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statlocal_trivia", function(assert) + local source = [[-- hello +local x = 1 +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statfor_trivia", function(assert) + local source = [[-- hello +for i = 1, 10 do + print(i) +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatFor):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statforin_trivia", function(assert) + local source = [[-- hello +for i in {1, 2, 3} do + print(i) +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatForIn):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statassign_trivia", function(assert) + local source = [[-- hello +local y = 2 +y = 1 +-- world]] + local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatAssign):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statcompoundassign_trivia", function(assert) + local source = [[-- hello +local y = 2 +y -= 1 +-- world]] + local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatCompoundAssign):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statfunction_trivia", function(assert) + local source = [[-- hello +function foo() + return 1 +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statlocalfunction_trivia", function(assert) + local source = [[-- hello +local function foo() + return 1 +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatLocalFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("stattypealias_trivia", function(assert) + local source = [[-- hello +type foo = number +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatTypeAlias):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("stattypefunction_trivia", function(assert) + local source = [[-- hello +type function foo() return number end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStatTypeFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("stat_trivia", function(assert) + local source = [[-- hello +local x = 1 +local y = 2 +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isStat):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typereference_trivia", function(assert) + local source = [[-- hello +type foo = number +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeReference):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typesingletonbool_trivia", function(assert) + local source = [[-- hello +type foo = true +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeSingletonBool):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typesingletonstring_trivia", function(assert) + local source = [[-- hello +local x : "hello" +-- world]] + local expected = "-- hello\nlocal x : replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeSingletonString):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typetypeof_trivia", function(assert) + local source = [[-- hello +type foo = typeof(123) +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeTypeof):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typegroup_trivia", function(assert) + local source = [[-- hello +type foo = (number) +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeGroup):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typeoptional_trivia", function(assert) + local source = [[-- hello +type foo = true? +-- world]] + local expected = "-- hello\ntype foo = truereplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeOptional):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typeunion_trivia", function(assert) + local source = [[-- hello +type foo = true? | false +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeUnion):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typeintersection_trivia", function(assert) + local source = [[-- hello +type foo = true & false +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeIntersection):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typearray_trivia", function(assert) + local source = [[-- hello +type foo = { number } +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeArray):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typetable_trivia", function(assert) + local source = [[-- hello +type foo = {} +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeTable):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typefunction_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> () +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypeFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("type_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> () +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isType):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepackexplicit_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (number, ...string) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypePackExplicit):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepackgeneric_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (number, G...) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (number, replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypePackGeneric):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepackvariadic_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (...number) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypePackVariadic):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepack_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (number, ...string) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isTypePack):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("local_trivia", function(assert) + local source = [[-- hello +local x, y = 1, 2 +-- world]] + local expected = "-- hello\nlocal replaced, replaced = 1, 2\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isLocal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("attribute_trivia", function(assert) + local source = [[-- hello +@checked +function foo() return end +-- world]] + local expected = "-- hello\nreplaced\nfunction foo() return end\n-- world" + + checkReplacement(source, expected, function(ast) + return query.findallfromroot(ast, syntaxUtils.isAttribute):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("comments_in_call", function(assert) + local source = [[foobar( + foo, -- what about this + bar, -- and this + baz -- and this?? +)]] + + local expected = "foobar(\n foo, -- what about this\n bar, -- and this\n quxx -- and this??\n)" + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isExprGlobal) + :filter(function(g) + return g.name.text == "baz" + end) + :replace(function() + return "quxx" + end) + end, assert) + end) + + suite:case("comments_in_table", function(assert) + local source = [[local x = { + foo = -- what about this + 1, + bar = 2, + -- and this + + baz = 3 -- and this?? +}]] + + local expected = [[local x = { + replaced = -- what about this + 1, + replaced = 2, + -- and this + + replaced = 3 -- and this?? +}]] + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isExprTable) + :flatmap(function(t) + local l = {} + for _, entry in t.entries do + table.insert(l, entry.key) + end + return l + end) + :replace(function() + return "replaced" + end) + end, assert) + end) +end) diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau new file mode 100644 index 000000000..21d6ab62e --- /dev/null +++ b/tests/std/syntax/query.test.luau @@ -0,0 +1,246 @@ +local syntax = require("@std/syntax") +local query = require("@std/syntax/query") +local test = require("@std/test") +local utils = require("@std/syntax/utils") + +test.suite("AST Query", function(suite) + suite:case("filter", function(assert) + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local filteredQuery = testQuery:filter(function(n) + return n.value > 2 + end) + + assert.eq(#filteredQuery.nodes, 2) + assert.eq(filteredQuery.nodes[1].value, 3) + assert.eq(filteredQuery.nodes[2].value, 4) + assert.eq(filteredQuery, testQuery) -- ensure the original query is mutated + end) + + suite:case("replace", function(assert) + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local replacements = testQuery:replace(function(n): string? + if n.value > 0 then + return "positive_" .. tostring(n.value) + end + return nil -- return nil for non-positive numbers + end) + + -- Should only have replacements for positive numbers + assert.eq(replacements[testQuery.nodes[2]], "positive_1") -- LUAUFIX CLI-178250: shouldn't need to cast + assert.eq(replacements[testQuery.nodes[3]], "positive_2") -- LUAUFIX CLI-178250: shouldn't need to cast + assert.eq(replacements[testQuery.nodes[4]], "positive_3") -- LUAUFIX CLI-178250: shouldn't need to cast + assert.eq(replacements[testQuery.nodes[5]], "positive_4") -- LUAUFIX CLI-178250: shouldn't need to cast + + -- Non-positive numbers should not have replacements (nil values) + assert.eq(replacements[testQuery.nodes[1]], nil) -- LUAUFIX CLI-178250: shouldn't need to cast + + -- Verify the replacement table only contains the expected entries + local count = 0 + for _, _ in replacements do + count += 1 + end + assert.eq(count, 4) -- 4 non-nil replacements + end) + + suite:case("map", function(assert) + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local mappedQuery = query.map(testQuery, function(n: syntax.AstExprConstantNumber): number? + if n.value % 2 == 0 then + return n.value * 10 + end + return nil -- return nil for odd numbers + end) + + assert.eq(#mappedQuery.nodes, 3) + assert.eq(mappedQuery.nodes[1], 0) + assert.eq(mappedQuery.nodes[2], 20) + assert.eq(mappedQuery.nodes[3], 40) + end) + + suite:case("foreach", function(assert) + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local sum = 0 + testQuery:foreach(function(n: syntax.AstExprConstantNumber) + sum = sum + n.value + end) + + assert.eq(sum, 3) -- 0 + 1 + 2 = 3 + end) + + suite:case("findall_nums", function(assert) + local ast = syntax.parse("print(1 + 2)") + local queryResult: query.query = query.findallfromroot( + ast, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + function(n: syntax.AstNode): syntax.AstExprConstantNumber? + if n.kind == "expr" and n.tag == "number" then + return n + end + return nil + end + ) + + assert.eq(#queryResult.nodes, 2) + local num: syntax.AstExpr = queryResult.nodes[1] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: syntax.AstExprConstantNumber).value, 1) + num = queryResult.nodes[2] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: syntax.AstExprConstantNumber).value, 2) + end) + + suite:case("findall_utils", function(assert) + local ast = syntax.parse("print(1 + 2)") + local queryResult: query.query = + query.findallfromroot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + assert.eq(#queryResult.nodes, 2) + local num: syntax.AstExpr = queryResult.nodes[1] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: syntax.AstExprConstantNumber).value, 1) + num = queryResult.nodes[2] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: syntax.AstExprConstantNumber).value, 2) + end) + + suite:case("findall_tokens", function(assert) + local ast = syntax.parse("local x = 1 or nil") + -- verify that query doesn't doubly count tokens + local queryResult = query.findallfromroot(ast, utils.isToken) + assert.eq(#queryResult.nodes, 6) + queryResult:foreach(function(node) + assert.eq(node.istoken, true) + end) + local constNums = query.findallfromroot(ast, utils.isExprConstantNumber) + assert.eq(#constNums.nodes, 1) + local constNils = query.findallfromroot(ast, utils.isExprConstantNil) + assert.eq(#constNils.nodes, 1) + local baseTokens = query.findallfromroot(ast, utils.isBaseToken) + assert.eq(#baseTokens.nodes, 4) + end) + + suite:case("findall", function(assert) + -- Test selecting from a query of binary expressions to find their operands + local ast = syntax.parse("local x = 1 + 2") + local nums = query.findallfromroot(ast, utils.isExprBinary):findall(utils.isExprConstantNumber) + + assert.eq(#nums.nodes, 2) + assert.eq(nums.nodes[1].value, 1) + assert.eq(nums.nodes[2].value, 2) + end) + + suite:case("findall", function(assert) + -- Test selecting through nested structures + local ast = syntax.parse("local x = {a = 1 + 2, b = 3 + 4}") + local tables = query.findallfromroot(ast, utils.isExprTable) + + -- Select all binary expressions within the table + local binaryExprs = tables:findall(utils.isExprBinary) + + assert.eq(#binaryExprs.nodes, 2) + + -- Now select all numbers from those binary expressions + local numbers = binaryExprs:findall(utils.isExprConstantNumber) + + assert.eq(#numbers.nodes, 4) + assert.eq(numbers.nodes[1].value, 1) + assert.eq(numbers.nodes[2].value, 2) + assert.eq(numbers.nodes[3].value, 3) + assert.eq(numbers.nodes[4].value, 4) + end) + + suite:case("flatmap", function(assert) + -- Test flatmap to collect multiple items from each node + local ast = syntax.parse("local x = 1 + 2; local y = 3 + 4") + local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) + local values = {} + for _, v in l.values do + table.insert(values, v.node) + end + return values + end) + + assert.eq(#locals.nodes, 2) -- Two binary expressions + assert.eq(locals.nodes[1].kind, "expr") + assert.eq(locals.nodes[1].tag, "binary") + assert.eq(locals.nodes[2].kind, "expr") + assert.eq(locals.nodes[2].tag, "binary") + end) + + suite:case("flatmap_empty", function(assert) + -- Test flatmap with some nodes returning empty arrays + local ast = syntax.parse("local x; local y = 1; local z") + local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) + local values = {} + for _, v in l.values do + table.insert(values, v.node) + end + return values + end) + + assert.eq(#locals.nodes, 1) -- Only 'local y = 1' has a value + assert.eq(locals.nodes[1].kind, "expr") + assert.eq(locals.nodes[1].tag, "number") + assert.eq((locals.nodes[1] :: syntax.AstExprConstantNumber).value, 1) + end) + + suite:case("flatmap_multiple", function(assert) + -- Test flatmap with nodes that return multiple items + local ast = syntax.parse("local a, b = 1, 2; local c, d = 3, 4, 5") + local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l: syntax.AstStatLocal) + local vars = {} + for _, v in l.variables do + table.insert(vars, v.node) + end + return vars + end) + + assert.eq(#locals.nodes, 4) -- a, b, c, d + assert.eq(locals.nodes[1].name.text, "a") + assert.eq(locals.nodes[2].name.text, "b") + assert.eq(locals.nodes[3].name.text, "c") + assert.eq(locals.nodes[4].name.text, "d") + end) + + suite:case("maptoarray", function(assert) + -- Test maptoarray to convert query nodes to a simple array + local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") + local nums = query.findallfromroot(ast, utils.isExprConstantNumber) + + local doubled = nums:maptoarray(function(n: syntax.AstExprConstantNumber): number + return n.value * 2 + end) + + assert.eq(#doubled, 5) + assert.eq(doubled[1], 0) + assert.eq(doubled[2], 2) + assert.eq(doubled[3], 4) + assert.eq(doubled[4], 6) + assert.eq(doubled[5], 8) + end) + + suite:case("maptoarray_with_nil", function(assert) + -- Test maptoarray filtering out nil values + local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") + local nums = query.findallfromroot(ast, utils.isExprConstantNumber) + + local evenOnly = nums:maptoarray(function(n: syntax.AstExprConstantNumber): number? + if n.value % 2 == 0 then + return n.value + end + return nil + end) + + assert.eq(#evenOnly, 3) + assert.eq(evenOnly[1], 0) + assert.eq(evenOnly[2], 2) + assert.eq(evenOnly[3], 4) + end) +end) diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau new file mode 100644 index 000000000..8cf38a31c --- /dev/null +++ b/tests/std/tableext.test.luau @@ -0,0 +1,138 @@ +local tableext = require("@std/tableext") +local test = require("@std/test") + +test.suite("TableExt", function(suite) + suite:case("map_and_filter", function(assert) + local t: { [string]: number } = { a = 1, b = 2, c = 3 } + local mapped = tableext.map(t, function(v: number) + return v * 2 + end) + assert.eq(mapped.a, 2) + assert.eq(mapped.b, 4) + assert.eq(mapped.c, 6) + + local filtered = tableext.filter(t, function(v: number) + return v % 2 == 1 + end) + assert.eq(filtered.a, 1) + assert.eq(filtered.b, nil) + assert.eq(filtered.c, 3) + end) + + suite:case("extend", function(assert) + local a: { number } = { 1, 2 } + local b: { number } = { 3, 4 } + local c: { number } = { 5, 6 } + tableext.extend(a, b, c) + assert.eq(#a, 6) + assert.eq(a[3], 3) + assert.eq(a[4], 4) + assert.eq(a[5], 5) + assert.eq(a[6], 6) + end) + + suite:case("combine", function(assert) + local a = { ["a"] = 1, ["b"] = 2 } + local b = { ["b"] = 3, ["c"] = 4 } + local c = { ["c"] = 1, ["d"] = 5 } + tableext.combine(a, b, c) + assert.eq(a["a"], 1) + assert.eq(a["b"], 3) + assert.eq(a["c"], 1) + assert.eq(a["d"], 5) + end) + + suite:case("keys", function(assert) + local a = { a = 1, b = 2, c = 3 } + local keys = tableext.keys(a) + for _, key in keys do + assert.neq(a[key], nil) + end + end) + + suite:case("reverse", function(assert) + -- in-place + local a = { 1, 2, 3 } + tableext.reverse(a, true) + assert.tableeq(a, { 3, 2, 1 }) + + -- return new + local b = { 4, 5, 6 } + local c = tableext.reverse(b) + assert.tableeq(b, { 4, 5, 6 }) + assert.tableeq(c, { 6, 5, 4 }) + end) + + suite:case("toset", function(assert) + local a = { 1, 2, 4, 2, 1 } + local setA = tableext.toset(a) + assert.eq(setA[1], true) + assert.eq(setA[2], true) + assert.eq(setA[3], nil) + assert.eq(setA[4], true) + assert.eq(setA[5], nil) + + local setSize = #tableext.keys(setA) + assert.eq(setSize, 3) + assert.neq(setSize, #a) + + -- Sanity check: sets created using an array of numbers don't break from having "gaps" between the number keys. + + local foundKeys: { number } = {} + for key, value in setA do + assert.eq(value, true) + table.insert(foundKeys, key) + end + assert.eq(#foundKeys, setSize) + assert.eq(setA[foundKeys[1]], true) + assert.eq(setA[foundKeys[2]], true) + assert.eq(setA[foundKeys[3]], true) + + local b = { "a", "b", "c", "a" } + local setB = tableext.toset(b) + assert.eq(setB["a"], true) + assert.eq(setB["b"], true) + assert.eq(setB["c"], true) + assert.eq(setB["d"], nil) + assert.eq(#tableext.keys(setB), 3) + assert.neq(#tableext.keys(setB), #b) + end) + + suite:case("any", function(assert) + local a = { 1, 2, 3, 4, 5 } + local hasEven = tableext.any(a, function(v: number) + return v % 2 == 0 + end) + assert.eq(hasEven, true) + + local hasGreaterThanFive = tableext.any(a, function(v: number) + return v > 5 + end) + assert.eq(hasGreaterThanFive, false) + + a = {} + local anyInEmpty = tableext.any(a, function(v: number) + return true + end) + assert.eq(anyInEmpty, false) + end) + + suite:case("all", function(assert) + local a = { 2, 4, 6, 8 } + local allEven = tableext.all(a, function(v: number) + return v % 2 == 0 + end) + assert.eq(allEven, true) + + local allLessThanEight = tableext.all(a, function(v: number) + return v < 8 + end) + assert.eq(allLessThanEight, false) + + a = {} + local allInEmpty = tableext.all(a, function(v: number) + return false + end) + assert.eq(allInEmpty, true) + end) +end) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau new file mode 100644 index 000000000..3558eb36e --- /dev/null +++ b/tests/std/test.test.luau @@ -0,0 +1,340 @@ +local assertions = require("@std/test/assert") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) +local tmpDir = system.tmpdir() + +local function assertErrorsWithMsg( + testName: string, + testContents: string, + expectedErrMsg: string, + assert: assertions.asserts +) + local testFilePath = path.join(tmpDir, `{testName}.test.luau`) + fs.writestringtofile(testFilePath, testContents) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, expectedErrMsg) +end + +test.suite("LuteStdTestFramework", function(suite) + suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) + -- Setup + local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") + fs.writestringtofile( + testFilePath, + [[ +local test = require("@std/test") + +test.suite("nil_field_suite", function(suite) + suite:case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error + end) +end) + +test.run() + ]] + ) + + -- Run lute on the created test file + local result = process.run({ lutePath, path.format(testFilePath) }) + -- Check that the output doesn't include xpcall + assert.eq(result.exitcode, 1) + assert.eq(result.stdout:find("xpcall", 1, true), nil) + -- Check that the error points to filepath + correct line number + assert.strcontains(result.stdout, `Runtime error`) + assert.strcontains( + result.stdout, + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` + ) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:6: attempt to index nil with 'field'`) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:10`) + + fs.remove(testFilePath) + end) + + suite:case("lute doesn't report xpcall as error when accessing field of nil in case", function(assert) + -- Setup + local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") + fs.writestringtofile( + testFilePath, + [[ +local test = require("@std/test") + +test.case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error +end) + +test.run() + ]] + ) + + -- Run lute on the created test file + local result = process.run({ lutePath, path.format(testFilePath) }) + + -- Check that the output doesn't include xpcall + assert.eq(result.exitcode, 1) + assert.eq(result.stdout:find("xpcall", 1, true), nil) + -- Check that the error points to filepath + correct line number + assert.strcontains(result.stdout, `Runtime error`) + assert.strcontains( + result.stdout, + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` + ) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:5: attempt to index nil with 'field'`) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:8`) + + fs.remove(testFilePath) + end) + + suite:case("assert.eq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_eq_fails", + [[ +local test = require("@std/test") + +test.case("eq_error", function(assert) + assert.eq(1, 2) +end) + +test.run() + ]], + "eq: 1 ~= 2", + assert + ) + end) + + suite:case("assert.eq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_eq_fails", + [[ +local test = require("@std/test") + +test.suite("eq_failure_suite", function(suite) + suite:case("eq_error", function(assert) + assert.eq(1, 2) + end) +end) + +test.run() + ]], + "eq: 1 ~= 2", + assert + ) + end) + + suite:case("assert.neq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_neq_fails", + [[ +local test = require("@std/test") + +test.case("neq_error", function(assert) + assert.neq(1, 1) +end) + +test.run() + ]], + "neq: 1 == 1", + assert + ) + end) + + suite:case("assert.neq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_neq_fails", + [[ +local test = require("@std/test") + +test.suite("neq_failure_suite", function(suite) + suite:case("neq_error", function(assert) + assert.neq(1, 1) + end) +end) + +test.run() + ]], + "neq: 1 == 1", + assert + ) + end) + + suite:case("assert.tableeq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_tableeq_fails", + [[ +local test = require("@std/test") + +test.case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) +end) + +test.run() + ]], + "tableeq: lhs[a] = 1 but rhs[a] = 2", + assert + ) + end) + + suite:case("assert.tableeq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_tableeq_fails", + [[ +local test = require("@std/test") + +test.suite("tableeq_failure_suite", function(suite) + suite:case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) + end) +end) + +test.run() + ]], + "tableeq: lhs[a] = 1 but rhs[a] = 2", + assert + ) + end) + + suite:case("assert.buffereq_unequal_length", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("buffereq_error", function(assert) + local buf1 = buffer.create(1) + local buf2 = buffer.create(2) + assert.buffereq(buf1, buf2) +end) + +test.run() + ]], + "buffers are of unequal length", + assert + ) + end) + + suite:case("assert.buffereq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("buffereq_failure_suite", function(suite) + suite:case("buffereq_error", function(assert) + local buf1 = buffer.create(2) + local buf2 = buffer.create(2) + buffer.writeu8(buf1, 0, 84) + buffer.writeu8(buf2, 0, 85) + assert.buffereq(buf1, buf2) + end) +end) + +test.run() + ]], + "buffereq: lhs[0] = 54, but rhs[0] = 55", + assert + ) + end) + + suite:case("assert.erroreq_no_error", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("erroreq_error", function(assert) + assert.erroreq(function() return end, "expected message") +end) + +test.run() + ]], + "Function did not error as expected.", + assert + ) + end) + + suite:case("assert.erroreq_error_message", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("erroreq_failure_suite", function(suite) + suite:case("erroreq_error", function(assert) + assert.erroreq(function() error("wrong message") end, "expected message") + end) +end) + +test.run() + ]], + 'Expected suffix of error message "expected message", but got ', -- the full message contains tmpdir path to test file + assert + ) + end) + + suite:case("assert.strcontains_instances_negative", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye", nil, -1) +end) + +test.run() + ]], + "instances must be greater than 0", + assert + ) + end) + + suite:case("assert.strcontains_single_instance_fails", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye") + end) +end) + +test.run() + ]], + 'Expected "hello world" to contain "goodbye".', + assert + ) + end) + + suite:case("assert.strcontains_multiple_instance_fails", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("muahahahaha", "ha", nil, 5) + end) +end) + +test.run() + ]], + 'Expected "muahahahaha" to contain "ha" 5 times.', + assert + ) + end) +end) diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau new file mode 100644 index 000000000..48f05443b --- /dev/null +++ b/tests/std/time.test.luau @@ -0,0 +1,157 @@ +local test = require("@std/test") +local time = require("@std/time") + +local duration = time.duration + +test.suite("TimeSuite", function(suite) + suite:case("constructors_seconds_millis_micros_nanos", function(assert) + -- seconds + local d1 = duration.seconds(5) + assert.eq(d1:tomilliseconds(), 5000) + assert.eq(d1:tomicroseconds(), 5_000_000) + assert.eq(d1:tonanoseconds(), 5_000_000_000) + + -- milliseconds + local d2 = duration.milliseconds(1500) + assert.eq(d2:tomilliseconds(), 1500) + assert.eq(d2:subsecmillis(), 500) + -- 1.5 is exactly representable; check seconds too + assert.eq(d2:toseconds(), 1.5) + + -- microseconds + local d3 = duration.microseconds(1_234_567) + assert.eq(d3:tomicroseconds(), 1_234_567) + assert.eq(d3:subsecmicros(), 234_567) + assert.eq(d3:tomilliseconds(), 1234) -- floor down + + -- nanoseconds (with normalization) + local d4 = duration.nanoseconds(1_234_567_890) + assert.eq(d4:tonanoseconds(), 1_234_567_890) + assert.eq(d4:subsecnanos(), 234_567_890) + assert.eq(d4:tomilliseconds(), 1234) + + -- boundary normalization: exactly 1s in nanoseconds + local d5 = duration.nanoseconds(1_000_000_000) + assert.eq(d5:toseconds(), 1) + assert.eq(d5:subsecnanos(), 0) + end) + + suite:case("constructors_minutes_hours_days_weeks", function(assert) + local mins = duration.minutes(3) + assert.eq(mins:toseconds(), 180) + + local hours = duration.hours(2) + assert.eq(hours:toseconds(), 2 * 60 * 60) + + local days = duration.days(1) + assert.eq(days:toseconds(), 24 * 60 * 60) + + local weeks = duration.weeks(2) + assert.eq(weeks:toseconds(), 2 * 7 * 24 * 60 * 60) + end) + + suite:case("create_raw_values_and_normalization_via_ops", function(assert) + -- create keeps raw values without normalization + local d = duration.create(1, 500_000_000) + assert.eq(d:toseconds(), 1.5) + assert.eq(d:subsecnanos(), 500_000_000) + assert.eq(d:tomilliseconds(), 1500) + + -- if nanoseconds exceed a second, create still allows it + local d2 = duration.create(1, 1_500_000_000) + assert.eq(d2:toseconds(), 2.5) + assert.eq(d2:subsecnanos(), 1_500_000_000) + + -- operations like addition normalize nanoseconds + local normalized = d2 + duration.seconds(0) + assert.eq(normalized:toseconds(), 2.5) + assert.eq(normalized:subsecnanos(), 500_000_000) + end) + + suite:case("subsecond_accessors", function(assert) + local d = duration.milliseconds(987) + assert.eq(d:subsecmillis(), 987) + + local d2 = duration.microseconds(654_321) + assert.eq(d2:subsecmicros(), 654_321 % 1_000_000) + + local d3 = duration.nanoseconds(123_456_789) + assert.eq(d3:subsecnanos(), 123_456_789 % 1_000_000_000) + end) + + suite:case("conversions_to_ms_us_ns", function(assert) + local d = duration.seconds(2) + assert.eq(d:tomilliseconds(), 2000) + assert.eq(d:tomicroseconds(), 2_000_000) + assert.eq(d:tonanoseconds(), 2_000_000_000) + + local mix = duration.milliseconds(1234) + duration.microseconds(567_000) + duration.nanoseconds(890) + -- 1234ms + 567ms + 0.000890ms = 1801.00089ms + assert.eq(mix:tomilliseconds(), 1801) + assert.eq(mix:tomicroseconds(), 1_801_000) + assert.eq(mix:tonanoseconds(), 1_801_000_890) + end) + + suite:case("addition_normalizes_nanoseconds", function(assert) + local a = duration.milliseconds(800) + local b = duration.milliseconds(300) + local c = a + b + assert.eq(c:tomilliseconds(), 1100) + + local a2 = duration.nanoseconds(800_000_000) + local b2 = duration.nanoseconds(300_000_000) + local c2 = a2 + b2 -- should carry +1s and leave 100_000_000ns + assert.eq(c2:toseconds(), 1.1) + assert.eq(c2:subsecnanos(), 100_000_000) + end) + + suite:case("subtraction_borrows_nanoseconds", function(assert) + local a = duration.milliseconds(1100) -- 1s 100ms + local b = duration.milliseconds(200) + local c = a - b -- 900ms after borrow + assert.eq(c:tomilliseconds(), 900) + + local d = duration.seconds(5) - duration.seconds(5) + assert.eq(d:tomilliseconds(), 0) + end) + + suite:case("multiplication_and_division", function(assert) + local a = duration.milliseconds(250) + local twice = a * 2 + assert.eq(twice:tomilliseconds(), 500) + + local b = duration.milliseconds(600) * 3 + assert.eq(b:tomilliseconds(), 1800) + + local c = duration.seconds(2) / 2 + assert.eq(c:tomilliseconds(), 1000) + end) + + suite:case("comparisons_and_equality", function(assert) + local a = duration.seconds(1) + local b = duration.milliseconds(1000) + local c = duration.milliseconds(999) + + assert.eq(a == b, true) + assert.eq(a == c, false) + assert.eq(c < a, true) + assert.eq(c <= a, true) + assert.eq(a <= b, true) + assert.eq(a < b, false) + end) + + suite:case("tostring_format", function(assert) + local a = duration.seconds(1) + assert.eq(tostring(a), "1.000000000") + + local b = duration.milliseconds(1) + assert.eq(tostring(b), "0.001000000") + + local c = duration.nanoseconds(5) + assert.eq(tostring(c), "0.000000005") + + local d = duration.milliseconds(123) + duration.nanoseconds(456) + -- 0s 123_000_456ns + assert.eq(tostring(d), "0.123000456") + end) +end) diff --git a/tests/testAstSerializer.spec.luau b/tests/testAstSerializer.spec.luau deleted file mode 100644 index ccec3bfec..000000000 --- a/tests/testAstSerializer.spec.luau +++ /dev/null @@ -1,173 +0,0 @@ -local luau = require("@lute/luau") -local fs = require("@lute/fs") - -local parser = require("@std/syntax/parser") -local printer = require("@std/syntax/printer") -local T = require("@lute/luau") - -local function assertEqualsLocation( - actual: T.Location, - startLine: number, - startColumn: number, - endLine: number, - endColumn: number -) - assert(actual.begin.line == startLine) - assert(actual.begin.column == startColumn) - assert(actual["end"].line == endLine) - assert(actual["end"].column == endColumn) -end - -local function test_tokenContainsLeadingSpaces() - local block = luau.parse(" local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localKeyword - assert(#token.leadingTrivia == 1) - assert(token.leadingTrivia[1].tag == "whitespace") - assert(token.leadingTrivia[1].text == " ") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 0, 2) -end - -local function test_tokenContainsLeadingNewline() - local block = luau.parse("\n" .. "local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localKeyword - assert(#token.leadingTrivia == 1) - assert(token.leadingTrivia[1].tag == "whitespace") - assert(token.leadingTrivia[1].text == "\n") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 1, 0) -end - -local function test_tokenContainsLeadingSingleLineComment() - local block = luau.parse("-- comment\n" .. "local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localKeyword - assert(#token.leadingTrivia == 2) - assert(token.leadingTrivia[1].tag == "comment") - assert(token.leadingTrivia[1].text == "-- comment") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 0, 10) - assert(token.leadingTrivia[2].tag == "whitespace") - assert(token.leadingTrivia[2].text == "\n") - assertEqualsLocation(token.leadingTrivia[2].location, 0, 10, 1, 0) -end - -local function test_tokenContainsLeadingBlockComment() - local block = luau.parse("--[[ comment ]] local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localKeyword - assert(#token.leadingTrivia == 2) - assert(token.leadingTrivia[1].tag == "blockcomment") - assert(token.leadingTrivia[1].text == "--[[ comment ]]") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 0, 15) - assert(token.leadingTrivia[2].tag == "whitespace") - assert(token.leadingTrivia[2].text == " ") - assertEqualsLocation(token.leadingTrivia[2].location, 0, 15, 0, 16) -end - -local function test_tokenizeWhitespace() - local block = luau.parse(" \n\t\t\n\n" .. "local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localKeyword - assert(#token.leadingTrivia == 3) - assert(token.leadingTrivia[1].text == " \n") - assert(token.leadingTrivia[2].text == "\t\t\n") - assert(token.leadingTrivia[3].text == "\n") -end - -local function test_triviaSplitBetweenLeadingAndTrailing() - local block = luau.parse("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'").root - assert(#block.statements == 2) - - local firstStmt = block.statements[1] - assert(firstStmt.tag == "local") - - local trailingToken = firstStmt.values[1].node - assert(trailingToken.tag == "string") - assert(#trailingToken.trailingTrivia == 3) - assert(trailingToken.trailingTrivia[1].text == " ") - assert(trailingToken.trailingTrivia[2].text == "-- comment") - assert(trailingToken.trailingTrivia[3].text == "\n") - - local secondStmt = block.statements[2] - assert(secondStmt.tag == "local") - - local leadingToken = secondStmt.localKeyword - assert(#leadingToken.leadingTrivia == 2) - assert(leadingToken.leadingTrivia[1].text == "-- comment 2") - assert(leadingToken.leadingTrivia[2].text == "\n") -end - -local function test_roundtrippableAst() - local function visitDirectory(directory: string) - for _, entry in fs.listdir(directory) do - local path = `{directory}/{entry.name}` - print(path) - local source = fs.readfiletostring(path) - local result = printer.printfile(parser.parsefile(source)) - - print(result) - assert(source == result) - end - end - - visitDirectory("examples") - visitDirectory("tests/astSerializerTests") -end - -local function test_lineOffsetsField() - local singleLineCode = "local x = 1" - local result = luau.parse(singleLineCode) - - assert(result.lineOffsets) - assert(type(result.lineOffsets) == "table", "lineOffsets should be a table") - assert(#result.lineOffsets == 1, "Should have 1 line offset for single line code") -- only line 0 - assert(result.lineOffsets[1] == 0, "First line should start at offset 0") - - local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" - local multiResult = luau.parse(multiLineCode) - - assert(multiResult.lineOffsets) - assert(type(multiResult.lineOffsets) == "table", "lineOffsets should be a table for multiline") - assert(#multiResult.lineOffsets == 3, "Should have 3 line offsets for 3-line code") -- lines 0, 1, 2 - assert(multiResult.lineOffsets[1] == 0, "First line should start at offset 0") - assert(multiResult.lineOffsets[2] == 12, "Second line should start after 'local x = 1\\n'") - assert(multiResult.lineOffsets[3] == 24, "Third line should start after 'local x = 1\\nlocal y = 2\\n'") - - local emptyLineCode = "local x = 1\n\nlocal y = 2" - local emptyResult = luau.parse(emptyLineCode) - - assert(emptyResult.lineOffsets) - assert(#emptyResult.lineOffsets == 3, "Should have 3 line offsets including empty line") - assert(emptyResult.lineOffsets[1] == 0, "First line should start at offset 0") - assert(emptyResult.lineOffsets[2] == 12, "Second line (empty) should start after 'local x = 1\\n'") - assert(emptyResult.lineOffsets[3] == 13, "Third line should start after empty line") -end - -test_tokenContainsLeadingSpaces() -test_tokenContainsLeadingNewline() -test_tokenContainsLeadingSingleLineComment() -test_tokenContainsLeadingBlockComment() -test_tokenizeWhitespace() -test_triviaSplitBetweenLeadingAndTrailing() -test_roundtrippableAst() -test_lineOffsetsField() diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh new file mode 100755 index 000000000..6dd717cb0 --- /dev/null +++ b/tools/bootstrap.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +set -e +install_requested=false + +# simple argument parsing +for arg in "$@"; do + if [[ "$arg" == "--install" ]]; then + install_requested=true + break + fi +done + +# function to display commands before running them +exe() { echo ": $@" ; "$@" ; } + +fetch_dependency() { + local dep_file="$1" + + # check the file exists + if [[ ! -f "$dep_file" ]]; then + echo "missing dependency file: $dep_file" + return 1 + fi + + # extract each field by key + name=$(grep '^name' "$file" | sed -E 's/^name *= *"(.*)"/\1/') + remote=$(grep '^remote' "$file" | sed -E 's/^remote *= *"(.*)"/\1/') + branch=$(grep '^branch' "$file" | sed -E 's/^branch *= *"(.*)"/\1/') + revision=$(grep '^revision' "$file" | sed -E 's/^revision *= *"(.*)"/\1/') + + # output the parsed variables + local target_dir="extern/$name" + + # get git's version string to determine whether to use revision or branch clones (e.g., "git version 2.50.1") + version_str=$(git --version) + # Extract the major and minor version numbers using regex + # e.g., 2 and 50 from "git version 2.50.1" + if [[ $version_str =~ ([0-9]+)\.([0-9]+)\.[0-9]+ ]]; then + gitMajor="${BASH_REMATCH[1]}" + gitMinor="${BASH_REMATCH[2]}" + + # git 2.49 added support for `--revision` to `git clone` + if [[ "$gitMajor" -ge 3 || ( "$gitMajor" -eq 2 && "$gitMinor" -ge 49 ) ]]; then + exe git clone --depth=1 --revision $revision $remote $target_dir + else + exe git clone --depth=1 --branch $branch $remote $target_dir + fi + else + echo "ill-formed git version string: $version_str" + exit 1 + fi +} + +# download dependencies from the tune files +find "extern" -mindepth 1 ! -name "*.tune" -prune -exec rm -rf {} + +for file in extern/*.tune; do + if [[ -f "$file" ]]; then + echo "fetching $(basename "$file")" + fetch_dependency "extern/$(basename "$file")" + fi +done + +# place empty versions of the standard library +rm -rf lute/std/src/generated +mkdir -p lute/std/src/generated + +exe cp ./tools/templates/std_impl.cpp ./lute/std/src/generated/modules.cpp +exe cp ./tools/templates/std_header.h ./lute/std/src/generated/modules.h + +# place empty versions of the luau-based lute subcommands for the cli +rm -rf lute/cli/generated +mkdir -p lute/cli/generated + +exe cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp +exe cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h + +## configure the build system for lute0 +os_type="$(uname)" +BUILD_ROOT="" +EXE_PATH=lute/cli/lute +OUT_BINARY=./build/lute0 +if [[ "$os_type" == "Darwin" ]]; then + BUILD_ROOT=build/xcode +elif [[ "$os_type" == MINGW* || "$os_type" == MSYS* || "$os_type" == CYGWIN* ]]; then + BUILD_ROOT=build/vs2022 + EXE_PATH+=".exe" + OUT_BINARY+=.exe +else + BUILD_ROOT=build +fi + +BUILD_PATH=$BUILD_ROOT/debug +rm -rf build && mkdir build +exe cmake -G=Ninja -B $BUILD_PATH -DCMAKE_BUILD_TYPE=Debug + +# build lute0 +exe ninja -C $BUILD_PATH $EXE_PATH +echo "" +echo "built lute0 at $BUILD_PATH/$EXE_PATH" + +# use lute0 to build lute with the standard library included. +LUTESTRAP=./$BUILD_PATH/$EXE_PATH +mv $LUTESTRAP $OUT_BINARY +exe $OUT_BINARY tools/luthier.luau configure --config release --clean lute +exe $OUT_BINARY tools/luthier.luau build --config release --clean lute + +# optionally install the final built lute version +INSTALL_DIR=$HOME/.lute/bin +BUILD_PATH=$BUILD_ROOT/release +LUTESTRAP=./$BUILD_PATH/$EXE_PATH +if $install_requested; then + # TODO: give the lute executable a self-install subcommand and just invoke that here instead + read -p "where would you like to install lute? (default: $INSTALL_DIR): " USER_PATH + + # If user_input is empty, keep default_path; else update it + if [[ -n "$USER_PATH" ]]; then + INSTALL_DIR=$USER_PATH + fi + mkdir -p $INSTALL_DIR + cp $LUTESTRAP $INSTALL_DIR + echo "" + echo "installed lute to $INSTALL_DIR/lute" + echo "please ensure this is accessible on your \$PATH" +fi diff --git a/tools/luthier.luau b/tools/luthier.luau index 9ccd67ea3..ba51fccea 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -35,6 +35,19 @@ local function gitVersion(): { major: number, minor: number, path: number } } end +local function readFileToString(path: string): string + local file = fs.open(path, "r") + local contents = fs.read(file) + fs.close(file) + return contents +end + +local function writeStringToFile(path: string, contents: string): () + local file = fs.open(path, "w+") + fs.write(file, contents) + fs.close(file) +end + local gitVersionInfo = gitVersion() local os = string.lower(system.os) @@ -66,6 +79,7 @@ args:add("clean", "flag", { help = "perform a clean build" }) args:add("which", "flag", { help = "print out the path to the compiled binary and exit", aliases = { "w" } }) args:add("cxx-compiler", "option", { help = "C++ compiler to use" }) args:add("c-compiler", "option", { help = "C compiler to use" }) +args:add("enable-sanitizers", "flag", { help = "enable sanitizers during configuration" }) if not isWindows and not isMac and not isLinux then error("Unknown platform " .. os) @@ -233,7 +247,7 @@ local function getStdLibHash(): string traverseFileTree(libsPath, function(path, relativePath) if safeFsType(path) == "file" then toHash ..= "./" .. relativePath - toHash ..= fs.readfiletostring(path) + toHash ..= readFileToString(path) end end) @@ -248,7 +262,7 @@ local function isGeneratedStdLibUpToDate(): boolean return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getStdLibHash() end @@ -290,7 +304,7 @@ local function generateStdLibFilesIfNeeded() local aliasedPath = "@std/" .. relativePath if safeFsType(path) == "file" then - local libSrc = fs.readfiletostring(path) + local libSrc = readFileToString(path) if #libSrc > 16_000 then -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 -- MSVC can't handle a single string literal being over 16380 single-byte characters, @@ -299,11 +313,14 @@ local function generateStdLibFilesIfNeeded() local lines = string.split(libSrc, "\n") local generatedLines = table.create(#lines) for _, line in lines do - table.insert(generatedLines, `"{string.gsub(line, '"', '\\"')}\\n"`) + local escapedLine = string.gsub(line, "\\", "\\\\") + escapedLine = string.gsub(escapedLine, '"', '\\"') + escapedLine = string.gsub(escapedLine, "\r", "") + table.insert(generatedLines, `"{escapedLine}\\n"`) end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else - fs.write(cpp, `\n \{"{aliasedPath}", R"({libSrc})"},\n`) + fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_STDLIB({libSrc})LUTE_STDLIB"},\n`) end end @@ -336,15 +353,21 @@ end local function getTypeDefinitionsHash(): string local libsPath = projectRelative("definitions") + local stdPath = projectRelative("lute", "std", "libs") local toHash = "" - traverseFileTree(libsPath, function(path, relativePath) - if safeFsType(path) == "file" then - toHash ..= "./" .. relativePath - toHash ..= fs.readfiletostring(path) - end - end) + local function traverseDefs(path: string, namespace: string) + traverseFileTree(path, function(path, relativePath) + if safeFsType(path) == "file" then + toHash ..= `./{namespace}/{relativePath}` + toHash ..= readFileToString(path) + end + end) + end + + traverseDefs(libsPath, "lute") + traverseDefs(stdPath, "std") local digest = crypto.digest(crypto.hash.blake2b256, toHash) return hexify(digest) @@ -357,7 +380,7 @@ local function isGeneratedTypeDefinitionsUpToDate(): boolean return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getTypeDefinitionsHash() end @@ -372,17 +395,24 @@ local function generateTypeDefinitionFiles() print("Type definitions out of date, creating new ones.") + local libsPath = projectRelative("definitions") + local stdPath = projectRelative("lute", "std", "libs") local dictionaryEntries: { [string]: string } = {} - for _, file in fs.listdir("definitions") do - if file.type ~= "file" then - continue - end + local function traverseDefs(path: string, namespace: string) + traverseFileTree(path, function(path, relativePath) + if safeFsType(path) == "file" then + local content = readFileToString(path) + dictionaryEntries[`{namespace}/{relativePath}`] = content + end + end) + end - local content = fs.readasync(`definitions/{file.name}`) + traverseDefs(libsPath, "lute") + traverseDefs(stdPath, "std") - dictionaryEntries[file.name] = content - end + dictionaryEntries["lint/types.luau"] = + readFileToString(projectRelative("lute", "cli", "commands", "lint", "types.luau")) local stringDictionary = "" @@ -394,8 +424,8 @@ local function generateTypeDefinitionFiles() fs.write(hash, getTypeDefinitionsHash()) fs.close(hash) - fs.writestringtofile( - "lute/cli/commands/setup/generated/definitions.luau", + writeStringToFile( + projectRelative("lute", "cli", "commands", "setup", "generated", "definitions.luau"), string.format(TYPEDEF_PATTERN, stringDictionary) ) end @@ -407,7 +437,7 @@ local function getCliCommandsHash() traverseFileTree(libsPath, function(path, relativePath) if safeFsType(path) == "file" then toHash ..= "./" .. relativePath - toHash ..= fs.readfiletostring(path) + toHash ..= readFileToString(path) end end) @@ -422,7 +452,7 @@ local function isGeneratedCliCommandsUpToDate() return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getCliCommandsHash() end @@ -459,7 +489,7 @@ local function generateCliCommandsFilesIfNeeded() local aliasedPath = "@cli/" .. relativePath if safeFsType(path) == "file" then - local libSrc = fs.readfiletostring(path) + local libSrc = readFileToString(path) if #libSrc > 16_000 then -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 -- MSVC can't handle a single string literal being over 16380 single-byte characters, @@ -468,11 +498,14 @@ local function generateCliCommandsFilesIfNeeded() local lines = string.split(libSrc, "\n") local generatedLines = table.create(#lines) for _, line in lines do - table.insert(generatedLines, `"{string.gsub(line, '"', '\\"')}\\n"`) + local escapedLine = string.gsub(line, "\\", "\\\\") + escapedLine = string.gsub(escapedLine, '"', '\\"') + escapedLine = string.gsub(escapedLine, "\r", "") + table.insert(generatedLines, `"{escapedLine}\\n"`) end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else - fs.write(cpp, `\n \{"{aliasedPath}", R"({libSrc})"},\n`) + fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_CMD({libSrc})LUTE_CMD"},\n`) end end @@ -505,8 +538,8 @@ end local function generateFilesIfNeeded() generateStdLibFilesIfNeeded() - generateCliCommandsFilesIfNeeded() generateTypeDefinitionFiles() + generateCliCommandsFilesIfNeeded() end local function getExePath(): string @@ -566,11 +599,15 @@ local function getConfigureArguments() table.insert(configArgs, "-DCMAKE_C_COMPILER=" .. args:get("c-compiler")) end + if args:has("enable-sanitizers") then + table.insert(configArgs, "-DLUTE_ENABLE_SANITIZERS=ON") + end + return configArgs end local function readTuneFile(path: string): Tune - return toml.deserialize(fs.readasync(path)) + return toml.deserialize(readFileToString(path)) end local function check(exitCode: number) @@ -639,7 +676,7 @@ local function getTuneFilesHash(): string end toHash ..= file.name - toHash ..= fs.readfiletostring(joinPath(externPath, file.name)) + toHash ..= readFileToString(joinPath(externPath, file.name)) end local digest = crypto.digest(crypto.hash.blake2b256, toHash) @@ -653,7 +690,7 @@ local function areTuneFilesUpToDate(): boolean return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getTuneFilesHash() end @@ -693,7 +730,10 @@ local function configure() table.move(arguments, 1, #arguments, 2, cmd) check(fetchDependenciesIfNeeded()) - return call(cmd) + + local result = call(cmd) + fs.copy(joinPath(getProjectPath(), "compile_commands.json"), projectRelative(BUILD_DIR, "compile_commands.json")) + return result end local function run() @@ -733,7 +773,7 @@ elseif subcommand == "configure" or subcommand == "tune" then elseif subcommand == "build" or subcommand == "craft" then generateFilesIfNeeded() - if not projectPathExists() then + if not projectPathExists() or not areTuneFilesUpToDate() then check(configure()) end @@ -741,7 +781,7 @@ elseif subcommand == "build" or subcommand == "craft" then elseif subcommand == "run" or subcommand == "play" then generateFilesIfNeeded() - if not projectPathExists() then + if not projectPathExists() or not areTuneFilesUpToDate() then check(configure()) end diff --git a/tools/profile.luau b/tools/profile.luau new file mode 100644 index 000000000..3e7259428 --- /dev/null +++ b/tools/profile.luau @@ -0,0 +1,32 @@ +local task = require("@lute/task") + +local started = os.clock() + +local amount = 100000 +local batches = 5 +local per_batch = amount / batches + +for current = 1, batches do + local thread = coroutine.running() + + print(`Batch {current} / {batches}`) + + for i = 1, per_batch do + --print("Spawning thread #" .. i) + task.spawn(function() + task.wait(0.1) + --_TEST_ASYNC_WORK(0.1) + if i == per_batch then + print("Last thread in batch #" .. current) + assert(coroutine.status(thread) == "suspended", `Thread {i} has status {coroutine.status(thread)}`) + task.spawn(thread) + end + end) + end + + coroutine.yield() +end +local took = os.clock() - started +print(`Spawned {amount} sleeping threads in {took}s`) + +return -1 diff --git a/tools/profile.sh b/tools/profile.sh new file mode 100755 index 000000000..c1eab24fc --- /dev/null +++ b/tools/profile.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +DEFAULT_SCRIPTNAME=${1:-profile.luau} +echo "profiling $DEFAULT_SCRIPTNAME" +xctrace record --template 'Time Profiler' --launch -- ./build/xcode/debug/lute/cli/lute $DEFAULT_SCRIPTNAME diff --git a/tools/templates/cli_header.h b/tools/templates/cli_header.h new file mode 100644 index 000000000..5ff503f74 --- /dev/null +++ b/tools/templates/cli_header.h @@ -0,0 +1,9 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. +#pragma once + +#include +#include + +extern const std::pair luteclicommands[1]; + diff --git a/tools/templates/cli_impl.cpp b/tools/templates/cli_impl.cpp new file mode 100644 index 000000000..03f2de9d1 --- /dev/null +++ b/tools/templates/cli_impl.cpp @@ -0,0 +1,8 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. + +#include +#include "commands.h" + +const std::pair luteclicommands[] = {{"", ""}}; + diff --git a/tools/templates/std_header.h b/tools/templates/std_header.h new file mode 100644 index 000000000..da5a7ca7d --- /dev/null +++ b/tools/templates/std_header.h @@ -0,0 +1,9 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. +#pragma once + +#include +#include + +extern const std::pair lutestdlib[1]; + diff --git a/tools/templates/std_impl.cpp b/tools/templates/std_impl.cpp new file mode 100644 index 000000000..fd88f5c16 --- /dev/null +++ b/tools/templates/std_impl.cpp @@ -0,0 +1,8 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. + +#include +#include "modules.h" + +constexpr std::pair lutestdlib[] = {{"", ""}}; +