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/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..65fbad6f6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# Default +* @luau-lang/lute-contrib + +# GitHub configuration +/.github/ @luau-lang/lute-maintainers + +# Package management +/lute/cli/commands/pkg/ @luau-lang/lute-pkg-maintainers +/tests/cli/pkg/ @luau-lang/lute-pkg-maintainers diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index def71560d..66514d511 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -16,6 +16,14 @@ 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" + install-system-deps: + description: "Whether to install system build dependencies via apt-get. Set to 'false' when the caller provisions its own toolchain (e.g. inside a custom container)." + required: false + default: "true" outputs: exe_path: @@ -26,17 +34,18 @@ runs: using: "composite" steps: - name: Install tools + if: ${{ inputs.use-bootstrap == 'false' }} uses: Roblox/setup-foreman@v3 with: token: ${{ inputs.token }} allow-external-github-orgs: true - name: Install dependencies (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && inputs.install-system-deps == 'true' shell: bash run: | sudo apt-get update - sudo apt-get install -y ninja-build clang + sudo apt-get install -y ninja-build clang ccache - name: Install dependencies (macOS) if: runner.os == 'macOS' @@ -44,6 +53,12 @@ runs: run: | brew update brew unlink brotli zstd + brew install ccache + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + shell: bash + run: choco install ccache -y - name: Set up MSVC if: runner.os == 'Windows' @@ -54,29 +69,76 @@ runs: uses: luau-lang/setup-nasm@v1 - name: Cache extern folder - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-extern with: path: extern key: extern-${{ hashFiles('extern/*.tune') }} + - name: Configure ccache + shell: bash + run: | + echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV + echo "CCACHE_MAXSIZE=500M" >> $GITHUB_ENV + echo "CCACHE_COMPRESS=true" >> $GITHUB_ENV + echo "CCACHE_COMPRESSLEVEL=6" >> $GITHUB_ENV + # zero statistics + ccache -z + + - name: Restore ccache + uses: actions/cache/restore@v5 + id: cache-ccache + with: + path: .ccache + key: ccache-${{ runner.os }}-${{ inputs.options }}- + lookup-only: false + + + - name: ccache stats (pre-build) + shell: bash + run: | + echo "Cache status: ${{ steps.cache-ccache.outputs.cache-hit }}" + ccache -s + + - name: Bootstrap Lute (if needed) and set LUTE + shell: bash + run: | + if [[ "${{ inputs.use-bootstrap }}" == "true" ]]; then + echo "Bootstrapping Lute..." + ./tools/bootstrap.sh --with-ccache --build-debug + echo "LUTE=./build/lute0" >> $GITHUB_ENV + else + echo "LUTE=lute" >> $GITHUB_ENV + fi + - name: Fetch ${{ inputs.target }} if: steps.cache-extern.outputs.cache-hit != 'true' shell: bash - run: lute ./tools/luthier.luau fetch ${{ inputs.target }} + run: $LUTE ./tools/luthier.luau fetch ${{ inputs.target }} - name: Configure ${{ inputs.target }} shell: bash - run: lute ./tools/luthier.luau configure ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} + run: $LUTE ./tools/luthier.luau configure ${{ inputs.target }} --with-ccache --config ${{ inputs.config }} ${{ inputs.options }} - name: Build ${{ inputs.target }} shell: bash - run: lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} + run: $LUTE ./tools/luthier.luau build ${{ inputs.target }} --with-ccache --config ${{ inputs.config }} ${{ inputs.options }} + + - name: ccache stats (post-build) + shell: bash + run: ccache -s + + - name: Save ccache + if: github.ref_name == 'primary' + uses: actions/cache/save@v5 + with: + path: .ccache + key: ccache-${{ runner.os }}-${{ inputs.options }}-${{ github.sha }} - name: Get Artifact Path id: artifact_path shell: bash run: | - exe_path=$(lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) + exe_path=$($LUTE ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) echo "Executable path: $exe_path" # Debug print echo "exe_path=$exe_path" >> "$GITHUB_OUTPUT" diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 000000000..ff74e01df --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,2 @@ +# Set to author to set pr creator as assignee +addAssignees: author diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde0f7a06..4580dd8f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,39 +1,101 @@ -name: build +name: CI on: push: - branches: ["primary"] + branches: ["primary", "release/**"] 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: 20 strategy: matrix: include: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe + run-on-event: all + - os: macos-latest + options: --enable-sanitizers + run-on-event: all - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers + run-on-event: pull_request - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ - - os: macos-latest + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers + run-on-event: pull_request + - os: ubuntu-latest + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers + use-bootstrap: true + run-on-event: push + - os: ubuntu-latest + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers + use-bootstrap: true + run-on-event: push fail-fast: false steps: - - uses: actions/checkout@v4 + - name: Determine if this matrix job should run + id: should_run + shell: bash + run: | + if [[ "${{ matrix.run-on-event }}" == "all" || "${{ matrix.run-on-event }}" == "${{ github.event_name }}" ]]; then + echo "proceed=true" >> "$GITHUB_OUTPUT" + else + echo "proceed=false" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@v6 - name: Setup and Build Lute + id: build_lute + if: ${{ steps.should_run.outputs.proceed == 'true' }} 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' }} - test: + - name: Mount cross-device tmpfs for fs.move test + if: ${{ steps.should_run.outputs.proceed == 'true' && runner.os == 'Linux' }} + run: | + sudo mkdir /mnt/lute-test-tmp + sudo mount -t tmpfs tmpfs /mnt/lute-test-tmp + sudo chmod 777 /mnt/lute-test-tmp + echo "LUTE_CROSSDEV_TMPDIR=/mnt/lute-test-tmp" >> "$GITHUB_ENV" + + - name: Mount cross-device RAM disk for fs.move test + if: ${{ steps.should_run.outputs.proceed == 'true' && runner.os == 'macOS' }} + run: | + DISK=$(hdiutil attach -nomount ram://131072 | xargs) + diskutil erasevolume HFS+ 'LuteTestDisk' "$DISK" + echo "LUTE_CROSSDEV_TMPDIR=/Volumes/LuteTestDisk" >> "$GITHUB_ENV" + + - name: Set up cross-device directory for fs.move test + if: ${{ steps.should_run.outputs.proceed == 'true' && runner.os == 'Windows' }} + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path "D:\lute-test-tmp" + echo "LUTE_CROSSDEV_TMPDIR=D:\lute-test-tmp" >> $env:GITHUB_ENV + + - name: Run Luau Tests + if: ${{ steps.should_run.outputs.proceed == 'true' }} + run: ${{ steps.build_lute.outputs.exe_path }} test --verbose + - name: Run Luau Snapshots + if: ${{ steps.should_run.outputs.proceed == 'true' }} + run: ${{ steps.build_lute.outputs.exe_path }} test snapshots + + run-lute-cxx-unittests: runs-on: ${{ matrix.os }} strategy: @@ -42,17 +104,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 + uses: actions/checkout@v6 - - name: Setup and Build Tests + - name: Setup and Build C++ Unit Tests id: build_tests uses: ./.github/actions/setup-and-build with: @@ -60,18 +123,17 @@ 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: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools uses: Roblox/setup-foreman@v3 @@ -80,15 +142,147 @@ jobs: allow-external-github-orgs: true - name: Run StyLua - run: stylua --check . + run: stylua --check . .lute + + build-docs-site: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - 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 }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + + - name: Install dependencies + run: npm install + working-directory: docs + + - name: Build docs site + run: | + export PATH="$(dirname '${{ steps.build_lute.outputs.exe_path }}'):$PATH" + npm run build + working-directory: docs + + - name: Upload docs artifact + id: deployment + uses: actions/upload-pages-artifact@v5 + with: + path: docs/.vitepress/dist/ + + lute-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - 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 }} + + - name: Run Lute Lint + run: ${{ steps.build_lute.outputs.exe_path }} lint -v . -c lint.config.luau + + lute-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: debug + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Lute Check + shell: bash + run: | + # @lute typechecks + find definitions -name "*.luau" -exec ${{ steps.build_lute.outputs.exe_path }} check {} + + # @std and @cli commands typechecks + find lute -name "*.luau" -not -path '*/extern/*' -exec ${{ steps.build_lute.outputs.exe_path }} check {} + + # @batteries typechecks + find batteries -name "*.luau" -exec ${{ steps.build_lute.outputs.exe_path }} check {} + + # All tests typecheck (special case for tests/src because they mess with config files) + find tests -name "*.luau" -not -path "tests/src/*" -exec ${{ steps.build_lute.outputs.exe_path }} check {} + + # All tools typecheck + find tools -name "*.luau" -exec ${{ steps.build_lute.outputs.exe_path }} check {} + + # All examples typecheck + find examples -name "*.luau" -exec ${{ steps.build_lute.outputs.exe_path }} check {} + aggregator: name: Gated Commits runs-on: ubuntu-latest needs: - - build + [ + run-lutecli-luau-tests, + run-lute-cxx-unittests, + check-format, + build-docs-site, + lute-lint, + lute-check, + ] if: ${{ always() }} steps: - - name: Aggregate + - name: Aggregator + id: 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" ] && [ "${{ needs.lute-check.result }}" == "success" ]; then + echo "All jobs succeeded" + echo "result=success" >> "$GITHUB_OUTPUT" + else + echo "One or more jobs failed" + echo "result=failure" >> "$GITHUB_OUTPUT" + exit 1 + fi + - name: Post-aggregation notification + if: > + always() && + github.event_name == 'push' && + github.ref == 'refs/heads/primary' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + run: | + LAST_STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/luau-lang/lute/actions/workflows/ci.yml/runs?branch=primary&status=completed&per_page=1" \ + | jq -r '.workflow_runs[0].conclusion') + + if [[ "$LAST_STATUS" == "${{ steps.aggregator.outputs.result }}" ]]; then + echo "No status change detected. Skipping notification." + exit 0 + elif [[ "$LAST_STATUS" == "failure" ]]; then + TEXT=":white_check_mark: *Lute CI is passing again*" + elif [[ "$LAST_STATUS" == "success" ]]; then + TEXT=":rotating_light: *Lute CI has started failing*" + fi + + echo Sending Slack notification: "$TEXT" + + CHANNEL_ID="${{ secrets.SLACK_CHANNEL_ID }}" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + curl -sS -o /dev/null -X POST https://slack.com/api/chat.postMessage \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json; charset=utf-8" \ + --data "{ + \"channel\": \"$CHANNEL_ID\", + \"text\": \"$TEXT (<$RUN_URL|view workflow run>)\" + }" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..deb729e74 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,147 @@ +name: Docker + +on: + workflow_dispatch: + inputs: + version: + description: "Lute version to publish (e.g. 1.0.0). Must match an existing GitHub release tag." + required: true + type: string + workflow_call: + inputs: + version: + description: "Lute version to publish." + required: true + type: string + +jobs: + docker: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + id-token: write + attestations: write + artifact-metadata: write + + env: + IMAGE: ghcr.io/${{ github.repository }} + VERSION: ${{ inputs.version }} + + steps: + - name: Validate version + run: | + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid version: $VERSION. Expected semver like 1.0.0 (no nightly suffix)." + exit 1 + fi + + - name: Verify release exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release view "v$VERSION" --repo "${{ github.repository }}" > /dev/null + + - name: Checkout code + uses: actions/checkout@v6 + with: + sparse-checkout: docker + + - name: Compute version tags + id: tags + run: | + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f1-2) + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push bin image + id: build_bin + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/bin.dockerfile + platforms: linux/amd64,linux/arm64 + provenance: false # SLSA provenance is attached separately via actions/attest + push: true + build-args: | + LUTE_VERSION=${{ env.VERSION }} + tags: | + ${{ env.IMAGE }}:bin + ${{ env.IMAGE }}:bin-${{ steps.tags.outputs.major }} + ${{ env.IMAGE }}:bin-${{ steps.tags.outputs.minor }} + ${{ env.IMAGE }}:bin-${{ env.VERSION }} + + - name: Attest bin image + uses: actions/attest@v4 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.build_bin.outputs.digest }} + push-to-registry: true + + - name: Build and push debian image + id: build_debian + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/debian.dockerfile + platforms: linux/amd64,linux/arm64 + provenance: false # SLSA provenance is attached separately via actions/attest + push: true + build-args: | + LUTE_VERSION=${{ env.VERSION }} + BIN_IMAGE=${{ env.IMAGE }}:bin-${{ env.VERSION }} + tags: | + ${{ env.IMAGE }}:latest + ${{ env.IMAGE }}:${{ steps.tags.outputs.major }} + ${{ env.IMAGE }}:${{ steps.tags.outputs.minor }} + ${{ env.IMAGE }}:${{ env.VERSION }} + ${{ env.IMAGE }}:debian + ${{ env.IMAGE }}:debian-${{ steps.tags.outputs.major }} + ${{ env.IMAGE }}:debian-${{ steps.tags.outputs.minor }} + ${{ env.IMAGE }}:debian-${{ env.VERSION }} + + - name: Attest debian image + uses: actions/attest@v4 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.build_debian.outputs.digest }} + push-to-registry: true + + - name: Build and push distroless image + id: build_distroless + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/distroless.dockerfile + platforms: linux/amd64,linux/arm64 + provenance: false # SLSA provenance is attached separately via actions/attest + push: true + build-args: | + LUTE_VERSION=${{ env.VERSION }} + BIN_IMAGE=${{ env.IMAGE }}:bin-${{ env.VERSION }} + tags: | + ${{ env.IMAGE }}:distroless + ${{ env.IMAGE }}:distroless-${{ steps.tags.outputs.major }} + ${{ env.IMAGE }}:distroless-${{ steps.tags.outputs.minor }} + ${{ env.IMAGE }}:distroless-${{ env.VERSION }} + + - name: Attest distroless image + uses: actions/attest@v4 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.build_distroless.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ffda5489c..037a20d05 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,28 +11,34 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Setup Node.js - uses: actions/setup-node@v4 - - - name: Install tools - uses: Roblox/setup-foreman@v3 + - 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 }} - allow-external-github-orgs: true + use-bootstrap: true + + - name: Setup Node.js + uses: actions/setup-node@v6 - name: Install dependencies run: npm install working-directory: docs - name: Build docs site - run: npm run build + run: | + export PATH="$(dirname '${{ steps.build_lute.outputs.exe_path }}'):$PATH" + npm run build working-directory: docs - name: Upload docs artifact id: deployment - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: docs/.vitepress/dist/ @@ -51,4 +57,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..f686894be --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,61 @@ +name: Nightly Release + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + proceed: ${{ steps.check.outputs.proceed }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for changes since last nightly tag + id: check + run: | + PROCEED="true" + + if [[ "${{ github.event_name }}" == "schedule" ]]; then + echo "Schedule trigger: Checking changes..." + + LATEST_NIGHTLY_TAG=$(git tag --sort=-creatordate --list '*-nightly.*' | head -n 1) + + if [[ -n "$LATEST_NIGHTLY_TAG" ]]; then + echo "Latest nightly tag found: $LATEST_NIGHTLY_TAG" + LATEST_NIGHTLY_COMMIT=$(git rev-list -n 1 "$LATEST_NIGHTLY_TAG" 2>/dev/null) + CURRENT_HEAD_COMMIT=$(git rev-parse HEAD) + + echo "Commit for tag $LATEST_NIGHTLY_TAG: $LATEST_NIGHTLY_COMMIT" + echo "Current HEAD commit: $CURRENT_HEAD_COMMIT" + + if [[ "$LATEST_NIGHTLY_COMMIT" == "$CURRENT_HEAD_COMMIT" ]]; then + echo "No code changes detected since the last nightly release ($LATEST_NIGHTLY_TAG)." + PROCEED="false" + else + echo "Code changes detected since $LATEST_NIGHTLY_TAG. Proceeding with build." + fi + else + echo "No previous nightly tag found. Proceeding with build." + fi + else + echo "Manual trigger. Proceeding with build." + fi + + echo "proceed=$PROCEED" >> $GITHUB_OUTPUT + + release-common: + needs: changes + if: ${{ needs.changes.outputs.proceed == 'true' }} + permissions: + contents: write + uses: ./.github/workflows/release-common.yml + with: + branch: primary + secrets: inherit diff --git a/.github/workflows/release-common.yml b/.github/workflows/release-common.yml new file mode 100644 index 000000000..b38022ab8 --- /dev/null +++ b/.github/workflows/release-common.yml @@ -0,0 +1,174 @@ +name: release-common + +on: + workflow_call: + inputs: + branch: + description: "Branch to use for the release. 'primary' triggers a nightly build." + required: false + default: "primary" + type: string + outputs: + version: + description: "The resolved Lute version that was released." + value: ${{ jobs.release.outputs.version }} + +env: + IS_NIGHTLY: ${{ inputs.branch == 'primary' }} + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Validate CI status + id: check + run: | + LAST_STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/luau-lang/lute/actions/workflows/ci.yml/runs?branch=${{ inputs.branch }}&status=completed&per_page=1" \ + | jq -r '.workflow_runs[0].conclusion') + if [[ "$LAST_STATUS" != "success" ]]; then + echo "The latest CI run on '${{ inputs.branch }}' branch did not succeed. Aborting release." + exit 1 + fi + + build: + strategy: + matrix: + include: + - os: ubuntu-latest + artifact_name: lute-linux-x86_64 + options: --c-compiler clang --cxx-compiler clang++ + use-bootstrap: true + container: quay.io/pypa/manylinux_2_28_x86_64 + - os: ubuntu-24.04-arm + artifact_name: lute-linux-aarch64 + options: --c-compiler clang --cxx-compiler clang++ + use-bootstrap: true + container: quay.io/pypa/manylinux_2_28_aarch64 + - os: macos-latest + artifact_name: lute-macos-aarch64 + - os: windows-latest + artifact_name: lute-windows-x86_64 + fail-fast: false + + needs: check + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.branch }} + + - name: Set nightly version suffix + if: ${{ env.IS_NIGHTLY == 'true' }} + run: echo "LUTE_VERSION_SUFFIX=nightly.$(date +%Y%m%d)" >> "$GITHUB_ENV" + + - name: Set up Linux release toolchain + if: ${{ matrix.container }} + shell: bash + run: | + set -euo pipefail + dnf install -y --allowerasing \ + llvm-toolset gcc gcc-c++ libstdc++-devel glibc-devel \ + ninja-build perl-IPC-Cmd which ccache + + # Force clang for the whole build (incl. bootstrap, where CMake + # would otherwise auto-pick gcc-toolset-14's g++ from PATH) and + # point it at the system gcc 8.5 install so libstdc++ stays at + # GLIBCXX_3.4.25 / CXXABI_1.3.11 (compatible with glibc 2.28). + GCC_FLAG="--gcc-install-dir=/usr/lib/gcc/$(uname -m)-redhat-linux/8" + { + echo "CC=clang" + echo "CXX=clang++" + echo "CFLAGS=${GCC_FLAG}" + echo "CXXFLAGS=${GCC_FLAG}" + echo "LDFLAGS=${GCC_FLAG}" + } >> "$GITHUB_ENV" + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: release + options: ${{ matrix.options }} + token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} + install-system-deps: ${{ matrix.container && 'false' || 'true' }} + + - name: Upload Artifact + uses: actions/upload-artifact@v7 + with: + path: ${{ steps.build_lute.outputs.exe_path }} + name: ${{ matrix.artifact_name }} + + release: + needs: [check, build] + runs-on: ubuntu-latest + + permissions: + contents: write + + outputs: + version: ${{ steps.get_version.outputs.version }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.branch }} + sparse-checkout: CMakeBuild/get_version.cmake + + - name: Set nightly version suffix + if: ${{ env.IS_NIGHTLY == 'true' }} + run: echo "LUTE_VERSION_SUFFIX=nightly.$(date +%Y%m%d)" >> "$GITHUB_ENV" + + - name: Download Artifact + uses: actions/download-artifact@v8 + with: + path: artifacts + + - run: | + mkdir release + for dir in ./artifacts/*; do + base_name=$(basename "$dir") + chmod 755 "$dir"/* # Artifacts don't preserve permissions, so re-add them. + zip -rj release/"${base_name}.zip" "$dir"/* + done + + - name: Get project version + id: get_version + run: | + VERSION=$(cmake -P CMakeBuild/get_version.cmake 2>&1) + echo "Resolved version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Create release tag + id: tag_release + run: | + TAG="v${{ steps.get_version.outputs.version }}" + echo "Release tag: $TAG" + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - 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: true + prerelease: ${{ env.IS_NIGHTLY == 'true' }} + generate_release_notes: ${{ env.IS_NIGHTLY == '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/release.yml b/.github/workflows/release.yml index a52f96517..873714dca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,154 +1,41 @@ -name: Build and Release +name: Release on: - schedule: - - cron: '0 0 * * *' workflow_dispatch: - inputs: - nightly: - description: "Trigger a nightly build" - required: false - default: false - type: boolean jobs: - check: + validate: runs-on: ubuntu-latest - outputs: - proceed: ${{ steps.check.outputs.proceed }} - permissions: - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Check for changes since last nightly tag - id: check + steps: + - name: Validate branch format run: | - PROCEED="true" - - if [[ "${{ github.event_name }}" == "schedule" ]]; then - echo "Schedule trigger: Checking changes..." - - LATEST_NIGHTLY_TAG=$(git tag --sort=-creatordate --list '*-nightly.*' | head -n 1) - - if [[ -n "$LATEST_NIGHTLY_TAG" ]]; then - echo "Latest nightly tag found: $LATEST_NIGHTLY_TAG" - LATEST_NIGHTLY_COMMIT=$(git rev-list -n 1 "$LATEST_NIGHTLY_TAG" 2>/dev/null) - CURRENT_HEAD_COMMIT=$(git rev-parse HEAD) - - echo "Commit for tag $LATEST_NIGHTLY_TAG: $LATEST_NIGHTLY_COMMIT" - echo "Current HEAD commit: $CURRENT_HEAD_COMMIT" + BRANCH="${{ github.ref_name }}" - if [[ "$LATEST_NIGHTLY_COMMIT" == "$CURRENT_HEAD_COMMIT" ]]; then - echo "No code changes detected since the last nightly release ($LATEST_NIGHTLY_TAG)." - PROCEED="false" - else - echo "Code changes detected since $LATEST_NIGHTLY_TAG. Proceeding with build." - fi - else - echo "No previous nightly tag found. Proceeding with build." - fi - else - echo "Manual trigger ('${{ github.event_name }}'). Proceeding with build." + if [[ ! "$BRANCH" =~ ^release/v[0-9]+\.[0-9]+\.x$ ]]; then + echo "Invalid branch format: $BRANCH. Expected release/v{Major}.{Minor}.x" + echo "Dispatch this workflow on a release branch (e.g. release/v0.1.x)" + exit 1 fi - echo "proceed=$PROCEED" >> $GITHUB_OUTPUT - - build: - strategy: - matrix: - include: - - os: ubuntu-22.04 - artifact_name: lute-linux-x86_64 - options: --c-compiler clang --cxx-compiler clang++ - - os: ubuntu-22.04-arm - artifact_name: lute-linux-aarch64 - options: --c-compiler clang --cxx-compiler clang++ - - os: macos-latest - artifact_name: lute-macos-aarch64 - - os: windows-latest - artifact_name: lute-windows-x86_64 - fail-fast: false - - needs: check - if: ${{ needs.check.outputs.proceed == 'true' }} - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup and Build Lute - id: build_lute - uses: ./.github/actions/setup-and-build - with: - target: Lute.CLI - config: release - options: ${{ matrix.options }} - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - path: ${{ steps.build_lute.outputs.exe_path }} - name: ${{ matrix.artifact_name }} - - release: - needs: build - runs-on: ubuntu-latest - + release-common: + needs: validate permissions: contents: write + uses: ./.github/workflows/release-common.yml + with: + branch: ${{ github.ref_name }} + secrets: inherit - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - sparse-checkout: CMakeBuild/get_version.cmake - - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - path: artifacts - - - run: | - mkdir release - for dir in ./artifacts/*; do - base_name=$(basename "$dir") - zip -rj release/"${base_name}.zip" "$dir"/* - done - - - name: Get project version - id: get_version - run: | - VERSION=$(cmake -P CMakeBuild/get_version.cmake 2>&1) - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Create Nightly Tag - id: tag_step - if: github.event_name == 'schedule' || github.event.inputs.nightly - run: | - DATE=$(date +%Y%m%d) - TAG="-nightly.$DATE" - echo "tag=$TAG" >> $GITHUB_OUTPUT - - - name: Create Release Tag - id: tag_release - run: | - VERSION=${{ steps.get_version.outputs.version }} - NIGHTLY_TAG=${{ steps.tag_step.outputs.tag }} - RELEASE_TAG="$VERSION$NIGHTLY_TAG" - echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT - - - name: Create Release - uses: luau-lang/action-gh-release@v2 - with: - tag_name: ${{ steps.tag_release.outputs.tag }} - name: ${{ steps.tag_release.outputs.tag }} - draft: false - prerelease: ${{ github.event.inputs.nightly || github.event_name == 'schedule' }} - files: release/*.zip + docker: + needs: release-common + permissions: + contents: read + packages: write + id-token: write + attestations: write + artifact-metadata: write + uses: ./.github/workflows/docker.yml + with: + version: ${{ needs.release-common.outputs.version }} + secrets: inherit diff --git a/.github/workflows/update-prs.yml b/.github/workflows/update-prs.yml new file mode 100644 index 000000000..0a0bb8283 --- /dev/null +++ b/.github/workflows/update-prs.yml @@ -0,0 +1,180 @@ +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 + outputs: + pull-request-url: ${{ steps.create-pr.outputs.pull-request-url }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - 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 + id: create-pr + if: steps.current-version.outputs.branch != steps.latest-release.outputs.tag + uses: luau-lang/create-pull-request@v7.0.9 + 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 + base: primary + + - 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 + outputs: + pull-request-url: ${{ steps.create-pr.outputs.pull-request-url }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - 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 + + # rokit/foreman versions are SemVer and must not include a leading "v" + LATEST_LUTE_VERSION="${LATEST_LUTE#v}" + echo "Latest Lute version: $LATEST_LUTE_VERSION" + echo "version=$LATEST_LUTE_VERSION" >> $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.version }}" ]; then + sed -i 's/^lute = "luau-lang\/lute@.*"/lute = "luau-lang\/lute@${{ steps.latest-release.outputs.version }}"/' 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.version }}" }/' foreman.toml + echo "Updated foreman.toml:" + cat foreman.toml + fi + + - name: Create Pull Request + id: create-pr + if: steps.current-version.outputs.lute != steps.latest-release.outputs.version + uses: luau-lang/create-pull-request@v7.0.9 + 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.version)}} + + **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.version + run: | + echo "Lute is already up to date: ${{ steps.current-version.outputs.lute }}" + + notify: + runs-on: ubuntu-latest + needs: [update-luau, update-lute] + if: always() + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + steps: + - name: Send Slack notification + run: | + CHANNEL_ID="${{ secrets.SLACK_CHANNEL_ID }}" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [[ "${{ needs.update-luau.result }}" == "success" && "${{ needs.update-lute.result }}" == "success" ]]; then + TEXT=":meow_code: *Update Luau/Lute workflow succeeded*" + + LUAU_PR="${{ needs.update-luau.outputs.pull-request-url }}" + LUTE_PR="${{ needs.update-lute.outputs.pull-request-url }}" + + if [[ -n "$LUAU_PR" ]]; then + ESCAPED=$'\n• Luau PR: ' + TEXT="$TEXT$ESCAPED <$LUAU_PR>" + fi + if [[ -n "$LUTE_PR" ]]; then + ESCAPED=$'\n• Lute PR: ' + TEXT="$TEXT$ESCAPED <$LUTE_PR>" + fi + else + TEXT=":meow_sad_rain: *Update Luau/Lute workflow failed* (<$RUN_URL|view workflow run>)" + fi + + curl -sS -o /dev/null -X POST https://slack.com/api/chat.postMessage \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json; charset=utf-8" \ + --data "$(jq -n --arg channel "$CHANNEL_ID" --arg text "$TEXT" '{channel: $channel, text: $text}')" diff --git a/.gitignore b/.gitignore index 8a03e7908..fb90d2570 100644 --- a/.gitignore +++ b/.gitignore @@ -13,11 +13,11 @@ __pycache__ .cache .clangd +**/.DS_Store compile_commands.json *~ -extern/libuv/libuv-static.pc -extern/libuv/DartConfiguration.tcl extern/*/ generated **/generated-types.luau WARP.md +.ccache diff --git a/.luaurc b/.luaurc index df8328cad..acee50a66 100644 --- a/.luaurc +++ b/.luaurc @@ -1,8 +1,8 @@ { "languageMode": "strict", "aliases": { - "batteries": "./batteries", "std": "./lute/std/libs", "lute": "./definitions", + "commands": "./lute/cli/commands" } } diff --git a/.lute/bootstrap.luau b/.lute/bootstrap.luau new file mode 100644 index 000000000..5aaa55a55 --- /dev/null +++ b/.lute/bootstrap.luau @@ -0,0 +1,11 @@ +--!strict + +local process = require("@lute/process") + +-- LUAUFIX: should `...` actually have the type `string...` for command-line environments +local args: { [number]: string, n: number } = table.pack(...) :: any + +local cmd: { string } = { "tools/bootstrap.sh", table.unpack(args, 2, args.n) } + +local result = process.run(cmd, { stdio = "inherit" }) +process.exit(result.exitcode) diff --git a/.lute/luthier.luau b/.lute/luthier.luau new file mode 100644 index 000000000..cbbcc7ada --- /dev/null +++ b/.lute/luthier.luau @@ -0,0 +1,11 @@ +--!strict + +local process = require("@lute/process") + +-- LUAUFIX: should `...` actually have the type `string...` for command-line environments +local args: { [number]: string, n: number } = table.pack(...) :: any + +local cmd: { string } = { 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..c5418bbed 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 1.0.1) + +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..35290ae59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,18 @@ cmake_minimum_required(VERSION 3.13) +if(APPLE AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum macOS version") +endif() + # Make sure 'set' for variables is respected by options in nested files set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) +# Let subprojects inherit CMAKE_INTERPROCEDURAL_OPTIMIZATION. +set(CMAKE_POLICY_DEFAULT_CMP0069 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 +22,66 @@ project( LANGUAGES CXX C ) +# Enable sanitizers globally before any subdirectory is added +option(LUTE_ENABLE_SANITIZERS "Enable sanitizers" OFF) +option(LUTE_STDLESS "Build without embedded Luau sources (for bootstrapping)" 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() + +option(LUTE_OPTIMIZE_SIZE "Enable binary size optimizations for release builds" ON) +if (LUTE_OPTIMIZE_SIZE AND NOT LUTE_ENABLE_SANITIZERS) + set(_lute_release_cfg "$>") + + if (NOT MSVC) + add_compile_options( + "$<${_lute_release_cfg}:-ffunction-sections>" + "$<${_lute_release_cfg}:-fdata-sections>" + "$<${_lute_release_cfg}:-fvisibility=hidden>" + "$<${_lute_release_cfg}:$<$:-fvisibility-inlines-hidden>>" + ) + + if (APPLE) + add_link_options("$<${_lute_release_cfg}:-Wl,-dead_strip>") + else() + add_link_options("$<${_lute_release_cfg}:-Wl,--gc-sections>") + endif() + else() + # Restrict to C/C++ so /Gy and /Gw don't leak into NASM invocations. + set(_lute_cxx_lang "$,$>") + add_compile_options( + "$<${_lute_release_cfg}:$<${_lute_cxx_lang}:/Gy>>" + "$<${_lute_release_cfg}:$<${_lute_cxx_lang}:/Gw>>" + ) + add_link_options( + "$<${_lute_release_cfg}:/OPT:REF>" + "$<${_lute_release_cfg}:/OPT:ICF>" + ) + endif() + + include(CheckIPOSupported) + check_ipo_supported(RESULT LUTE_IPO_SUPPORTED OUTPUT LUTE_IPO_ERROR) + if (LUTE_IPO_SUPPORTED) + message(STATUS "Lute: enabling LTO for Release/MinSizeRel/RelWithDebInfo") + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO TRUE) + else() + message(STATUS "Lute: LTO not supported by this toolchain: ${LUTE_IPO_ERROR}") + endif() +endif() + +# LUTE_EXTERN_C must be declared before extern/luau is configured so that +# it can force LUAU_EXTERN_C on — Luau must use longjmp instead of C++ +# exceptions when lute's open functions are exposed with C linkage. +option(LUTE_EXTERN_C "Use extern C Lute API functions" OFF) +if(LUTE_EXTERN_C) + set(LUAU_EXTERN_C ON) +endif() + # luau setup set(LUAU_BUILD_CLI OFF) set(LUAU_BUILD_TESTS OFF) @@ -35,30 +101,56 @@ set(LIBUV_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/libuv/include) # zlib setup # set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "Do not build examples" FORCE) +set(ZLIB_BUILD_TESTING OFF CACHE BOOL "Do not build zlib tests" FORCE) +set(ZLIB_BUILD_SHARED OFF CACHE BOOL "Do not build shared zlib" FORCE) +set(ZLIB_BUILD_STATIC ON CACHE BOOL "Build static zlib" FORCE) +set(ZLIB_INSTALL OFF CACHE BOOL "Do not install zlib" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static zlib" FORCE) add_subdirectory(extern/zlib) +if(TARGET zlibstatic AND NOT TARGET ZLIB::ZLIB) + add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL) + target_link_libraries(ZLIB::ZLIB INTERFACE "$") +endif() set(ZLIB_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extern/zlib") -set(ZLIB_FOUND TRUE CACHE BOOL "" FORCE) - if(MSVC) if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(ZLIB_LIBRARY "${ZLIB_OUTPUT_DIR}/zlibstaticd.lib") + set(ZLIB_LIBRARY "${ZLIB_OUTPUT_DIR}/zlibstaticd.lib" CACHE FILEPATH "" FORCE) else() - set(ZLIB_LIBRARY "${ZLIB_OUTPUT_DIR}/zlibstatic.lib") + set(ZLIB_LIBRARY "${ZLIB_OUTPUT_DIR}/zlibstatic.lib" CACHE FILEPATH "" FORCE) endif() else() - set(ZLIB_LIBRARY "${ZLIB_OUTPUT_DIR}/libz.a") + set(ZLIB_LIBRARY "${ZLIB_OUTPUT_DIR}/libz.a" CACHE FILEPATH "" FORCE) endif() -set(ZLIB_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/zlib;${ZLIB_OUTPUT_DIR}) +set(ZLIB_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/zlib;${ZLIB_OUTPUT_DIR} CACHE PATH "" FORCE) set(ZLIB_INCLUDE_DIRS ${ZLIB_INCLUDE_DIR} CACHE STRING "" FORCE) +set(ZLIB_LIBRARIES ZLIB::ZLIB CACHE STRING "" FORCE) # boringssl setup +set(BUILD_TESTING OFF) 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() + +# BoringSSL's _HAS_EXCEPTIONS=0 changes MSVC STL vtables; under /LTCG this +# trips C4743, which BoringSSL's /WX escalates to LNK1257. Keep it out of LTCG. +if (MSVC AND LUTE_OPTIMIZE_SIZE AND NOT LUTE_ENABLE_SANITIZERS AND LUTE_IPO_SUPPORTED) + foreach(_bssl_target crypto ssl fipsmodule decrepit pki) + if (TARGET ${_bssl_target}) + set_property(TARGET ${_bssl_target} PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE) + set_property(TARGET ${_bssl_target} PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE FALSE) + set_property(TARGET ${_bssl_target} PROPERTY INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL FALSE) + set_property(TARGET ${_bssl_target} PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO FALSE) + endif() + endforeach() +endif() + # curl setup set(USE_LIBIDN2 OFF) @@ -66,6 +158,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) @@ -73,6 +166,36 @@ set(BUILD_EXAMPLES OFF) set(BUILD_CURL_EXE OFF) set(BUILD_SHARED_LIBS OFF) set(BUILD_STATIC_LIBS ON) +set(CURL_ENABLE_EXPORT_TARGET OFF) + +# curl's OpenSSL-family probes run in nested try-compile projects, where the +# in-tree ZLIB::ZLIB target is not visible. These are false for BoringSSL. +set(HAVE_DES_ECB_ENCRYPT "" CACHE INTERNAL "curl BoringSSL feature probe") +set(HAVE_SSL_SET0_WBIO "" CACHE INTERNAL "curl BoringSSL feature probe") +set(HAVE_OPENSSL_SRP "" CACHE INTERNAL "curl BoringSSL feature probe") + +# lute/net only speaks HTTP/HTTPS/WebSocket. +set(CURL_DISABLE_FTP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_TFTP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_FILE ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_DICT ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_TELNET ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_SMB ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_SMTP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_IMAP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_POP3 ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_GOPHER ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_RTSP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_MQTT ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_IPFS ON CACHE BOOL "" FORCE) + +# One more set of DISABLES. We might need to re-enable these at some point +# if we need the features they provide +set(CURL_DISABLE_AWS ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_INSTALL ON CACHE BOOL "" FORCE) +set(ENABLE_UNIX_SOCKETS OFF CACHE BOOL "" FORCE) set(HAVE_BORINGSSL ON) add_subdirectory(extern/curl) @@ -90,7 +213,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 +224,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 @@ -115,6 +238,9 @@ else() endif() # libraries +add_subdirectory(lute/batteries) +add_subdirectory(lute/common) +add_subdirectory(lute/definitions) add_subdirectory(lute/require) add_subdirectory(lute/runtime) add_subdirectory(lute/crypto) @@ -127,6 +253,9 @@ add_subdirectory(lute/vm) add_subdirectory(lute/process) add_subdirectory(lute/system) add_subdirectory(lute/time) +add_subdirectory(lute/io) +add_subdirectory(lute/syntax) +add_subdirectory(lute/debug) # 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..2b15e3730 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,70 @@ -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. Lute also features a standard library of Luau code, called `std`, that aims to expose a more featureful standard library for general-purpose programming that we hope can be an interface shared across Luau runtimes. -Lute is still very much a work-in-progress, and should be treated as pre-1.0 software without stability guarantees for its API. -We would love to hear from you about your experiences working with other Luau or Lua runtimes, and about what sort of functionality is needed to best make Luau accessible and productive for general-purpose programming. - ### 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. 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. If you do not have `lute` available to run this step, the CMake option `-DLUTE_STDLESS=ON` can be passed to skip embedding entirely. + +As a result, building a full version of Lute requires a local version of `lute` to run the code generation. We've provide a few paths below for resolving this bootstrapping problem, depending on your preferences and constraints. You can also download and manually install a prebuilt binary from our [Releases](https://github.com/luau-lang/lute/releases) page, and place it wherever you'd like. + +### Building Lute from scratch + +The simplest way to build `lute` without a version of `lute` already present is by bootstrapping it yourself. Toward that end, we provide 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`. + +For subsequent builds, you can use your current copy of `lute` to invoke `luthier` directly to perform clean or incremental builds of the CLI executable or the test suite: +```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 with a toolchain manager + +A popular approach in the Roblox community generally is to leverage toolchain managers to handle the installation and setup of developer tooling for projects, and this approach works just as well for building Lute. Out of the box, we provide configurations for two popular toolchain managers, [`foreman`](https://github.com/Roblox/foreman) and [`rokit`](https://github.com/rojo-rbx/rokit). You can invoke them with `foreman install` or `rokit install` respectively to have them download an appropriate version of `lute` to use for the build process. With this copy 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! +``` + +### 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. +This will have you performing the steps of the bootstrap script yourself. A manual build therefore would look something like: + +- Fetch all the external dependencies from `extern` by hand, consulting the `.tune` files for the appropriate versions to pull in. +- Configure with `cmake -G=Ninja -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DLUTE_STDLESS=ON` to build a version with no embedded Luau functionality. +- 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 embedded Luau source files, then reconfigure without `-DLUTE_STDLESS=ON` and rebuild to get a fully-featured `lute`. + +### CCache support +Lute supports the use of ccache to speed up builds. Both the `bootstrap` and `luthier` build scripts support a `--with-ccache` option, which proxies all compilation through ccache. To benefit from +caching during local development, download ccache using your systems package manager (apt/brew/choco/etc), and perform a clean configure with `--with-ccache` passed. Subsequent builds +will then use the cached build artifacts in the .ccache directory. +Additionally, you'll need to set the following variables: +``` +CCACHE_DIR=path/to/store/ccachedir +CCACHE_MAXSIZE=maximum size of the cache +CCACHE_COMPRESS=true +``` diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..340f361bb --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,37 @@ +# Release Process + +Lute uses branches with the format: `release/v{Major}.{Minor}.x` to store +changes associated with a given release. We tag commits on these branches with +the format `v{Major}.{Minor}.{Patch}` to indicate that a commit should be build +as that release version. These release branches exist so that we can continue to +support older versions of lute according to some currently unknown SLA, but +continue development on the primary branch. Release notes must be written +manually. + +## Working on release branches +As much as possible, we should avoid manually making changes to release +branches, and prefer to develop directly to primary. If changes need to be +applied to release branches (security vulnerabilities or hotfixes), we should +cherry-pick those in as appropriate, only applying changes manually as a last +resort. + +## Cutting a release +Cutting a release should be mostly automatic. Navigate to +github.com/luau-lang/lute > actions > release. For this action, select a release +branch from the dropdown to cut the release from - it should start with +`release/v{Major}.{Minor}.x`. + +This job will: +1) Checkout the branch `release/v{Major}.{Minor}.x`. +2) Ensure that this branch passes all status checks +3) Derive a tag in the format `v{Major}.{Minor}.{Patch}` +4) Build `lute` for a bunch of different platforms +5) Create a draft release with the artifacts built in 4) + +You should add patch notes for this release manually. + +## Nightly releases +Nightlies can be scheduled or triggered manually. Patch notes will be +automatically generated for nightlies. + + diff --git a/batteries/.luaurc b/batteries/.luaurc new file mode 100644 index 000000000..598234a83 --- /dev/null +++ b/batteries/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "./" + } +} 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..ad0a3f7b9 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -1,12 +1,13 @@ local cli = {} cli.__index = cli -type ArgKind = "positional" | "flag" | "option" +type ArgKind = "positional" | "flag" | "option" | "variadic" type ArgOptions = { help: string?, aliases: { string }?, default: string?, required: boolean?, + hidden: boolean?, } type ArgData = { @@ -16,14 +17,16 @@ type ArgData = { } type ParseResult = { - values: { [string]: string }, + values: { [string]: string? }, flags: { [string]: boolean }, + variadics: { string }, fwdArgs: { string }, } type ParserData = { arguments: { [number]: ArgData }, positional: { [number]: ArgData }, + variadicArg: ArgData?, parsed: ParseResult, } @@ -34,14 +37,15 @@ function cli.parser(): Parser local self = { arguments = {}, positional = {}, - parsed = { values = {}, flags = {}, fwdArgs = {} }, + variadicArg = nil :: ArgData?, + parsed = { values = {}, flags = {}, variadics = {}, fwdArgs = {} }, } return setmetatable(self, cli) end function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): () - local argument = { + local argument: ArgData = { name = name, kind = kind, options = options or { aliases = {}, required = false }, @@ -50,6 +54,8 @@ function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions) table.insert(self.arguments, argument) if kind == "positional" then table.insert(self.positional, argument) + elseif kind == "variadic" then + self.variadicArg = argument end end @@ -64,7 +70,7 @@ function cli.parse(self: Parser, args: { string }): () -- if the argument is exactly "--", we pass everything along if arg == "--" then - table.move(args, i + 1, #args, 1, self.parsed.fwdArgs) + table.move(args, i + 1, #args, #self.parsed.fwdArgs + 1, self.parsed.fwdArgs) break end @@ -133,16 +139,24 @@ function cli.parse(self: Parser, args: { string }): () continue end + -- if we have a variadic argument, we can take this argument as part of it + if self.variadicArg then + table.insert(self.parsed.variadics, arg) + 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) + if argument.options.required then + if argument.kind == "variadic" then + assert(#self.parsed.variadics > 0, "Missing required variadic argument: " .. argument.name) + else + assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name) + end end if self.parsed.values[argument.name] == nil and argument.options.default then @@ -151,7 +165,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 @@ -159,16 +173,64 @@ function cli.has(self: Parser, name: string): boolean return self.parsed.flags[name] ~= nil end +function cli.variadics(self: Parser): { string } + return self.parsed.variadics +end + function cli.forwarded(self: Parser): { string }? return self.parsed.fwdArgs end -function cli.help(self: Parser): () - print("Usage:") +function cli.usage(self: Parser, name: string): string + local parts = { name } + + for _, arg in self.positional do + if not arg.options.hidden then + if arg.options.required then + table.insert(parts, `<{arg.name}>`) + else + table.insert(parts, `[{arg.name}]`) + end + end + end + + for _, arg in self.arguments do + if not arg.options.hidden and arg.kind ~= "positional" and arg.kind ~= "variadic" then + table.insert(parts, "[options]") + break + end + end + + if self.variadicArg and not self.variadicArg.options.hidden then + local v = self.variadicArg + if v.options.required then + table.insert(parts, `<{v.name}...>`) + else + table.insert(parts, `[{v.name}...]`) + end + end + + return table.concat(parts, " ") +end + +function cli.help(self: Parser, formatter: ((s: string) -> string)?): () + local fmt = formatter or function(s) + return s + end + + print(fmt("Usage:")) for _, argument in self.arguments do + if argument.options.hidden then + continue + end + 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 "")) + print( + fmt( + ` --{argument.name}{if #aliasStr > 0 then ` (-{aliasStr})` else ""} - {reqStr}{argument.options.help or ""}` + ) + ) end end diff --git a/batteries/collections/deque.luau b/batteries/collections/deque.luau new file mode 100644 index 000000000..d9ab15e7d --- /dev/null +++ b/batteries/collections/deque.luau @@ -0,0 +1,67 @@ +type DequeData = { + _head: number, + _tail: number, + _values: { [number]: T }, +} + +local deque = {} + +export type Deque = setmetatable, typeof(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..3cc3fa595 --- /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| ` :: any, oldStr, newStr) + elseif operation == "ADD" then + return string.format(`+ %{maxNumWidth}s %{maxNumWidth}s| ` :: any, "", newStr) + elseif operation == "DELETE" then + return string.format(`- %{maxNumWidth}s %{maxNumWidth}s| ` :: any, 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 _, 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 _, 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 _, 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 _, 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/glob.luau b/batteries/glob.luau new file mode 100644 index 000000000..09383251b --- /dev/null +++ b/batteries/glob.luau @@ -0,0 +1,221 @@ +--[[ + Glob pattern matching and filesystem discovery. + + `glob.match(pattern, input)` tests whether a path matches a glob pattern. + `glob.discover(pattern)` walks the filesystem returning all matching `Path`s. + + The implementation follows the Rust glob crate. The rules are: + + - `?` matches any single character (not `/`) + - `*` matches any (possibly empty) sequence of characters (not `/`) + - `**` matches the current directory and arbitrary subdirectories; it must + form an entire path component on its own (e.g. `a/**/b` is valid, but + `a**b` is not) + - `[...]` matches any character inside the brackets; ranges like `[a-z]` + are supported, ordered by Unicode code point + - `[!...]` matches any character NOT inside the brackets + - Metacharacters can be matched literally by wrapping them in brackets, + e.g. `[?]` matches a literal `?`, `[*]` matches a literal `*` +]] +local fs = require("@std/fs") +local path = require("@std/path") + +local function matchesSegment(pattern: string, name: string, patternStart: number?, nameStart: number?): boolean + local patternIndex = patternStart or 1 + local nameIndex = nameStart or 1 + while patternIndex <= #pattern do + local patternChar = string.sub(pattern, patternIndex, patternIndex) + if patternChar == "*" then + patternIndex += 1 + for candidateIndex = nameIndex, #name + 1 do + if matchesSegment(pattern, name, patternIndex, candidateIndex) then + return true + end + end + return false + elseif patternChar == "?" then + if nameIndex > #name then + return false + end + patternIndex += 1 + nameIndex += 1 + elseif patternChar == "[" then + if nameIndex > #name then + return false + end + local nameChar = string.sub(name, nameIndex, nameIndex) + patternIndex += 1 + local negate = string.sub(pattern, patternIndex, patternIndex) == "!" + if negate then + patternIndex += 1 + end + local found = false + while patternIndex <= #pattern and string.sub(pattern, patternIndex, patternIndex) ~= "]" do + local rangeLow = string.sub(pattern, patternIndex, patternIndex) + patternIndex += 1 + if + string.sub(pattern, patternIndex, patternIndex) == "-" + and patternIndex + 1 <= #pattern + and string.sub(pattern, patternIndex + 1, patternIndex + 1) ~= "]" + then + patternIndex += 1 + local rangeHigh = string.sub(pattern, patternIndex, patternIndex) + patternIndex += 1 + if nameChar >= rangeLow and nameChar <= rangeHigh then + found = true + end + else + if nameChar == rangeLow then + found = true + end + end + end + patternIndex += 1 + if negate then + found = not found + end + if not found then + return false + end + nameIndex += 1 + else + if nameIndex > #name or string.sub(name, nameIndex, nameIndex) ~= patternChar then + return false + end + patternIndex += 1 + nameIndex += 1 + end + end + return nameIndex > #name +end + +type MatchResult = "match" | "no_match" | "partial" + +-- Matches input path segments against pattern segments, returning: +-- "match" — full match (all pattern and input segments consumed) +-- "partial" — input consumed but pattern segments remain (worth descending) +-- "no_match" — input diverges from pattern (prune) +local function matchStep( + patternSegments: { string }, + inputSegments: { string }, + patternIndex: number, + inputIndex: number +): MatchResult + while patternIndex <= #patternSegments do + local segment = patternSegments[patternIndex] + if segment == "**" then + local best: MatchResult = "no_match" + for candidateIndex = inputIndex, #inputSegments + 1 do + local result = matchStep(patternSegments, inputSegments, patternIndex + 1, candidateIndex) + if result == "match" then + return "match" + elseif result == "partial" then + best = "partial" + end + end + return best + else + if inputIndex > #inputSegments then + return "partial" + end + if not matchesSegment(segment, inputSegments[inputIndex]) then + return "no_match" + end + patternIndex += 1 + inputIndex += 1 + end + end + if inputIndex > #inputSegments then + return "match" + end + return "no_match" +end + +local function walk(directory: path.Pathlike, segments: { string }, segmentIndex: number, results: { path.Path }) + if segmentIndex > #segments then + return + end + local success, entries = pcall(fs.listDirectory, directory) + if not success then + return + end + local segment = segments[segmentIndex] + local isLast = segmentIndex == #segments + if segment == "**" then + if not isLast then + walk(directory, segments, segmentIndex + 1, results) + end + for _, entry in entries do + local entryPath = path.join(directory, entry.name) + if isLast then + table.insert(results, entryPath) + end + if entry.type == "dir" then + walk(entryPath, segments, segmentIndex, results) + end + end + else + for _, entry in entries do + if matchesSegment(segment, entry.name) then + local entryPath = path.join(directory, entry.name) + if isLast then + table.insert(results, entryPath) + elseif entry.type == "dir" then + walk(entryPath, segments, segmentIndex + 1, results) + end + end + end + end +end + +local glob = {} + +-- Returns true if the given path matches the glob pattern, without touching the filesystem. +function glob.match(pattern: path.Pathlike, input: path.Pathlike): boolean + local patternParts = path.parse(pattern).parts + local inputParts = path.parse(input).parts + return matchStep(patternParts, inputParts, 1, 1) == "match" +end + +-- Walks the filesystem and returns all paths matching the glob pattern. +function glob.discover(input: path.Pathlike): { path.Path } + local parsed = path.parse(input) + local parts: { string } = parsed.parts + + local firstGlobIndex = #parts + 1 + for index, part in parts do + if string.find(part, "[%*%?%[]") then + firstGlobIndex = index + break + end + end + + if firstGlobIndex > #parts then + if fs.exists(parsed) then + return { parsed } + end + return {} + end + + local prefixPath = table.clone(parsed) + prefixPath.parts = { table.unpack(parts, 1, firstGlobIndex - 1) } + local segments = { table.unpack(parts, firstGlobIndex) } + + local results: { path.Path } = {} + walk(prefixPath, segments, 1, results) + -- To ensure a consistent sort order across POSIX and Windows, we sort by + -- parts rather than sorting based on string representation, which ensures + -- that code implicitly dependent on the ordering (e.g. snapshot tests) do + -- not break when running on different platforms. + table.sort(results, function(a: path.Path, b: path.Path) + for i = 1, math.min(#a.parts, #b.parts) do + if a.parts[i] ~= b.parts[i] then + return a.parts[i] < b.parts[i] + end + end + return #a.parts < #b.parts + end) + return results +end + +return glob diff --git a/batteries/json.luau b/batteries/json.luau deleted file mode 100644 index 36eaf5521..000000000 --- a/batteries/json.luau +++ /dev/null @@ -1,485 +0,0 @@ ---!strict - -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, -} - -type JSONPrimitive = nil | number | string | boolean -type Object = { [string]: Value } -type Array = { Value } -export type Value = JSONPrimitive | Array | Object - --- serialization - -type SerializerState = { - buf: buffer, - cursor: number, - prettyPrint: boolean, - depth: number, -} - -local function checkState(state: SerializerState, len: number) - local curLen = buffer.len(state.buf) - - if state.cursor + len < curLen then - return - end - - local newBuffer = buffer.create(curLen * 2) - - buffer.copy(newBuffer, 0, state.buf) - - state.buf = newBuffer -end - -local function writeByte(state: SerializerState, byte: number) - checkState(state, 1) - - buffer.writeu8(state.buf, state.cursor, byte) - - state.cursor += 1 -end - -local function writeSpaces(state: SerializerState) - if state.depth == 0 then - return - end - - if state.prettyPrint then - checkState(state, state.depth * 4) - - for i = 0, 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(" ")) - - state.cursor += 1 - end -end - -local function writeString(state: SerializerState, str: string) - checkState(state, #str) - - buffer.writestring(state.buf, state.cursor, str) - - state.cursor += #str -end - -local serializeAny - -local ESCAPE_MAP = { - [0x5C] = string.byte("\\"), -- 5C = '\' - [0x08] = string.byte("b"), - [0x0C] = string.byte("f"), - [0x0A] = string.byte("n"), - [0x0D] = string.byte("r"), - [0x09] = string.byte("t"), -} - -local function serializeUnicode(codepoint: number) - if codepoint >= 0x10000 then - local high = math.floor((codepoint - 0x10000) / 0x400) + 0xD800 - local low = ((codepoint - 0x10000) % 0x400) + 0xDC00 - return string.format("\\u%04x\\u%04x", high, low) - end - - return string.format("\\u%04x", codepoint) -end - -local function serializeString(state: SerializerState, str: string) - checkState(state, #str) - - writeByte(state, string.byte('"')) - - for pos, codepoint in utf8.codes(str) do - if ESCAPE_MAP[codepoint] then - writeByte(state, string.byte("\\")) - writeByte(state, ESCAPE_MAP[codepoint]) - elseif codepoint < 32 or codepoint > 126 then - writeString(state, serializeUnicode(codepoint)) - else - writeString(state, utf8.char(codepoint)) - end - end - - writeByte(state, string.byte('"')) -end - -local function serializeArray(state: SerializerState, array: Array) - state.depth += 1 - - writeByte(state, string.byte("[")) - - if state.prettyPrint and #array ~= 0 then - writeByte(state, string.byte("\n")) - end - - for i, value in array do - if i ~= 1 then - writeByte(state, string.byte(",")) - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - end - - if i ~= 1 or state.prettyPrint then - writeSpaces(state) - end - - serializeAny(state, value) - end - - state.depth -= 1 - - if state.prettyPrint and #array ~= 0 then - writeByte(state, string.byte("\n")) - writeSpaces(state) - end - - writeByte(state, string.byte("]")) -end - -local function serializeTable(state: SerializerState, object: Object) - writeByte(state, string.byte("{")) - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - - state.depth += 1 - - local first = true - for key, value in object do - if not first then - writeByte(state, string.byte(",")) - writeByte(state, string.byte(" ")) - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - end - - first = false - - writeSpaces(state) - - writeByte(state, string.byte('"')) - writeString(state, key) - writeString(state, '": ') - - serializeAny(state, value) - end - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - - state.depth -= 1 - - writeSpaces(state) - - writeByte(state, string.byte("}")) -end - -serializeAny = function(state: SerializerState, value: Value) - local valueType = type(value) - - if value == json.NULL then - writeString(state, "null") - elseif valueType == "boolean" then - writeString(state, if value then "true" else "false") - elseif valueType == "number" then - writeString(state, tostring(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) - else - serializeArray(state, value :: Array) - end - else - error("Unknown value", 2) - end -end - --- deserialization - -type DeserializerState = { - src: string, - cursor: number, -} - -local function deserializerError(state: DeserializerState, msg: string): never - return error(`JSON error - {msg} around {state.cursor}`) -end - -local function skipWhitespace(state: DeserializerState): boolean - state.cursor = string.find(state.src, "%S", state.cursor) :: number - - if not state.cursor then - return false - end - - return true -end - -local function currentByte(state: DeserializerState) - return string.byte(state.src, state.cursor) -end - -local function deserializeNumber(state: DeserializerState) - -- first "segment" - local nStart, nEnd = string.find(state.src, "^[%-%deE]*", 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) - - if not decStart then - deserializerError(state, "Trailing '.' in number value") - end - - nEnd = decEnd - 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 - state.cursor += 1 - - local startPos = state.cursor - - if currentByte(state) == string.byte('"') then - state.cursor += 1 - - return "" - end - - while state.cursor <= #state.src do - if currentByte(state) == string.byte('"') then - state.cursor += 1 - - local source = string.sub(state.src, startPos, state.cursor - 2) - - source = string.gsub( - source, - "\\u([dD]83[dD])\\u(d[cC]%w%w)", - function(high, low) - return decodeSurrogatePair(high, low) or deserializerError(state, "Invalid unicode surrogate pair") - end :: any - ) - -- Handle regular Unicode escapes - source = string.gsub(source, "\\u(%x%x%x%x)", function(code) - return utf8.char(tonumber(code, 16) :: number) - end) - - source = string.gsub(source, "\\\\", "\0") - source = string.gsub(source, "\\b", "\b") - source = string.gsub(source, "\\f", "\f") - source = string.gsub(source, "\\n", "\n") - source = string.gsub(source, "\\r", "\r") - source = string.gsub(source, "\\t", "\t") - source = string.gsub(source, '\\"', '"') - source = string.gsub(source, "\0", "\\") - - return source - end - - if currentByte(state) == string.byte("\\") then - state.cursor += 1 - end - - state.cursor += 1 - end - - -- error - - state.cursor = startPos - - return deserializerError(state, "Unterminated string") -end - -local deserialize - -local function deserializeArray(state: DeserializerState): Array - state.cursor += 1 - - local current: Array = {} - - local expectingValue = false - while state.cursor < #state.src do - skipWhitespace(state) - - if currentByte(state) == string.byte(",") then - expectingValue = true - state.cursor += 1 - end - - skipWhitespace(state) - - if currentByte(state) == string.byte("]") then - break - 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") - end - - state.cursor += 1 - - return current -end - -local function deserializeObject(state: DeserializerState): Object - state.cursor += 1 - - local current = {} - - local expectingValue = false - while state.cursor < #state.src do - skipWhitespace(state) - - if currentByte(state) == string.byte("}") then - break - end - - skipWhitespace(state) - - if currentByte(state) ~= string.byte('"') then - return deserializerError(state, "Expected a string key") - end - - local key = deserializeString(state) - - skipWhitespace(state) - - if currentByte(state) ~= string.byte(":") then - return deserializerError(state, "Expected ':' for key value pair") - end - - state.cursor += 1 - - local value = deserialize(state) - - current[key] = value - - if not skipWhitespace(state) then - deserializerError(state, "Unterminated object") - end - - skipWhitespace(state) - - if currentByte(state) == string.byte(",") then - expectingValue = true - state.cursor += 1 - else - expectingValue = false - end - end - - if expectingValue then - return deserializerError(state, "Trailing comma") - end - - if not skipWhitespace(state) or currentByte(state) ~= string.byte("}") then - deserializerError(state, "Unterminated object") - end - - state.cursor += 1 - - return current -end - -deserialize = function(state: DeserializerState): Value - skipWhitespace(state) - - local fourChars = string.sub(state.src, state.cursor, state.cursor + 3) - - if fourChars == "null" then - state.cursor += 4 - return json.NULL - elseif fourChars == "true" then - state.cursor += 4 - return true - elseif 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 - return deserializeNumber(state) - elseif string.byte(state.src, state.cursor) == string.byte('"') then - return deserializeString(state) - elseif string.byte(state.src, state.cursor) == string.byte("[") then - return deserializeArray(state) - elseif string.byte(state.src, state.cursor) == string.byte("{") then - return deserializeObject(state) - end - - return deserializerError(state, `Unexpected token '{string.sub(state.src, state.cursor, state.cursor)}'`) -end - --- user-facing - -json.serialize = function(value: Value, prettyPrint: boolean?) - local state: SerializerState = { - buf = buffer.create(1024), - cursor = 0, - prettyPrint = prettyPrint or false, - depth = 0, - } - - serializeAny(state, value) - - return buffer.readstring(state.buf, 0, state.cursor) -end - -json.deserialize = function(src: string) - local state = { - src = src, - cursor = 0, - } - - return deserialize(state) -end - -return table.freeze(json) diff --git a/batteries/pp.luau b/batteries/pp.luau index 413668cdf..e002e1986 100644 --- a/batteries/pp.luau +++ b/batteries/pp.luau @@ -90,14 +90,15 @@ local function traverseTable( local output = "" local indentStr = string.rep(" ", indent) - local keys = {} + -- FIXME(Luau): We shouldn't need this annotation. + local keys: { unknown } = {} -- Collect all keys, not just primitives for key in dataTable do table.insert(keys, key) end - table.sort(keys, function(a: string, b: string): boolean + table.sort(keys, function(a, b): boolean local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b]) if typeofTableA ~= typeofTableB then @@ -114,7 +115,7 @@ local function traverseTable( local inSequence = false local previousKey = 0 - for idx, key in keys do + for _, key in keys do if type(key) == "number" and key > 0 and key - 1 == previousKey then previousKey = key inSequence = true diff --git a/batteries/toml.luau b/batteries/toml.luau index b6a191adc..4a2d4385d 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -1,38 +1,49 @@ local toml = {} -- serialization -type SerializerState = { - buf: buffer, - cursor: number, -} -local function serializeValue(value: string | number) +const function serializeValue(value: string | number): string if typeof(value) == "string" then value = string.gsub(value, "\\", "\\\\") value = string.gsub(value, "\n", "\\n") value = string.gsub(value, "\t", "\\t") return '"' .. value .. '"' - elseif value == math.huge then + end + + if value == math.huge then return "inf" - elseif value == -math.huge then + end + + if value == -math.huge then return "-inf" - elseif value ~= value then + end + + if value ~= value then return "nan" - else - return tostring(value) end + + return tostring(value) +end + +const function quoteKey(key: string): string + if not string.match(key, "^[%w_-]+$") then + return `"{key}"` + end + + return key end -local function hasNestedTables(tbl: {}) +const function hasNestedTables(tbl: { [unknown]: unknown }): boolean for _, v in tbl do if typeof(v) == "table" and next(v) ~= nil then return true end end + return false end -local function tableToToml(tbl: {}, parent: string) +const function tableToToml(tbl: { [any]: any }, parent: string?): string local result = "" local subTables = {} local hasDirectValues = false @@ -41,7 +52,7 @@ local function tableToToml(tbl: {}, parent: string) if typeof(v) == "table" and next(v) ~= nil then if #v > 0 then for _, entry in v do - result ..= "\n[[" .. (parent and parent .. "." or "") .. k .. "]]\n" + result ..= "\n[[" .. (parent and parent .. "." or "") .. quoteKey(k) .. "]]\n" result ..= tableToToml(entry, nil) end else @@ -49,12 +60,12 @@ local function tableToToml(tbl: {}, parent: string) end else hasDirectValues = true - result ..= k .. " = " .. serializeValue(v) .. "\n" + result ..= quoteKey(k) .. " = " .. serializeValue(v) .. "\n" end end for k, v in subTables do - local key = parent and (parent .. "." .. k) or k + local key = parent and (parent .. "." .. quoteKey(k)) or quoteKey(k) if hasNestedTables(v) == true then result ..= tableToToml(v, key) @@ -71,102 +82,1496 @@ local function tableToToml(tbl: {}, parent: string) return result end -local function serialize(tbl: {}): string +const function serialize(tbl: { [any]: any }): string return tableToToml(tbl, nil) end -- deserialization -type DeserializerState = { - buf: string, - cursor: number, -} - -local function skipWhitespace(state: DeserializerState) - local pos = state.cursor - while pos <= string.len(state.buf) and string.match(string.sub(state.buf, pos, pos), "%s") do + +export type TomlValue = string | number | boolean | { TomlValue } | { [string]: TomlValue } + +const function deserialize(input: string): { [string]: TomlValue } + -- convert CRLF to LF + if input:find("\r\n", 1, true) then + input = input:gsub("\r\n", "\n") + end + + -- Reject bare CR (only CRLF → LF is allowed) + if input:find("\r", 1, true) then + error("TOML parse error: bare carriage return (\\r) not allowed") + end + + -- Validate UTF-8 encoding + do + local i = 1 + local n = #input + while i <= n do + local b = string.byte(input, i) + local seqLen: number + + if b < 0x80 then + seqLen = 1 + elseif b < 0xC2 then + error("TOML parse error: invalid UTF-8 byte at position " .. i) + elseif b < 0xE0 then + seqLen = 2 + elseif b < 0xF0 then + seqLen = 3 + elseif b <= 0xF4 then + seqLen = 4 + else + error("TOML parse error: invalid UTF-8 byte at position " .. i) + end + + if i + seqLen - 1 > n then + error("TOML parse error: truncated UTF-8 sequence at position " .. i) + end + + -- Validate continuation bytes and overlong/surrogate checks + if seqLen == 2 then + local b2 = string.byte(input, i + 1) + if b2 < 0x80 or b2 > 0xBF then + error("TOML parse error: invalid UTF-8 continuation at position " .. (i + 1)) + end + elseif seqLen == 3 then + local b2 = string.byte(input, i + 1) + local b3 = string.byte(input, i + 2) + if b == 0xE0 then + if b2 < 0xA0 or b2 > 0xBF then + error("TOML parse error: overlong UTF-8 sequence at position " .. i) + end + elseif b == 0xED then + if b2 < 0x80 or b2 > 0x9F then + error("TOML parse error: UTF-8 surrogate codepoint at position " .. i) + end + else + if b2 < 0x80 or b2 > 0xBF then + error("TOML parse error: invalid UTF-8 continuation at position " .. (i + 1)) + end + end + if b3 < 0x80 or b3 > 0xBF then + error("TOML parse error: invalid UTF-8 continuation at position " .. (i + 2)) + end + elseif seqLen == 4 then + local b2 = string.byte(input, i + 1) + local b3 = string.byte(input, i + 2) + local b4 = string.byte(input, i + 3) + + if b == 0xF0 then + if b2 < 0x90 or b2 > 0xBF then + error("TOML parse error: overlong UTF-8 sequence at position " .. i) + end + elseif b == 0xF4 then + if b2 < 0x80 or b2 > 0x8F then + error("TOML parse error: UTF-8 codepoint > U+10FFFF at position " .. i) + end + else + if b2 < 0x80 or b2 > 0xBF then + error("TOML parse error: invalid UTF-8 continuation at position " .. (i + 1)) + end + end + + if b3 < 0x80 or b3 > 0xBF then + error("TOML parse error: invalid UTF-8 continuation at position " .. (i + 2)) + end + + if b4 < 0x80 or b4 > 0xBF then + error("TOML parse error: invalid UTF-8 continuation at position " .. (i + 3)) + end + end + i += seqLen + end + end + + local src = input + local pos = 1 + local len = #src + local srcBuf = buffer.fromstring(src) + + -- Table metadata: maps table reference -> { kind, keys } + type TableKind = + | "root" -- the document root + | "defined" -- created by an explicit [header] + | "implicit-header" -- implicitly created while navigating a [a.b.c] path + | "implicit-dotted" -- implicitly created by a dotted key assignment (a.b = v) + | "inline" -- an inline table; immutable after creation + | "array-element" -- an element of an [[array-of-tables]] + | "array-value" -- a static array literal (cannot be navigated into) + type TableMeta = { kind: TableKind, keys: { [string]: boolean } } + local tableMeta: { [any]: TableMeta } = {} + + -- tracks which Luau arrays are array-of-tables (vs regular array values) + local arrayTableSets: { [any]: boolean } = {} + + const function getMeta(tbl: any): TableMeta + if not tableMeta[tbl] then + tableMeta[tbl] = { kind = "implicit-header", keys = {} } + end + + return tableMeta[tbl] + end + + const function tomlParseError(msg: string): never + error(`TOML parse error at position {pos}: {msg}`) + end + + -- character helpers (all operate on the shared pos/src/len upvalues) + + const function byteAt(offset: number?): number? -- byte at pos+offset + local p = pos + (offset or 0) + return if p <= len then buffer.readu8(srcBuf, p - 1) else nil + end + + const function isDigit(c: number?): boolean + return c ~= nil and c >= 48 and c <= 57 + end + + const function isHexDigit(c: number?): boolean + return c ~= nil and ((c >= 48 and c <= 57) or (c >= 65 and c <= 70) or (c >= 97 and c <= 102)) + end + + const function isOctDigit(c: number?): boolean + return c ~= nil and c >= 48 and c <= 55 + end + + const function isBinDigit(c: number?): boolean + return c ~= nil and (c == 48 or c == 49) + end + + -- bare key chars: A-Z a-z 0-9 _ - + const function isBareKey(c: number?): boolean + if c == nil then + return false + end + + return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or (c >= 48 and c <= 57) or c == 95 or c == 45 + end + + -- control characters that are forbidden unescaped (everything < 0x20 except tab, + -- plus DEL 0x7F; LF 0x0A is handled separately per context) + const function isControlCharacter(c: number?): boolean + if c == nil then + return false + end + + return c <= 8 or c == 11 or c == 12 or (c >= 14 and c <= 31) or c == 127 + end + + -- whitespace skipping + + const function skipWhitespace() + while pos <= len do + local c = buffer.readu8(srcBuf, pos - 1) + + if c ~= 32 and c ~= 9 then + break + end + + pos += 1 + end + end + + -- skip a # comment up to (but not including) the newline + const function skipComment() + if byteAt() ~= 35 then + return + end -- '#' + pos += 1 + + while pos <= len do + local c = buffer.readu8(srcBuf, pos - 1) + + if c == 10 then + break + end + + if isControlCharacter(c) then + tomlParseError("control character in comment") + end + + pos += 1 + end end - state.cursor = pos -end -local function readLine(state: DeserializerState) - local nextLine = string.find(state.buf, "\n", state.cursor) or string.len(state.buf) + 1 - local line = string.sub(state.buf, state.cursor, nextLine - 1) - state.cursor = nextLine + 1 - return line -end + -- skip horizontal whitespace, newlines, and comments + const function skipWhitespaceNewlinesAndComments() + while pos <= len do + local c = buffer.readu8(srcBuf, pos - 1) -local function deserialize(input: string) - -- Normalize line endings: Replace Windows (\r\n) and old Mac (\r) with Unix (\n) - input = string.gsub(input, "\r\n", "\n") - input = string.gsub(input, "\r", "\n") + if c == 32 or c == 9 or c == 10 then + pos += 1 + elseif c == 35 then + skipComment() + else + break + end + end + end - local state: DeserializerState = { - buf = input, - cursor = 1, - } - local result = {} - local currentTable = result - local arrayTables = {} + -- after a value: consume trailing whitespace + optional comment, then require newline or EOF + const function expectLineEnd() + skipWhitespace() + skipComment() - while state.cursor <= string.len(state.buf) do - skipWhitespace(state) - local line = readLine(state) + if pos > len then + return + end - if line == "" or string.sub(line, 1, 1) == "#" then - continue + local c = byteAt() + if c ~= 10 then + tomlParseError(`expected newline or EOF, got '${string.char(c :: number)}'`) + end + + pos += 1 + end + + -- string escape helpers + + const function parseUnicodeEscape(long: boolean): string + local n = if long then 8 else 4 + local hexStr = string.sub(src, pos, pos + n - 1) + + if #hexStr < n then + tomlParseError("incomplete unicode escape") + end + + for i = 1, n do + if not isHexDigit(string.byte(hexStr, i)) then + tomlParseError("invalid hex digit in unicode escape") + end + end + + pos += n + + local cp = tonumber(hexStr, 16) :: number + + if cp >= 0xD800 and cp <= 0xDFFF then + tomlParseError("surrogate codepoint in unicode escape") + end + + if cp > 0x10FFFF then + tomlParseError("unicode codepoint out of range") + end + + return utf8.char(cp) + end + + -- \xXX escape (TOML 1.1): unicode codepoint U+0000..U+00FF + const function parseHexEscape(): string + local hexStr = string.sub(src, pos, pos + 1) + + if #hexStr < 2 or not isHexDigit(string.byte(hexStr, 1)) or not isHexDigit(string.byte(hexStr, 2)) then + tomlParseError("invalid \\x escape") + end + + pos += 2 + + return utf8.char(tonumber(hexStr, 16) :: number) + end + + -- maps escape-sequence byte to its result (string) or a no-arg handler (() -> string) + const escapeMapping: { [number]: string | () -> string } = table.freeze({ + [34] = '"', -- \" + -- \UXXXXXXXX + [85] = function() + return parseUnicodeEscape(true) + end, + [92] = "\\", -- \\ + [98] = "\b", -- \b + [101] = "\x1B", -- \e (TOML 1.1) + [102] = "\f", -- \f + [110] = "\n", -- \n + [114] = "\r", -- \r + [116] = "\t", -- \t + -- \uXXXX + [117] = function() + return parseUnicodeEscape(false) + end, + [120] = parseHexEscape, -- \xXX (TOML 1.1) + }) + + const function parseEscape(): string + if pos > len then + tomlParseError("incomplete escape sequence") + end + + local c = byteAt() :: number + pos += 1 + + local escapeResult = escapeMapping[c] + + if escapeResult == nil then + tomlParseError(`invalid escape sequence \\${string.char(c)}`) + end + + if typeof(escapeResult) == "function" then + return escapeResult() + end + + return escapeResult + end + + -- string parsers (pos is always past the opening delimiter on entry) + + const function parseBasicString(): string + local parts = {} + + while pos <= len do + -- Fast path: scan forward over plain characters in one go + local runStart = pos + + while pos <= len do + local c2 = buffer.readu8(srcBuf, pos - 1) + + -- break on: " (34), \ (92), DEL (127), ctrl chars except tab (9) + if c2 == 34 or c2 == 92 or c2 == 127 or (c2 < 32 and c2 ~= 9) then + break + end + + pos += 1 + end + + if pos > runStart then + table.insert(parts, string.sub(src, runStart, pos - 1)) + end + + if pos > len then + break + end + + local c = buffer.readu8(srcBuf, pos - 1) + + if c == 34 then -- closing " + pos += 1 + return table.concat(parts) + end + + if c == 92 then -- backslash + pos += 1 + table.insert(parts, parseEscape()) + continue + end + + if c == 10 then + tomlParseError("unescaped newline in basic string") + end + + tomlParseError("unescaped control character in basic string") + end + + tomlParseError("unterminated basic string") + + -- FIXME(Luau): `tomlParseError` paths are not currently treated as not returning + error("unreachable") + end + + const function parseMultilineBasicString(): string + -- trim a single leading newline + if byteAt() == 10 then + pos += 1 end - if string.match(line, "^%[%[(.-)%]%]$") then - local tableName = string.match(line, "^%[%[(.-)%]%]$") - arrayTables[tableName] = arrayTables[tableName] or {} + local parts = {} + + while pos <= len do + -- Fast path: scan over plain chars (LF and tab are literal content too) + local runStart = pos + + while pos <= len do + local c2 = buffer.readu8(srcBuf, pos - 1) + + -- break on: " (34), \ (92), DEL (127), ctrl except tab (9) and LF (10) + if c2 == 34 or c2 == 92 or c2 == 127 or (c2 < 32 and c2 ~= 9 and c2 ~= 10) then + break + end + + pos += 1 + end + + if pos > runStart then + table.insert(parts, string.sub(src, runStart, pos - 1)) + end + + if pos > len then + break + end + + local c = buffer.readu8(srcBuf, pos - 1) - local newEntry = {} - table.insert(arrayTables[tableName], newEntry) + if c == 34 then + if string.sub(src, pos, pos + 2) == '"""' then + pos += 3 + local extra = 0 - result[tableName] = arrayTables[tableName] - currentTable = newEntry - elseif string.match(line, "^%[(.-)%]$") then - local tablePath = string.match(line, "^%[(.-)%]$") - local parent = result + while pos <= len and buffer.readu8(srcBuf, pos - 1) == 34 and extra < 2 do + table.insert(parts, '"') + pos += 1 + extra += 1 + end - for section in string.gmatch(tablePath, "([^.]+)") do - if not parent[section] then - parent[section] = {} + return table.concat(parts) end - parent = parent[section] + + -- lone " is valid content in a multiline basic string + table.insert(parts, '"') + pos += 1 + + continue end - currentTable = parent - elseif string.match(line, "^(.-)%s*=%s*(.-)$") then - local key, value = string.match(line, "^(.-)%s*=%s*(.-)$") - key = string.match(key, "^%s*(.-)%s*$") - value = string.match(value, "^%s*(.-)%s*$") + if c == 92 then -- backslash + pos += 1 + + -- peek past trailing horizontal whitespace to detect line-ending backslash + local peek = pos + + while peek <= len do + local pc = buffer.readu8(srcBuf, peek - 1) + + if pc ~= 32 and pc ~= 9 then + break + end + + peek += 1 + end + + if peek <= len and buffer.readu8(srcBuf, peek - 1) == 10 then + -- line-ending backslash: skip newline and all following whitespace and newlines + pos = peek + 1 + while pos <= len do + local c3 = buffer.readu8(srcBuf, pos - 1) + if c3 ~= 32 and c3 ~= 9 and c3 ~= 10 then + break + end + pos += 1 + end + elseif peek > len then + tomlParseError("incomplete escape at end of file") + else + table.insert(parts, parseEscape()) + end - if string.match(value, '^"(.*)"$') or string.match(value, "^'(.*)'$") then - value = string.sub(value, 2, -2) - value = string.gsub(value, "\\\\", "\\") - value = string.gsub(value, "\\n", "\n") - value = string.gsub(value, "\\t", "\t") - elseif tonumber(value) then - value = tonumber(value) - elseif value == "true" then - value = true - elseif value == "false" then - value = false - elseif value == "inf" or value == "+inf" then - value = math.huge - elseif value == "-inf" then - value = -math.huge - elseif value == "nan" then - value = 0 / 0 + continue end - currentTable[key] = value + -- ctrl char or DEL + tomlParseError("unescaped control character in multiline basic string") end + + tomlParseError("unterminated multiline basic string") + + -- FIXME(Luau): `tomlParseError` paths are not currently treated as not returning + error("unreachable") end - return result + const function parseLiteralString(): string + local start = pos + + while pos <= len do + local c = buffer.readu8(srcBuf, pos - 1) + if c == 39 then -- closing ' + local result = string.sub(src, start, pos - 1) + pos += 1 + return result + end + + if c == 9 or (c >= 32 and c ~= 127) or c >= 128 then + pos += 1 -- plain char (tab, printable ASCII, or multibyte UTF-8) + continue + end + + if c == 10 then + tomlParseError("unescaped newline in literal string") + end + + tomlParseError("unescaped control character in literal string") + end + + tomlParseError("unterminated literal string") + + -- FIXME(Luau): `tomlParseError` paths are not currently treated as not returning + error("unreachable") + end + + const function parseMultilineLiteralString(): string + if byteAt() == 10 then + pos += 1 + end -- trim leading newline + + local parts = {} + + while pos <= len do + -- Fast path: scan over plain chars (LF and tab are literal content) + local runStart = pos + while pos <= len do + local c2 = buffer.readu8(srcBuf, pos - 1) + -- break on: ' (39), DEL (127), ctrl except tab (9) and LF (10) + if c2 == 39 or c2 == 127 or (c2 < 32 and c2 ~= 9 and c2 ~= 10) then + break + end + pos += 1 + end + + if pos > runStart then + table.insert(parts, string.sub(src, runStart, pos - 1)) + end + + if pos > len then + break + end + + local c = buffer.readu8(srcBuf, pos - 1) + if c == 39 then + if string.sub(src, pos, pos + 2) == "'''" then + pos += 3 + local extra = 0 + while pos <= len and buffer.readu8(srcBuf, pos - 1) == 39 and extra < 2 do + table.insert(parts, "'") + pos += 1 + extra += 1 + end + return table.concat(parts) + end + -- lone ' is valid content in a multiline literal string + table.insert(parts, "'") + pos += 1 + continue + end + + -- ctrl char or DEL + tomlParseError("unescaped control character in multiline literal string") + end + + tomlParseError("unterminated multiline literal string") + + -- FIXME(Luau): `tomlParseError` paths are not currently treated as not returning + error("unreachable") + end + + -- number / datetime helpers + + -- parse a run of digits (with internal underscores allowed), return digits-only string + const function parseDigits(digitCheck: (number?) -> boolean): string + if not digitCheck(byteAt()) then + tomlParseError("expected digit") + end + + local result = "" + local runStart = pos + + while pos <= len do + local c = buffer.readu8(srcBuf, pos - 1) + + if digitCheck(c) then + pos += 1 + elseif c == 95 then -- underscore + result ..= string.sub(src, runStart, pos - 1) + pos += 1 + + if pos > len then + tomlParseError("trailing underscore in number") + end + + local nc = buffer.readu8(srcBuf, pos - 1) + if nc == 95 then + tomlParseError("consecutive underscores in number") + end + + if not digitCheck(nc) then + tomlParseError("trailing underscore in number") + end + + runStart = pos + else + break + end + end + + result ..= string.sub(src, runStart, pos - 1) + return result + end + + const function looksLikeDate(): boolean + -- YYYY- : 4 decimal digits followed by '-' + if pos + 4 > len then + return false + end + + for i = 0, 3 do + if not isDigit(buffer.readu8(srcBuf, pos + i - 1)) then + return false + end + end + + return buffer.readu8(srcBuf, pos + 3) == 45 + end + + const function looksLikeTime(): boolean + -- HH: : exactly 2 decimal digits followed by ':' + return isDigit(byteAt(0)) and isDigit(byteAt(1)) and byteAt(2) == 58 + end + + -- Returns days in month for given year and month (1-12) + const function daysInMonth(year: number, month: number): number + if month == 2 then + local isLeap = (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0) + return if isLeap then 29 else 28 + end + + if month == 4 or month == 6 or month == 9 or month == 11 then + return 30 + end + + return 31 + end + + const function parseDatetime(): string + local hasDate = looksLikeDate() + local result = "" + + if hasDate then + -- Parse YYYY-MM-DD + local yearStr = string.sub(src, pos, pos + 3) + pos += 4 + + if byteAt() ~= 45 then + tomlParseError("expected '-' in date") + end + + pos += 1 + + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit month") + end + + local monthStr = string.sub(src, pos, pos + 1) + pos += 2 + local month = tonumber(monthStr) :: number + + if month < 1 or month > 12 then + tomlParseError(`invalid month: {month}`) + end + + if byteAt() ~= 45 then + tomlParseError("expected '-' in date") + end + + pos += 1 + + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit day") + end + + local dayStr = string.sub(src, pos, pos + 1) + pos += 2 + + local day = tonumber(dayStr) :: number + local year = tonumber(yearStr) :: number + if day < 1 or day > daysInMonth(year, month) then + tomlParseError(`invalid day {day} for month {month}`) + end + + result = `{yearStr}-{monthStr}-{dayStr}` + + -- Optional datetime separator: T, t, or space followed by digit + local sep = byteAt() + if sep == 84 or sep == 116 then + pos += 1 + elseif sep == 32 and isDigit(byteAt(1)) then + pos += 1 + else + return result -- date-local + end + + result ..= "T" + end + + -- Parse HH:MM[:SS[.frac]] + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit hour") + end + + local hourStr = string.sub(src, pos, pos + 1) + pos += 2 + local hour = tonumber(hourStr) :: number + if hour > 23 then + tomlParseError(`invalid hour: {hour}`) + end + + if byteAt() ~= 58 then + tomlParseError("expected ':' in time") + end + + pos += 1 + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit minute") + end + + local minStr = string.sub(src, pos, pos + 1) + pos += 2 + local minute = tonumber(minStr) :: number + if minute > 59 then + tomlParseError(`invalid minute: {minute}`) + end + + result ..= `{hourStr}:{minStr}` + + -- Optional seconds (TOML 1.1) + local secStr = "00" + if byteAt() == 58 then + pos += 1 + + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit second") + end + + secStr = string.sub(src, pos, pos + 1) + pos += 2 + local sec = tonumber(secStr) :: number + if sec > 59 then + tomlParseError(`invalid second: {sec}`) + end + end + result ..= `:{secStr}` + + -- Optional fractional seconds + if byteAt() == 46 then + pos += 1 + + if not isDigit(byteAt()) then + tomlParseError("expected digit after '.' in time") + end + + local fracStart = pos + while isDigit(byteAt()) do + pos += 1 + end + + local fracPart = string.sub(src, fracStart, pos - 1) + result ..= `.{fracPart}` + end + + -- Optional offset: Z, +HH:MM, -HH:MM + local oc = byteAt() + if oc == 90 or oc == 122 then + if not hasDate then + tomlParseError("offset not allowed in time-local") + end + + pos += 1 + result ..= "Z" + elseif oc == 43 or oc == 45 then + if not hasDate then + tomlParseError("offset not allowed in time-local") + end + + local sign = string.char(oc :: number) + + pos += 1 + + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit offset hour") + end + + local ohStr = string.sub(src, pos, pos + 1) + pos += 2 + local oh = tonumber(ohStr) :: number + if oh > 23 then + tomlParseError(`invalid offset hour: {oh}`) + end + + if byteAt() ~= 58 then + tomlParseError("expected ':' in offset") + end + + pos += 1 + + if not (isDigit(byteAt(0)) and isDigit(byteAt(1))) then + tomlParseError("expected 2-digit offset minute") + end + + local omStr = string.sub(src, pos, pos + 1) + pos += 2 + local om = tonumber(omStr) :: number + if om > 59 then + tomlParseError(`invalid offset minute: {om}`) + end + + result ..= `{sign}{ohStr}:{omStr}` + end + + return result + end + + local parseValue: () -> TomlValue + + const numberPrefixTable: { [number]: (() -> number)? } = table.freeze({ + -- 0x + [120] = function() + return tonumber(parseDigits(isHexDigit), 16) :: number + end, + + -- 0o + [111] = function() + return tonumber(parseDigits(isOctDigit), 8) :: number + end, + + -- 0b + [98] = function() + return tonumber(parseDigits(isBinDigit), 2) :: number + end, + } :: any) -- FIXME(Luau): freezing the table breaks bidirectional typing + + const function parseNumOrDatetime(sign: string?): string | number + local signStr = sign or "" + local c = byteAt() + + -- bare inf / nan + if string.sub(src, pos, pos + 2) == "inf" then + pos += 3 + return if signStr == "-" then -math.huge else math.huge + end + + if string.sub(src, pos, pos + 2) == "nan" then + pos += 3 + return math.nan + end + + if not isDigit(c) then + tomlParseError(`expected digit, got '${string.char(c :: number)}'`) + end + + -- prefixed integer bases (no sign allowed) + if sign == nil and c == 48 then + local nc = byteAt(1) or 0 + + local parser = numberPrefixTable[nc] + if parser then + pos += 2 + return parser() + end + end + + -- datetime / time detection (no sign) + if sign == nil then + if looksLikeDate() then + return parseDatetime() + end + + if looksLikeTime() then + return parseDatetime() + end + end + + -- decimal integer or float + local isZero = c == 48 + local intDigits: string + if isZero then + pos += 1 + + if isDigit(byteAt()) then + tomlParseError("leading zero in integer") + end + + intDigits = "0" + else + intDigits = parseDigits(isDigit) + end + + local nc = byteAt() + local isFloat = false + local result = signStr .. intDigits + + if nc == 46 then -- '.' + pos += 1 + isFloat = true + + if not isDigit(byteAt()) then + tomlParseError("expected digit after decimal point") + end + + result ..= "." .. parseDigits(isDigit) + nc = byteAt() + end + + if nc == 101 or nc == 69 then -- e / E + pos += 1 + isFloat = true + + local expSign = "" + + local ec = byteAt() + if ec == 43 or ec == 45 then + expSign = string.char(ec :: number) + pos += 1 + end + + if not isDigit(byteAt()) then + tomlParseError("expected digit in exponent") + end + + result ..= "e" .. expSign .. parseDigits(isDigit) + end + + return if isFloat then tonumber(result) :: number else tonumber(signStr .. intDigits) :: number + end + + -- array and inline-table parsers (forward declared for mutual recursion) + local parseArray: () -> { TomlValue } + local parseInlineTable: () -> { [string]: TomlValue } + local parseKey: () -> { string } + local setNestedKey: (tbl: { [string]: TomlValue }, parts: { string }, value: TomlValue, fromDottedKey: boolean) -> () + + -- maps first byte of a value to its parser; digit range handled separately below + const valueDispatch: { [number]: () -> TomlValue } = table.freeze({ + -- when we find `"`... + [34] = function() + pos += 1 + + if string.sub(src, pos, pos + 1) == '""' then + pos += 2 + return parseMultilineBasicString() + end + + return parseBasicString() + end, + + -- when we find `'`... + [39] = function() + pos += 1 + + if string.sub(src, pos, pos + 1) == "''" then + pos += 2 + return parseMultilineLiteralString() + end + + return parseLiteralString() + end, + + -- when we find `+`... + [43] = function() + pos += 1 + return parseNumOrDatetime("+") + end, + + -- when we find `-`... + [45] = function() + pos += 1 + return parseNumOrDatetime("-") + end, + + -- when we find `[`... + [91] = function() + pos += 1 + return parseArray() + end, + + -- when we find `f`, check that it's `false` + [102] = function() + if string.sub(src, pos, pos + 4) ~= "false" then + tomlParseError("invalid value starting with 'f'") + end + + -- advance past `false` + pos += 5 + + if isBareKey(byteAt()) then + tomlParseError("invalid value") + end + + return false + end, + + -- when we find `i`, check that it's `inf` + [105] = function() + if string.sub(src, pos, pos + 2) ~= "inf" then + tomlParseError("invalid value starting with 'i'") + end + + -- advance past `inf` + pos += 3 + + return math.huge + end, + + -- when we find `n`, check that it's `nan` + [110] = function() + if string.sub(src, pos, pos + 2) ~= "nan" then + tomlParseError("invalid value starting with 'n'") + end + + -- advance past `nan` + pos += 3 + + return math.nan + end, + + -- when we find `t`, check that it's `true` + [116] = function() + if string.sub(src, pos, pos + 3) ~= "true" then + tomlParseError("invalid value starting with 't'") + end + + -- advance past `true` + pos += 4 + + if isBareKey(byteAt()) then + tomlParseError("invalid value") + end + + return true + end, + -- when we find `{`... + [123] = function() + pos += 1 + return parseInlineTable() + end, + }) + + parseValue = function(): TomlValue + skipWhitespace() + if pos > len then + tomlParseError("expected value") + end + + local c = byteAt() :: number + + local handler = valueDispatch[c] + if handler ~= nil then + return handler() + end + + if not isDigit(c) then + tomlParseError(`unexpected character '${string.char(c)}' in value`) + end + + return parseNumOrDatetime(nil) + end + + parseArray = function(): { TomlValue } + local result: { TomlValue } = {} + tableMeta[result] = { kind = "array-value", keys = {} } -- mark as static array + + skipWhitespaceNewlinesAndComments() + if byteAt() == 93 then + pos += 1 + return result + end -- empty [] + + while true do + skipWhitespaceNewlinesAndComments() + if pos > len then + tomlParseError("unterminated array") + end + + if byteAt() == 93 then + pos += 1 + break + end -- trailing comma case + + table.insert(result, parseValue()) + + skipWhitespaceNewlinesAndComments() + + if pos > len then + tomlParseError("unterminated array") + end + + local c = byteAt() + if c == 44 then -- ',' + pos += 1 + continue + end + + if c == 93 then -- ']' + pos += 1 + break + end + + tomlParseError(`expected ',' or ']' in array, got '${string.char(c :: number)}'`) + end + + return result + end + + parseKey = function(): { string } + skipWhitespace() + local parts: { string } = {} + + while true do + local c = byteAt() + local part: string + + if c == 34 then -- " + pos += 1 + part = parseBasicString() + elseif c == 39 then -- ' + pos += 1 + part = parseLiteralString() + elseif isBareKey(c) then + local start = pos + + while pos <= len do + local c2 = buffer.readu8(srcBuf, pos - 1) + if not isBareKey(c2) then + break + end + pos += 1 + end + + part = string.sub(src, start, pos - 1) + else + if #parts == 0 then + tomlParseError(`expected key, got '${string.char(c :: number or 0)}'`) + else + tomlParseError("expected key segment after '.'") + end + end + + table.insert(parts, part) + + skipWhitespace() + if byteAt() == 46 then -- '.' + pos += 1 + skipWhitespace() + else + break + end + end + + return parts + end + + const dottedKindErrors: { [TableKind]: (key: string) -> string } = table.freeze({ + ["array-element"] = function(k) + return `cannot extend explicitly-defined table '{k}' via dotted key` + end, + ["array-value"] = function(k) + return `cannot navigate through static array value '{k}'` + end, + ["defined"] = function(k) + return `cannot extend explicitly-defined table '{k}' via dotted key` + end, + ["inline"] = function(k) + return `cannot extend inline table via key '{k}'` + end, + } :: any) -- FIXME(Luau): freezing the table breaks bidirectional typing + + -- navigate through dotted intermediate keys, creating tables as needed; + -- returns (leaf_table, leaf_key_string) + const function navigateDotted( + tbl: { [string]: TomlValue }, + parts: { string }, + newKind: TableKind + ): ({ [string]: TomlValue }, string) + local current: { [string]: TomlValue } = tbl + + for i = 1, #parts - 1 do + local k = parts[i] + local child = current[k] + + if child == nil then + local newTbl: { [string]: TomlValue } = {} + tableMeta[newTbl] = { kind = newKind, keys = {} } + current[k] = newTbl + getMeta(current).keys[k] = true + current = newTbl + elseif typeof(child) == "table" then + if arrayTableSets[child] then + tomlParseError(`cannot extend array-of-tables '${k}' via dotted key`) + end + + local m = getMeta(child) + + local kindError = dottedKindErrors[m.kind] + if kindError ~= nil then + tomlParseError(kindError(k)) + end + + current = child :: { [string]: TomlValue } + else + tomlParseError(`key '${k}' is not a table`) + end + end + return current, parts[#parts] + end + + setNestedKey = function(tbl: { [string]: TomlValue }, parts: { string }, value: TomlValue, fromDottedKey: boolean) + local newKind = if fromDottedKey then "implicit-dotted" else "implicit-header" + local leafTbl, leafKey = navigateDotted(tbl, parts, newKind) + + local m = getMeta(leafTbl) + + if m.keys[leafKey] then + tomlParseError(`duplicate key '${leafKey}'`) + end + + m.keys[leafKey] = true + leafTbl[leafKey] = value + + -- tag any freshly assigned table value + if typeof(value) == "table" and not tableMeta[value] then + tableMeta[value] = { kind = newKind, keys = {} } + end + end + + parseInlineTable = function(): { [string]: TomlValue } + local result: { [string]: TomlValue } = {} + tableMeta[result] = { kind = "inline", keys = {} } + skipWhitespaceNewlinesAndComments() -- TOML 1.1: newlines allowed inside inline tables + + if byteAt() == 125 then + pos += 1 + return result + end -- empty {} + + while true do + skipWhitespaceNewlinesAndComments() + if pos > len then + tomlParseError("unterminated inline table") + end + + if byteAt() == 125 then + pos += 1 + break + end -- '}' (trailing comma support) + + local keys = parseKey() + skipWhitespace() + + if byteAt() ~= 61 then + tomlParseError("expected '=' in inline table") + end -- '=' + + pos += 1 + + local value = parseValue() + setNestedKey(result, keys, value, false) + skipWhitespaceNewlinesAndComments() + + if pos > len then + tomlParseError("unterminated inline table") + end + + local c = byteAt() + if c == 44 then -- ',' + pos += 1 + continue + end + + if c == 125 then -- '}' + pos += 1 + break + end + + tomlParseError(`expected ',' or '}}' in inline table, got '${string.char(c :: number)}'`) + end + + return result + end + + -- kind errors when traversing intermediate path components under a [header] + const headerPathKindErrors: { [TableKind]: (key: string) -> string } = table.freeze({ + ["array-value"] = function(k) + return `key '{k}' is a static array, cannot navigate through it` + end, + ["inline"] = function(_) + return "cannot extend inline table" + end, + } :: any) -- FIXME(Luau): freezing the table breaks bidirectional typing + + -- kind errors when the leaf key of a [header] already exists as a table + const headerLeafKindErrors: { [TableKind]: (key: string) -> string } = table.freeze({ + ["array-element"] = function(k) + return `cannot define [{k}]: already an array-of-tables element` + end, + ["array-value"] = function(k) + return `cannot define [{k}]: key is a static array value` + end, + ["defined"] = function(k) + return `duplicate table header [{k}]` + end, + ["implicit-dotted"] = function(k) + return `table [{k}] was already defined via a dotted key` + end, + ["inline"] = function(_) + return "cannot extend inline table with a table header" + end, + } :: any) -- FIXME(Luau): freezing the table breaks bidirectional typing + + -- navigate to the table referenced by a [key] or [[key]] header + const function navigateToHeader( + root: { [string]: TomlValue }, + parts: { string }, + isArrayTable: boolean + ): { [string]: TomlValue } + local current: { [string]: TomlValue } = root + + for i = 1, #parts - 1 do + local k = parts[i] + local child = current[k] + + if child == nil then + local newTbl: { [string]: TomlValue } = {} + tableMeta[newTbl] = { kind = "implicit-header", keys = {} } + current[k] = newTbl + getMeta(current).keys[k] = true + + current = newTbl + continue + end + + if typeof(child) ~= "table" then + tomlParseError(`key '${k}' is not a table`) + end + + if arrayTableSets[child] then + local childAsArray = child :: { TomlValue } + current = childAsArray[#childAsArray] :: { [string]: TomlValue } + continue + end + + local m = getMeta(child) + + local kindError = headerPathKindErrors[m.kind] + if kindError ~= nil then + tomlParseError(kindError(k)) + end + + current = child :: { [string]: TomlValue } + end + + local leafKey = parts[#parts] + local existing = current[leafKey] + + if isArrayTable then + if existing == nil then + local arr: { { [string]: TomlValue } } = {} + + arrayTableSets[arr] = true + current[leafKey] = arr :: { TomlValue } + getMeta(current).keys[leafKey] = true + + local entry: { [string]: TomlValue } = {} + tableMeta[entry] = { kind = "array-element", keys = {} } + table.insert(arr, entry) + + return entry + end + + if typeof(existing) == "table" and arrayTableSets[existing] then + local entry: { [string]: TomlValue } = {} + tableMeta[entry] = { kind = "array-element", keys = {} } + table.insert(existing :: { { [string]: TomlValue } }, entry) + + return entry + end + + tomlParseError(`cannot define [[${leafKey}]]: key already exists as a different type`) + end + + if existing == nil then + local newTbl: { [string]: TomlValue } = {} + tableMeta[newTbl] = { kind = "defined", keys = {} } + current[leafKey] = newTbl + getMeta(current).keys[leafKey] = true + + return newTbl + end + + if typeof(existing) ~= "table" then + tomlParseError(`key '${leafKey}' is not a table`) + end + + -- Check arrayTableSets BEFORE getMeta to avoid creating spurious meta + if arrayTableSets[existing] then + tomlParseError(`cannot define [${leafKey}]: already an array-of-tables`) + end + + local m = getMeta(existing) + + if m.kind == "implicit-header" then + m.kind = "defined" + return existing :: { [string]: TomlValue } + end + + local kindError = headerLeafKindErrors[m.kind] + + if kindError ~= nil then + tomlParseError(kindError(leafKey)) + end + + tomlParseError(`cannot redefine [${leafKey}]`) + + -- FIXME(Luau): `tomlParseError` paths are not currently treated as not returning + error("unreachable") + end + + -- main parse loop + + local root: { [string]: TomlValue } = {} + tableMeta[root] = { kind = "root", keys = {} } + local currentTable: { [string]: TomlValue } = root + + while pos <= len do + skipWhitespaceNewlinesAndComments() + if pos > len then + break + end + + local c = byteAt() :: number + + if c == 91 then -- '[' + if byteAt(1) == 91 then -- '[[' + pos += 2 + + skipWhitespace() + local parts = parseKey() + skipWhitespace() + + if string.sub(src, pos, pos + 1) ~= "]]" then + tomlParseError("expected ']]'") + end + + pos += 2 + expectLineEnd() + + currentTable = navigateToHeader(root, parts, true) + else + pos += 1 + + skipWhitespace() + local parts = parseKey() + skipWhitespace() + + if byteAt() ~= 93 then + tomlParseError("expected ']'") + end -- ']' + + pos += 1 + expectLineEnd() + + currentTable = navigateToHeader(root, parts, false) + end + else + local parts = parseKey() + skipWhitespace() + + if byteAt() ~= 61 then + tomlParseError("expected '='") + end -- '=' + + pos += 1 + + local value = parseValue() + setNestedKey(currentTable, parts, value, true) + + expectLineEnd() + end + end + + return root end -- user-facing diff --git a/definitions/crypto.luau b/definitions/crypto.luau index 821e95548..7b6928a4e 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -1,24 +1,53 @@ +-- lute-lint-global-ignore(unused_variable) local crypto = {} export type Hash = { __hash: T } -crypto.hash = { +crypto.hash = table.freeze({ md5 = {} :: Hash<"md5">, sha1 = {} :: Hash<"sha1">, sha256 = {} :: Hash<"sha256">, sha512 = {} :: Hash<"sha512">, blake2b256 = {} :: Hash<"blake2b256">, -} -crypto.password = {} +}) +--- Computes a cryptographic hash of `message` using the given `hash` algorithm. Returns the hash as a buffer. function crypto.digest(hash: Hash, message: string | buffer): buffer error("not implemented") end +crypto.secretbox = {} + +--- A sealed message containing the encrypted ciphertext, nonce, and key. +export type SecretBox = { + read ciphertext: buffer, + read nonce: buffer, + read key: buffer, +} + +--- Generates a new random secret key for use with `secretbox.seal`. +function crypto.secretbox.keygen(): buffer + error("not implemented") +end + +--- Encrypts `message` with `key` (or a freshly generated key if omitted). Returns a `SecretBox`. +function crypto.secretbox.seal(message: string | buffer, key: buffer?): SecretBox + error("not implemented") +end + +--- Decrypts `box` and returns the original plaintext as a buffer. +function crypto.secretbox.open(box: SecretBox): buffer + error("not implemented") +end + +crypto.password = {} + +--- Hashes `password` using a slow, memory-hard algorithm suitable for password storage. Returns the hash as a buffer. function crypto.password.hash(password: string): buffer error("not implemented") end +--- Verifies that `password` matches the stored `hash`. Returns `true` if the password is correct. function crypto.password.verify(hash: buffer, password: string): boolean error("not implemented") end diff --git a/definitions/fs.luau b/definitions/fs.luau index 4dbfe83ed..275a1a2c1 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -1,105 +1,141 @@ +-- lute-lint-global-ignore(unused_variable) +local time = require("./time") + local fs = {} +--- An open file handle returned by `fs.open`. export type FileHandle = { fd: number, err: number, } +--- The type of a filesystem entry: +--- - `"file"`: A regular file. +--- - `"dir"`: A directory. +--- - `"link"`: A symbolic link. +--- - `"fifo"`: A named pipe (FIFO). +--- - `"socket"`: A Unix domain socket. +--- - `"char"`: A character device. +--- - `"block"`: A block device. +--- - `"unknown"`: An entry whose type could not be determined. export type FileType = "file" | "dir" | "link" | "fifo" | "socket" | "char" | "block" | "unknown" +--- How a file should be opened: +--- - `"r"`: Open for reading. Fails if the file does not exist. +--- - `"w"`: Open for writing; creates the file if absent, truncates it if present. +--- - `"x"`: Open for exclusive creation. Fails if the file already exists. +--- - `"a"`: Open for appending; creates the file if absent, writes go to the end. +--- - `"r+"`: Open for reading and writing. Fails if the file does not exist. +--- - `"w+"`: Open for reading and writing; creates the file if absent, truncates it if present. +--- - `"x+"`: Open for reading and exclusive creation. Fails if the file already exists. +--- - `"a+"`: Open for reading and appending; creates the file if absent, writes go to the end. export type HandleMode = "r" | "w" | "x" | "a" | "r+" | "w+" | "x+" | "a+" +--- Metadata about a filesystem entry, such as size, type, and timestamps. export type FileMetadata = { type: FileType, permissions: { readonly: boolean }, size: number, - created_at: any, - accessed_at: any, - modified_at: any, + created: time.Duration, + accessed: time.Duration, + modified: time.Duration, } +--- A single entry returned by `fs.listdir`, containing the entry's name and type. export type DirectoryEntry = { name: string, type: FileType, } +--- A handle to an active filesystem watcher, returned by `fs.watch`. export type WatchHandle = { close: (self: WatchHandle) -> (), } +--- A filesystem change event passed to the `fs.watch` callback. export type WatchEvent = { change: boolean, rename: boolean, } +--- Opens the file at `path` in the given `mode` (defaults to `"r"`). Returns a file handle. function fs.open(path: string, mode: HandleMode?): FileHandle error("not implemented") end +--- Reads the full contents of `handle` and returns them as a string. function fs.read(handle: FileHandle): string error("not implemented") end +--- Writes `contents` to `handle`. function fs.write(handle: FileHandle, contents: string): () error("not implemented") end +--- Closes `handle`, flushing any pending writes. function fs.close(handle: FileHandle): () error("not implemented") end +--- Removes the file at `path`. function fs.remove(path: string): () error("not implemented") end +--- Returns metadata for the file or directory at `path`. function fs.stat(path: string): FileMetadata error("not implemented") end +--- Returns the `FileType` of the entry at `path`. function fs.type(path: string): FileType error("not implemented") end +--- Creates a directory at the specified path. +--- To set the permissions mode for a directory (Unix only), see @std/process for `run` or `system` to shell out to `chmod` or the equivalent. function fs.mkdir(path: string): () error("not implemented") end +--- Creates a hard link at `dest` pointing to `src`. function fs.link(src: string, dest: string): () error("not implemented") end +--- Creates a symbolic link at `dest` pointing to `src`. function fs.symlink(src: string, dest: string): () error("not implemented") end +--- Watches `path` for filesystem changes, calling `callback` with the filename and event on each change. function fs.watch(path: string, callback: (filename: string, event: WatchEvent) -> ()): WatchHandle error("not implemented") end +--- Returns `true` if a file or directory exists at `path`. function fs.exists(path: string): boolean error("not implemented") end +--- Copies the file at `src` to `dest`. function fs.copy(src: string, dest: string): () error("not implemented") end -function fs.listdir(path: string): { DirectoryEntry } - error("not implemented") -end - -function fs.rmdir(path: string): () +--- Moves the file or directory at `src` to `dest`. +--- Falls back to a copy-then-remove if `src` and `dest` are on different filesystems. +function fs.move(src: string, dest: string): () error("not implemented") end -function fs.readfiletostring(filepath: string): string - error("not implemented") -end - -function fs.writestringtofile(filepath: string, contents: string): () +--- Returns an array of `DirectoryEntry` values for the immediate children of the directory at `path`. +function fs.listdir(path: string): { DirectoryEntry } error("not implemented") end -function fs.readasync(filepath: string): string +--- Removes the directory at `path`. +function fs.rmdir(path: string): () error("not implemented") end diff --git a/definitions/io.luau b/definitions/io.luau new file mode 100644 index 000000000..acad37b88 --- /dev/null +++ b/definitions/io.luau @@ -0,0 +1,13 @@ +local io = {} + +--- Writes one or more strings to standard output without a trailing newline. +function io.write(...: string): () + error("unimplemented") +end + +--- Reads a line from standard input and returns it as a string. +function io.read(): string + error("unimplemented") +end + +return io diff --git a/definitions/luau.luau b/definitions/luau.luau index 2318ae9f6..0d3c79039 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,576 +1,94 @@ -local luau = {} - -export type Position = { - line: number, - column: number, -} - -export type Location = { - begin: Position, - ["end"]: Position, -- TODO: do we really want to have to use brackets everywhere? -} - -export type Whitespace = { - tag: "whitespace", - location: Location, - text: string, -} - -export type SingleLineComment = { - tag: "comment", - location: Location, - text: string, -} - -export type MultiLineComment = { - tag: "blockcomment", - location: Location, - text: string, - -- TODO: depth: number, -} - -export type Trivia = Whitespace | SingleLineComment | MultiLineComment - -export type Token = { - leadingTrivia: { Trivia }, - position: Position, - text: Kind, - trailingTrivia: { Trivia }, -} - -export type Eof = Token<""> & { tag: "eof" } - -export type Pair = { node: T, separator: Token? } -export type Punctuated = { Pair } - -export type AstLocal = { - name: Token, - colon: Token<":">?, - annotation: AstType?, - shadows: AstLocal?, -} - -export type AstExprGroup = { - tag: "group", - openParens: Token<"(">, - expression: AstExpr, - closeParens: Token<")">, -} - -export type AstExprConstantNil = Token<"nil"> & { tag: "nil" } - -export type AstExprConstantBool = Token<"true" | "false"> & { tag: "boolean", value: boolean } - -export type AstExprConstantNumber = Token & { tag: "number", value: number } - -export type AstExprConstantString = Token & { - tag: "string", - quoteStyle: "single" | "double" | "block" | "interp", - blockDepth: number, -} - -export type AstExprLocal = { - tag: "local", - token: Token, - ["local"]: AstLocal, - upvalue: boolean, -} - -export type AstExprGlobal = { - tag: "global", - name: Token, -} - -export type AstExprVarargs = Token<"..."> & { tag: "vararg" } - -export type AstExprCall = { - tag: "call", - func: AstExpr, - openParens: Token<"(">?, - arguments: Punctuated, - closeParens: Token<")">?, - self: boolean, - argLocation: Location, -} - -export type AstExprIndexName = { - tag: "indexname", - expression: AstExpr, - accessor: Token<"." | ":">, - index: Token, - indexLocation: Location, -} - -export type AstExprIndexExpr = { - tag: "index", - expression: AstExpr, - openBrackets: Token<"[">, - index: AstExpr, - closeBrackets: Token<"]">, -} - -export type AstFunctionBody = { - openGenerics: Token<"<">?, - generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, - self: AstLocal?, - openParens: Token<"(">, - parameters: Punctuated, - vararg: Token<"...">?, - varargColon: Token<":">?, - varargAnnotation: AstTypePack?, - closeParens: Token<")">, - returnSpecifier: Token<":">?, - returnAnnotation: AstTypePack?, - body: AstStatBlock, - endKeyword: Token<"end">, -} - -export type AstExprAnonymousFunction = { - tag: "function", - attributes: { AstAttribute }, - functionKeyword: Token<"function">, - body: AstFunctionBody, -} - -export type AstExprTableItem = | { kind: "list", value: AstExpr, separator: Token<"," | ";">? } | { - kind: "record", - key: Token, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, -} | { - kind: "general", - indexerOpen: Token<"[">, - key: AstExpr, - indexerClose: Token<"]">, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, -} - -export type AstExprTable = { - tag: "table", - openBrace: Token<"{">, - entries: { AstExprTableItem }, - closeBrace: Token<"}">, -} - -export type AstExprUnary = { - tag: "unary", - operator: Token<"not" | "-" | "#">, - operand: AstExpr, -} - -export type AstExprBinary = { - tag: "binary", - lhsoperand: AstExpr, - operator: Token, -- TODO: enforce token type - rhsoperand: AstExpr, -} - -export type AstExprInterpString = { - tag: "interpolatedstring", - strings: { Token }, - expressions: { AstExpr }, -} - -export type AstExprTypeAssertion = { - tag: "cast", - operand: AstExpr, - operator: Token<"::">, - annotation: AstType, -} - -export type AstExprIfElseIfs = { - elseifKeyword: Token<"elseif">, - condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstExpr, -} - -export type AstExprIfElse = { - tag: "conditional", - ifKeyword: Token<"if">, - condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstExpr, - elseifs: { AstExprIfElseIfs }, - elseKeyword: Token<"else">, - antecedent: AstExpr, -} - -export type AstExpr = - | AstExprGroup - | AstExprConstantNil - | AstExprConstantBool - | AstExprConstantNumber - | AstExprConstantString - | AstExprLocal - | AstExprGlobal - | AstExprVarargs - | AstExprCall - | AstExprIndexName - | AstExprIndexExpr - | AstExprAnonymousFunction - | AstExprTable - | AstExprUnary - | AstExprBinary - | AstExprInterpString - | AstExprTypeAssertion - | AstExprIfElse - -export type AstStatBlock = { - tag: "block", - statements: { AstStat }, -} - -export type AstStatElseIf = { - elseifKeyword: Token<"elseif">, - condition: AstExpr, - thenKeyword: Token<"then">, - consequent: AstStatBlock, -} - -export type AstStatIf = { - tag: "conditional", - 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">, -} - -export type AstStatWhile = { - tag: "while", - whileKeyword: Token<"while">, - condition: AstExpr, - doKeyword: Token<"do">, - body: AstStatBlock, - endKeyword: Token<"end">, -} - -export type AstStatRepeat = { - tag: "repeat", - repeatKeyword: Token<"repeat">, - body: AstStatBlock, - untilKeyword: Token<"until">, - condition: AstExpr, -} - -export type AstStatBreak = Token<"break"> & { tag: "break" } +-- lute-lint-global-ignore(unused_variable) -export type AstStatContinue = Token<"continue"> & { tag: "continue" } - -export type AstStatReturn = { - tag: "return", - returnKeyword: Token<"return">, - expressions: Punctuated, -} - -export type AstStatExpr = { - tag: "expression", - expression: AstExpr, -} - -export type AstStatLocal = { - tag: "local", - localKeyword: Token<"local">, - variables: Punctuated, - equals: Token<"=">?, - values: Punctuated, -} - -export type AstStatFor = { - tag: "for", - forKeyword: Token<"for">, - variable: AstLocal, - equals: Token<"=">, - from: AstExpr, - toComma: Token<",">, - to: AstExpr, - stepComma: Token<",">?, - step: AstExpr?, - doKeyword: Token<"do">, - body: AstStatBlock, - endKeyword: Token<"end">, -} - -export type AstStatForIn = { - tag: "forin", - forKeyword: Token<"for">, - variables: Punctuated>, - inKeyword: Token<"in">, - values: Punctuated, - doKeyword: Token<"do">, - body: AstStatBlock, - endKeyword: Token<"end">, -} - -export type AstStatAssign = { - tag: "assign", - variables: Punctuated, - equals: Token<"=">, - values: Punctuated, -} - -export type AstStatCompoundAssign = { - tag: "compoundassign", - variable: AstExpr, - operand: Token, -- TODO: enforce token type, - value: AstExpr, -} - -export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { tag: "attribute" } - -export type AstStatFunction = { - tag: "function", - attributes: { AstAttribute }, - functionKeyword: Token<"function">, - name: AstExpr, - body: AstFunctionBody, -} - -export type AstStatLocalFunction = { - tag: "localfunction", - attributes: { AstAttribute }, - localKeyword: Token<"local">, - functionKeyword: Token<"function">, - name: AstLocal, - body: AstFunctionBody, -} - -export type AstStatTypeAlias = { - tag: "typealias", - export: Token<"export">?, - typeToken: Token<"type">, - name: Token, - openGenerics: Token<"<">?, - generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, - equals: Token<"=">, - type: AstType, -} - -export type AstStatTypeFunction = { - tag: "typefunction", - export: Token<"export">?, - type: Token<"type">, - functionKeyword: Token<"function">, - name: Token, - body: AstFunctionBody, -} - -export type AstStat = - | AstStatBlock - | AstStatIf - | AstStatWhile - | AstStatRepeat - | AstStatBreak - | AstStatContinue - | AstStatReturn - | AstStatExpr - | AstStatLocal - | AstStatFor - | AstStatForIn - | AstStatAssign - | AstStatCompoundAssign - | AstStatFunction - | AstStatLocalFunction - | AstStatTypeAlias - | AstStatTypeFunction - -export type AstGenericType = { - tag: "generic", - name: Token, - equals: Token<"=">?, - default: AstType?, -} - -export type AstGenericTypePack = { - tag: "genericpack", - name: Token, - ellipsis: Token<"...">, - equals: Token<"=">?, - default: AstTypePack?, -} - -export type AstTypeReference = { - tag: "reference", - prefix: Token?, - prefixPoint: Token<".">?, - name: Token, - openParameters: Token<"<">?, - parameters: Punctuated?, - closeParameters: Token<">">?, -} - -export type AstTypeSingletonBool = Token<"true" | "false"> & { - tag: "boolean", - value: boolean, -} - -export type AstTypeSingletonString = Token & { - tag: "string", - quoteStyle: "single" | "double", -} - -export type AstTypeTypeof = { - tag: "typeof", - typeof: Token<"typeof">, - openParens: Token<"(">, - expression: AstExpr, - closeParens: Token<")">, -} - -export type AstTypeGroup = { - tag: "group", - openParens: Token<"(">, - type: AstType, - closeParens: Token<">">, -} - -export type AstTypeOptional = Token<"?"> & { tag: "optional" } - -export type AstTypeUnion = { - tag: "union", - leading: Token<"|">?, - -- Separator may be nil for AstTypeOptional - types: Punctuated, -} - -export type AstTypeIntersection = { - tag: "intersection", - leading: Token<"&">?, - types: Punctuated, -} - -export type AstTypeArray = { - tag: "array", - openBrace: Token<"{">, - access: Token<"read" | "write">?, - type: AstType, - closeBrace: Token<"}">, -} - -export type AstTypeTableItem = { - kind: "indexer", - access: Token<"read" | "write">?, - indexerOpen: Token<"[">, - key: AstType, - indexerClose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, -} | { - kind: "stringproperty", - access: Token<"read" | "write">?, - indexerOpen: Token<"[">, - key: AstTypeSingletonString, - indexerClose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, -} | { - kind: "property", - access: Token<"read" | "write">?, - key: AstTypeSingletonString, - indexerClose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, -} - -export type AstTypeTable = { - tag: "table", - openBrace: Token<"{">, - entries: { AstTypeTableItem }, - closeBrace: Token<"}">, -} - -export type AstTypeFunctionParameter = { - name: Token?, - colon: Token<":">?, - type: AstType, -} - -export type AstTypeFunction = { - tag: "function", - openGenerics: Token<"<">?, - generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, - openParens: Token<"(">, - parameters: Punctuated, - vararg: AstTypePack?, - closeParens: Token<")">, - returnArrow: Token<"->">, - returnTypes: AstTypePack, -} - -export type AstType = - | AstTypeReference - | AstTypeSingletonBool - | AstTypeSingletonString - | AstTypeTypeof - | AstTypeGroup - | AstTypeUnion - | AstTypeIntersection - | AstTypeOptional - | AstTypeArray - | AstTypeTable - | AstTypeFunction - -export type AstTypePackExplicit = { - tag: "explicit", - openParens: Token<"(">?, - types: Punctuated, - tailType: AstTypePack?, - closeParens: Token<")">?, -} - -export type AstTypePackGeneric = { - tag: "generic", - name: Token, - ellipsis: Token<"...">, -} - -export type AstTypePackVariadic = { - 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 +local luau = {} -export type ParseResult = { - root: AstStatBlock, - eof: Eof, - lines: number, - lineOffsets: { number }, -} +--- Compiled Luau bytecode produced by `luau.compile`. +export type Bytecode = { bytecode: string } -function luau.parse(source: string): ParseResult +--- Compiles Luau `source` to bytecode. +function luau.compile(source: string): Bytecode error("not implemented") end -function luau.parseexpr(source: string): AstExpr +--- Loads `bytecode` into a callable function. `chunkname` names the chunk for error messages. An optional `env` table can override the global environment. +function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }?): (...any) -> ...any error("not implemented") end -export type Bytecode = { bytecode: string } - -function luau.compile(source: string): Bytecode +--- Resolves the require path `path` relative to `fromchunkname`, returning the absolute path. +function luau.resolveModule(path: string, fromchunkname: string): string error("not implemented") end -function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }): (...any) -> ...any +--- A resolved Luau type, as returned by `typeofModule`. +export type Type = { + tag: "nil" + | "unknown" + | "never" + | "any" + | "boolean" + | "number" + | "string" + | "buffer" + | "thread" + | "singleton" + | "negation" + | "union" + | "intersection" + | "table" + | "function" + | "extern" + | "generic", + + -- for singleton type + value: string | boolean | nil, + + -- for negation type + inner: Type, + + -- for union and intersection types + components: { Type }, + + -- for table type + properties: { [string]: { read: Type?, write: Type? } }, + indexer: { index: Type }?, -- TODO: add readresult and writeresult + -- TODO: readindexer: { index: type, result: type } }?, + -- TODO: writeindexer: { index: type, result: type } }?, + metatable: Type?, + + -- for function type + parameters: TypePack, + argnames: { string? }, + returns: TypePack, + generics: { Type }, + genericpacks: { TypePack }, + + -- for extern type + -- 'properties', 'metatable' are shared with table type + parent: Type?, + + -- for generic type + name: string, + ispack: boolean, +} + +--- A resolved Luau type pack, as returned by `typeofModule`. +export type TypePack = { + tag: "typepack" | "variadic" | "generic", + head: { Type }?, + tail: TypePack?, + + -- for variadic type packs + type: Type, + hidden: boolean, + + -- for generic type packs + name: string, + ispack: boolean, +} + +--- Returns the type pack representing the return types of the module at `modulepath`, or `nil` if unavailable. +function luau.typeofModule(modulepath: string): TypePack? error("not implemented") end diff --git a/definitions/net.luau b/definitions/net.luau deleted file mode 100644 index eb80bab32..000000000 --- a/definitions/net.luau +++ /dev/null @@ -1,48 +0,0 @@ -local net = {} - -export type Metadata = { - method: string?, - body: string?, - headers: { [string]: string }?, -} - -export type Request = { - body: string, - headers: { [string]: string }, - status: number, - ok: boolean, -} - -function net.request(url: string, metadata: Metadata?): Request - error("not implemented") -end - -export type ReceivedRequest = { - method: string, - path: string, - body: string, - query: { [string]: string }, - headers: { [string]: string }, -} - -export type ServerResponse = string | { - status: number?, - body: string?, - headers: { [string]: string }?, -} - -export type Handler = (request: ReceivedRequest) -> ServerResponse - -export type Configuration = { - hostname: string?, - port: number?, - reuseport: boolean?, - tls: { certfilename: string, keyfilename: string, passphrase: string?, cafilename: string? }?, - handler: Handler, -} - -function net.serve(config: Handler | Configuration) - error("not implemented") -end - -return net diff --git a/definitions/net/client.luau b/definitions/net/client.luau new file mode 100644 index 000000000..c4e700890 --- /dev/null +++ b/definitions/net/client.luau @@ -0,0 +1,44 @@ +-- lute-lint-global-ignore(unused_variable) +local client = {} + +--- HTTP request metadata, including optional method, body, and headers. +export type Metadata = { + method: string?, + body: string?, + headers: { [string]: string }?, +} + +--- An HTTP response, containing status code, headers, body, and a success flag. +export type Response = { + body: string, + headers: { [string]: string }, + status: number, + ok: boolean, +} + +--- Makes an HTTP request to `url` with optional request metadata. Returns the response. +function client.request(url: string, metadata: Metadata?): Response + error("not implemented") +end + +--- Options for establishing a WebSocket connection, including optional event handlers. +export type WebSocketOptions = { + headers: { [string]: string }?, + onopen: (() -> ())?, + onmessage: ((message: string | buffer) -> ())?, + onclose: ((code: number, reason: string) -> ())?, + onerror: ((error: string) -> ())?, +} + +--- A client-side WebSocket connection handle. +export type WebSocket = { + send: (self: WebSocket, data: string | buffer) -> (), + close: (self: WebSocket) -> (), +} + +--- Opens a WebSocket connection to `url`. Returns a `WebSocket` handle. +function client.websocket(url: string, options: WebSocketOptions?): WebSocket + error("not implemented") +end + +return client diff --git a/definitions/net/init.luau b/definitions/net/init.luau new file mode 100644 index 000000000..d828a7f4c --- /dev/null +++ b/definitions/net/init.luau @@ -0,0 +1,32 @@ +local client = require("@self/client") +local server = require("@self/server") + +local net = {} + +--- HTTP request metadata, including optional method, body, and headers. +export type Metadata = client.Metadata +--- An HTTP response, containing status code, headers, body, and a success flag. +export type Response = client.Response +--- Options for establishing a WebSocket connection, including optional event handlers. +export type WebSocketOptions = client.WebSocketOptions +--- A client-side WebSocket connection handle. +export type WebSocket = client.WebSocket +--- An HTTP request received by the server, containing method, path, body, query params, and headers. +export type ReceivedRequest = server.ReceivedRequest +--- A response that can be returned from a server handler: a plain string body, or a table with status, body, and headers. +export type ServerResponse = server.ServerResponse +--- A request handler function: receives a `ReceivedRequest` and `Server`, and returns a `ServerResponse`. +export type Handler = server.Handler +--- Configuration for starting an HTTP server. +export type Configuration = server.Configuration +--- A running HTTP server handle, with hostname, port, and lifecycle methods. +export type Server = server.Server +--- A server-side WebSocket connection handle. +export type ServerWebSocket = server.ServerWebSocket +--- Event handlers for server-side WebSocket connections. +export type WebSocketHandlers = server.WebSocketHandlers + +net.client = client +net.server = server + +return net diff --git a/definitions/net/server.luau b/definitions/net/server.luau new file mode 100644 index 000000000..7b797c31f --- /dev/null +++ b/definitions/net/server.luau @@ -0,0 +1,60 @@ +-- lute-lint-global-ignore(unused_variable) +local server = {} + +--- An HTTP request received by the server, containing method, path, body, query params, and headers. +export type ReceivedRequest = { + method: string, + path: string, + body: string, + query: { [string]: string }, + headers: { [string]: string }, +} + +--- A response that can be returned from a server handler: a plain string body, or a table with status, body, and headers. +export type ServerResponse = string | { + status: number?, + body: string?, + headers: { [string]: string }?, +} + +--- A server-side WebSocket connection handle. +export type ServerWebSocket = { + send: (self: ServerWebSocket, data: string | buffer) -> number, + close: (self: ServerWebSocket, code: number?, message: string?) -> (), +} + +--- Event handlers for server-side WebSocket connections. +export type WebSocketHandlers = { + open: ((ws: ServerWebSocket) -> ())?, + message: ((ws: ServerWebSocket, message: string | buffer) -> ())?, + close: ((ws: ServerWebSocket, code: number, message: string) -> ())?, + drain: ((ws: ServerWebSocket) -> ())?, +} + +--- A running HTTP server handle, with hostname, port, and lifecycle methods. +export type Server = { + hostname: string, + port: number, + close: () -> boolean, + upgrade: (self: Server, req: ReceivedRequest) -> boolean, +} + +--- A request handler function: receives a `ReceivedRequest` and `Server`, and returns a `ServerResponse`. +export type Handler = (request: ReceivedRequest, server: Server) -> ServerResponse? + +--- Configuration for starting an HTTP server. +export type Configuration = { + hostname: string?, + port: number?, + reuseport: boolean?, + tls: { certfilename: string, keyfilename: string, passphrase: string?, cafilename: string? }?, + handler: Handler?, + websocket: WebSocketHandlers?, +} + +--- Starts an HTTP server with the given handler or full configuration. Returns a `Server` handle. +function server.serve(config: Handler | Configuration): Server + error("not implemented") +end + +return server diff --git a/definitions/process.luau b/definitions/process.luau index b839ecd9c..8c22fadc4 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -1,13 +1,35 @@ -export type StdioKind = "default" | "inherit" | "none" | "" +-- lute-lint-global-ignore(unused_variable) +--- How a child process's standard streams should be handled: +--- - `"default"`: Capture stdout and stderr to pipes, accessible via `ProcessResult`. +--- - `"inherit"`: Pass the parent process's stdio streams through to the child. +--- - `"none"`: Discard the child's stdio streams. +export type StdioKind = "default" | "inherit" | "none" +--- Options for `process.run`: +--- - `cwd`: The working directory for the child process. Defaults to the current working directory. +--- - `stdio`: How to handle the child's stdio streams. Defaults to `"default"`. +--- - `env`: Environment variables for the child process. If omitted, inherits the parent's environment. export type ProcessRunOptions = { - shell: (string | boolean)?, cwd: string?, stdio: StdioKind?, env: { [string]: string }?, } +--- Options for `process.system`: +--- - `system`: The shell executable to use. If omitted, uses the platform default (`$SHELL` on Unix, `%COMSPEC%` on Windows). +--- - `cwd`: The working directory for the child process. Defaults to the current working directory. +--- - `stdio`: How to handle the child's stdio streams. Defaults to `"default"`. +--- - `env`: Environment variables for the child process. If omitted, inherits the parent's environment. +export type ProcessSystemOptions = { + system: string?, + cwd: string?, + stdio: StdioKind?, + + env: { [string]: string }?, +} + +--- The result of a completed process, including exit code and captured stdout/stderr. export type ProcessResult = { stdout: string, stderr: string, @@ -17,19 +39,73 @@ export type ProcessResult = { signal: string?, } +--- A handle returned by `process.signal`. Call `:close()` to deregister the handler. +export type SignalHandle = { + close: (self: SignalHandle) -> (), +} + +--- An operating system signal name. +--- +--- Only `SIGINT` and `SIGTERM` are guaranteed to be supported on all platforms. +--- Other signals may be unsupported, but will result in no-op behavior when used. +export type Signal = + | "SIGINT" + | "SIGTERM" + | "SIGHUP" + | "SIGQUIT" + | "SIGUSR1" + | "SIGUSR2" + | "SIGWINCH" + | "SIGPIPE" + | "SIGBREAK" + | "SIGALRM" + local process = {} +--- Command-line arguments passed to the current script, as an array of strings. +process.args = {} :: { string } +--- A table of environment variables for the current process. process.env = {} :: { [string]: string } +--- Returns the current user's home directory as a string. function process.homedir(): string error("not implemented") end +--- Returns the current working directory as a string. function process.cwd(): string error("not implemented") end -function process.run(args: string | { string }, options: ProcessRunOptions?): ProcessResult +--- Runs the program given by `args[1]` with the remaining entries as arguments. Returns the process result including exit code and any captured output. +function process.run(args: { string }, options: ProcessRunOptions?): ProcessResult + error("not implemented") +end + +--- Runs `command` through the system shell. Returns the process result. +function process.system(command: string, options: ProcessSystemOptions?): ProcessResult + error("not implemented") +end + +--- Exits the current process with the given `exitcode`. +function process.exit(exitcode: number): never + error("not implemented") +end + +--- Returns the path of the currently running lute executable as a string. +function process.execPath(): string + error("not implemented") +end + +--- Returns the PID of the current process. +function process.pid(): number + error("not implemented") +end + +--- Registers `callback` to be called whenever `signal` is delivered to this process. +--- Suppresses the default OS behavior (e.g. `"SIGINT"` will no longer terminate the process). +--- Returns a handle; call `handle:close()` to deregister. +function process.onSignal(signal: Signal, callback: () -> ()): SignalHandle error("not implemented") end diff --git a/definitions/syntax/cst.luau b/definitions/syntax/cst.luau new file mode 100644 index 000000000..604b96eaf --- /dev/null +++ b/definitions/syntax/cst.luau @@ -0,0 +1,829 @@ +-- lute-lint-global-ignore(unused_variable) +type SpanData = { + read beginLine: number, + read beginColumn: number, + read endLine: number, + read endColumn: number, +} +type SpanMT = { + read __lt: (a: SpanData, b: SpanData) -> boolean, +} +--- A source location range, defined by begin and end line/column positions. +export type Span = setmetatable + +local span = {} + +--- Creates a `Span` from a table with `beginLine`, `beginColumn`, `endLine`, and `endColumn`. +function span.create(tbl: { beginLine: number, beginColumn: number, endLine: number, endColumn: number }): Span + error("not implemented") +end + +--- A whitespace trivia token. +export type Whitespace = { + read tag: "whitespace", + read location: Span, + read text: string, +} + +--- A single-line comment trivia token (`--`). +export type SingleLineComment = { + read tag: "comment", + read location: Span, + read text: string, +} + +--- A multi-line block comment trivia token (`--[[...]]`). +export type MultiLineComment = { + read tag: "blockcomment", + read location: Span, + read text: string, + -- TODO: depth: number, +} + +--- Trivia attached to a token: whitespace or comments. +export type Trivia = Whitespace | SingleLineComment | MultiLineComment + +export type CstToken = { + read leadingTrivia: { Trivia }, + read location: Span, + read text: Kind, + read trailingTrivia: { Trivia }, + read kind: "token", +} + +--- The end-of-file token. +export type CstEof = CstToken<""> & { read tag: "eof" } + +type CstPunctuatedData = { read [number]: T, read separators: { CstToken } } + +-- __len not needed because # already returns number of array entries in table +type CstPunctuatedMT = { + read __iter: (CstPunctuatedData) -> (({ read [number]: T }, number?) -> (number?, T), { T }), +} + +--- A punctuated sequence of items (e.g., function parameters, arguments, type parameters) with separators (e.g., commas). +--- The `__iter` metamethod modifies generalized iteration so that only nodes are iterated over +export type CstPunctuated = setmetatable< + CstPunctuatedData, + CstPunctuatedMT +> + +--- A local variable binding site in the CST. +export type CstLocal = { + read location: Span, + read kind: "local", + read name: CstToken, + read colon: CstToken<":">?, + read annotation: CstType?, + read shadows: CstLocal?, +} + +--- A parenthesized expression node. +export type CstExprGroup = { + read location: Span, + read kind: "expr", + read tag: "group", + read openParens: CstToken<"(">, + read expression: CstExpr, + read closeParens: CstToken<")">, +} + +--- A `nil` literal node. +export type CstExprConstantNil = { + read location: Span, + read kind: "expr", + read tag: "nil", + read token: CstToken<"nil">, +} + +--- A boolean literal node (`true` or `false`). +export type CstExprConstantBool = { + read location: Span, + read kind: "expr", + read tag: "boolean", + read value: boolean, + read token: CstToken<"true" | "false">, +} + +--- A number literal node. +export type CstExprConstantNumber = { + read location: Span, + read kind: "expr", + read tag: "number", + read value: number, + read token: CstToken, +} + +--- An integer literal node. +export type CstExprConstantInteger = { + read location: Span, + read kind: "expr", + read tag: "integer", + read value: number, + read token: CstToken, +} + +--- A string literal node. +export type CstExprConstantString = { + read location: Span, + read kind: "expr", + read tag: "string", + read quoteStyle: "single" | "double" | "block" | "interp", + read blockDepth: number, + read value: CstToken, +} + +--- A reference to a local variable. +export type CstExprLocal = { + read location: Span, + read kind: "expr", + read tag: "local", + read token: CstToken, + read ["local"]: CstLocal, + read upvalue: boolean, +} + +--- A reference to a global variable. +export type CstExprGlobal = { read location: Span, read kind: "expr", read tag: "global", read name: CstToken } + +--- A varargs expression node (`...`). +export type CstExprVarargs = { + read location: Span, + read kind: "expr", + read tag: "vararg", + read token: CstToken<"...">, +} + +--- A function call expression node. +export type CstExprCall = { + read location: Span, + read kind: "expr", + read tag: "call", + read func: CstExpr, + read openParens: CstToken<"(">?, + read arguments: CstPunctuated, + read closeParens: CstToken<")">?, + read self: boolean, + read argLocation: Span, +} + +--- An explicit generic instantiation expression node (e.g., `f<>`). +export type CstExprInstantiate = { + read location: Span, + read kind: "expr", + read tag: "instantiate", + read expr: CstExpr, + read leftArrow1: CstToken<"<">, + read leftArrow2: CstToken<"<">, + read typeArguments: CstPunctuated, + read rightArrow1: CstToken<">">, + read rightArrow2: CstToken<">">, +} + +--- A dot or colon index expression node (e.g., `t.k` or `t:method`). +export type CstExprIndexName = { + read location: Span, + read kind: "expr", + read tag: "indexname", + read expression: CstExpr, + read accessor: CstToken<"." | ":">, + read index: CstToken, + read indexLocation: Span, +} + +--- A bracketed index expression node (e.g., `t[k]`). +export type CstExprIndexExpr = { + read location: Span, + read kind: "expr", + read tag: "index", + read expression: CstExpr, + read openBrackets: CstToken<"[">, + read index: CstExpr, + read closeBrackets: CstToken<"]">, +} + +--- An anonymous function expression node. +export type CstExprFunction = { + read location: Span, + read kind: "expr", + read tag: "function", + read attributes: { CstAttribute }, + read functionKeyword: CstToken<"function">, + read openGenerics: CstToken<"<">?, + read generics: CstPunctuated, + read genericPacks: CstPunctuated, + read closeGenerics: CstToken<">">?, + read openParens: CstToken<"(">, + read self: CstLocal?, + read parameters: CstPunctuated, + read hasVararg: boolean, -- Present so AstExprFunction subtypes structurally against CstExprFunction + read vararg: CstToken<"...">?, + read varargColon: CstToken<":">?, + read varargAnnotation: CstTypePack?, + read closeParens: CstToken<")">, + read returnSpecifier: CstToken<":">?, + read returnAnnotation: CstTypePack?, + read body: CstStatBlock, + read endKeyword: CstToken<"end">, +} + +-- helper types for items contained in an CstExprTable, not actually CstExprs themselves +--- A positional table constructor entry (e.g., `value` in `{ value }`). +export type CstTableExprListItem = { + read location: Span, + read kind: "list", + read value: CstExpr, + read separator: CstToken<"," | ";">?, + read isTableItem: true, +} + +--- A named table constructor entry (e.g., `key = value`). +export type CstTableExprRecordItem = { + read location: Span, + read kind: "record", + read key: CstToken, + read equals: CstToken<"=">, + read value: CstExpr, + read separator: CstToken<"," | ";">?, + read isTableItem: true, +} + +--- A table constructor entry with a computed key (e.g., `[expr] = value`). +export type CstTableExprGeneralItem = { + read location: Span, + read kind: "general", + read indexerOpen: CstToken<"[">, + read key: CstExpr, + read indexerClose: CstToken<"]">, + read equals: CstToken<"=">, + read value: CstExpr, + read separator: CstToken<"," | ";">?, + read isTableItem: true, +} + +--- Union of all table constructor entry types. +export type CstTableExprItem = CstTableExprListItem | CstTableExprRecordItem | CstTableExprGeneralItem + +--- A table constructor expression node. +export type CstExprTable = { + read location: Span, + read kind: "expr", + read tag: "table", + read openBrace: CstToken<"{">, + read entries: { CstTableExprItem }, + read closeBrace: CstToken<"}">, +} + +--- A unary expression node (e.g., `not x`, `-x`, `#x`). +export type CstExprUnary = { + read location: Span, + read kind: "expr", + read tag: "unary", + read operator: CstToken<"not" | "-" | "#">, + read operand: CstExpr, +} + +--- A binary expression node (e.g., `a + b`). +export type CstExprBinary = { + read location: Span, + read kind: "expr", + read tag: "binary", + read lhsOperand: CstExpr, + read operator: CstToken, + read rhsOperand: CstExpr, +} + +--- An interpolated string expression node (e.g., `` `hello {name}` ``). +export type CstExprInterpString = { + read location: Span, + read kind: "expr", + read tag: "interpolatedstring", + read strings: { CstToken }, + read expressions: { CstExpr }, +} + +--- A type assertion expression node (e.g., `x :: T`). +export type CstExprTypeAssertion = { + read location: Span, + read kind: "expr", + read tag: "cast", + read operand: CstExpr, + read operator: CstToken<"::">, + read annotation: CstType, +} + +-- helper type for elseif clauses of an if-else expression, not actually an CstExpr itself +--- An `elseif` clause in an if-else expression. +export type CstElseIfExpr = { + read elseIfKeyword: CstToken<"elseif">, + read condition: CstExpr, + read thenKeyword: CstToken<"then">, + read thenExpr: CstExpr, +} + +--- An if-else expression node. +export type CstExprIfElse = { + read location: Span, + read kind: "expr", + read tag: "conditional", + read ifKeyword: CstToken<"if">, + read condition: CstExpr, + read thenKeyword: CstToken<"then">, + read thenExpr: CstExpr, + read elseifs: { CstElseIfExpr }, + read elseKeyword: CstToken<"else">, + read elseExpr: CstExpr, +} + +--- Union of all expression node types in the Luau CST. +export type CstExpr = + | CstExprGroup + | CstExprConstantNil + | CstExprConstantBool + | CstExprConstantNumber + | CstExprConstantInteger + | CstExprConstantString + | CstExprLocal + | CstExprGlobal + | CstExprVarargs + | CstExprCall + | CstExprInstantiate + | CstExprIndexName + | CstExprIndexExpr + | CstExprFunction + | CstExprTable + | CstExprUnary + | CstExprBinary + | CstExprInterpString + | CstExprTypeAssertion + | CstExprIfElse + +--- A block statement node (a sequence of statements). +export type CstStatBlock = { + read location: Span, + read kind: "stat", + read tag: "block", + read statements: { CstStat }, +} + +--- A `do...end` block statement node. +export type CstStatDo = { + read location: Span, + read kind: "stat", + read tag: "do", + read doKeyword: CstToken<"do">, + read body: CstStatBlock, + read endKeyword: CstToken<"end">, +} + +--- An `elseif` clause in an if statement. +export type CstElseIfStat = { + read elseIfKeyword: CstToken<"elseif">, + read condition: CstExpr, + read thenKeyword: CstToken<"then">, + read thenBlock: CstStatBlock, +} + +--- An `if...then...elseif...else...end` statement node. +export type CstStatIf = { + read location: Span, + read kind: "stat", + read tag: "conditional", + read ifKeyword: CstToken<"if">, + read condition: CstExpr, + read thenKeyword: CstToken<"then">, + read thenBlock: CstStatBlock, + read elseifs: { CstElseIfStat }, + read elseKeyword: CstToken<"else">?, -- TODO: This could be elseif! + read elseBlock: CstStatBlock?, + read endKeyword: CstToken<"end">, +} + +--- A `while...do...end` loop statement node. +export type CstStatWhile = { + read location: Span, + read kind: "stat", + read tag: "while", + read whileKeyword: CstToken<"while">, + read condition: CstExpr, + read doKeyword: CstToken<"do">, + read body: CstStatBlock, + read endKeyword: CstToken<"end">, +} + +--- A `repeat...until` loop statement node. +export type CstStatRepeat = { + read location: Span, + read kind: "stat", + read tag: "repeat", + read repeatKeyword: CstToken<"repeat">, + read body: CstStatBlock, + read untilKeyword: CstToken<"until">, + read condition: CstExpr, +} + +--- A `break` statement node. +export type CstStatBreak = { read location: Span, read kind: "stat", read tag: "break", read token: CstToken<"break"> } + +--- A `continue` statement node. +export type CstStatContinue = { + read location: Span, + read kind: "stat", + read tag: "continue", + read token: CstToken<"continue">, +} + +--- A `return` statement node. +export type CstStatReturn = { + read location: Span, + read kind: "stat", + read tag: "return", + read returnKeyword: CstToken<"return">, + read expressions: CstPunctuated, +} + +--- A statement consisting of a bare expression (e.g., a function call used as a statement). +export type CstStatExpr = { + read location: Span, + read kind: "stat", + read tag: "expression", + read expression: CstExpr, +} + +--- A `local` variable declaration statement node. +export type CstStatLocal = { + read location: Span, + read kind: "stat", + read tag: "local", + read localKeyword: CstToken<"local">, + read variables: CstPunctuated, + read equals: CstToken<"=">?, + read values: CstPunctuated, +} + +--- A `const` variable declaration statement node. +export type CstStatConst = { + read location: Span, + read kind: "stat", + read tag: "const", + read constKeyword: CstToken<"const">, + read variables: CstPunctuated, + read equals: CstToken<"=">?, + read values: CstPunctuated, +} + +--- A numeric `for` loop statement node. +export type CstStatFor = { + read location: Span, + read kind: "stat", + read tag: "for", + read forKeyword: CstToken<"for">, + read variable: CstLocal, + read equals: CstToken<"=">, + read from: CstExpr, + read toComma: CstToken<",">, + read to: CstExpr, + read stepComma: CstToken<",">?, + read step: CstExpr?, + read doKeyword: CstToken<"do">, + read body: CstStatBlock, + read endKeyword: CstToken<"end">, +} + +--- A generic `for...in` loop statement node. +export type CstStatForIn = { + read location: Span, + read kind: "stat", + read tag: "forin", + read forKeyword: CstToken<"for">, + read variables: CstPunctuated, + read inKeyword: CstToken<"in">, + read values: CstPunctuated, + read doKeyword: CstToken<"do">, + read body: CstStatBlock, + read endKeyword: CstToken<"end">, +} + +--- An assignment statement node. +export type CstStatAssign = { + read location: Span, + read kind: "stat", + read tag: "assign", + read variables: CstPunctuated, + read equals: CstToken<"=">, + read values: CstPunctuated, +} + +--- A compound assignment statement node (e.g., `x += 1`). +export type CstStatCompoundAssign = { + read location: Span, + read kind: "stat", + read tag: "compoundassign", + read variable: CstExpr, + read operator: CstToken, -- TODO: Enforce token type + read value: CstExpr, +} + +--- A function attribute node (`@checked`, `@native`, or `@deprecated`). +export type CstAttribute = { + read location: Span, + read kind: "attribute", + read name: CstToken<"@checked" | "@native" | "@deprecated">, +} + +--- A function declaration statement node. +export type CstStatFunction = { + read location: Span, + read kind: "stat", + read tag: "function", + read name: CstExpr, + read func: CstExprFunction, +} + +--- A `local function` declaration statement node. +export type CstStatLocalFunction = { + read location: Span, + read kind: "stat", + read tag: "localfunction", + read localKeyword: CstToken<"local">, + read name: CstLocal, + read func: CstExprFunction, +} + +--- A `type` alias declaration statement node. +export type CstStatTypeAlias = { + read location: Span, + read kind: "stat", + read tag: "typealias", + read isExported: boolean, + read export: CstToken<"export">?, + read typeToken: CstToken<"type">, + read name: CstToken, + read openGenerics: CstToken<"<">?, + read generics: CstPunctuated?, + read genericPacks: CstPunctuated?, + read closeGenerics: CstToken<">">?, + read equals: CstToken<"=">, + read type: CstType, +} + +-- A type function +--- A `type function` declaration statement node. +export type CstStatTypeFunction = { + read location: Span, + read kind: "stat", + read tag: "typefunction", + read isExported: boolean, + read export: CstToken<"export">?, + read type: CstToken<"type">, + read name: CstToken, + read body: CstExprFunction, +} + +--- Union of all statement node types in the Luau CST. +export type CstStat = + | CstStatBlock + | CstStatDo + | CstStatIf + | CstStatWhile + | CstStatRepeat + | CstStatBreak + | CstStatContinue + | CstStatReturn + | CstStatExpr + | CstStatLocal + | CstStatConst + | CstStatFor + | CstStatForIn + | CstStatAssign + | CstStatCompoundAssign + | CstStatFunction + | CstStatLocalFunction + | CstStatTypeAlias + | CstStatTypeFunction + +--- A generic type parameter (e.g., `T` in `function f`). +export type CstGenericType = { + read tag: "generic", + read name: CstToken, + read equals: CstToken<"=">?, + read default: CstType?, +} + +--- A generic type pack parameter (e.g., `T...` in `function f`). +export type CstGenericTypePack = { + read tag: "genericpack", + read name: CstToken, + read ellipsis: CstToken<"...">, + read equals: CstToken<"=">?, + read default: CstTypePack?, +} + +--- A named type reference node (e.g., `string`, `MyType`). +export type CstTypeReference = { + read location: Span, + read kind: "type", + read tag: "reference", + read prefix: CstToken?, + read prefixPoint: CstToken<".">?, + read name: CstToken, + read openParameters: CstToken<"<">?, + read parameters: CstPunctuated?, + read closeParameters: CstToken<">">?, +} + +--- A boolean singleton type node (`true` or `false`). +export type CstTypeSingletonBool = { + read location: Span, + read kind: "type", + read tag: "boolean", + read value: boolean, + read token: CstToken<"true" | "false">, +} + +--- A string singleton type node (e.g., `"hello"`). +export type CstTypeSingletonString = { + read location: Span, + read kind: "type", + read tag: "string", + read quoteStyle: "single" | "double", + read value: CstToken, +} + +--- A `typeof(expr)` type annotation node. +export type CstTypeTypeof = { + read location: Span, + read kind: "type", + read tag: "typeof", + read typeof: CstToken<"typeof">, + read openParens: CstToken<"(">, + read expression: CstExpr, + read closeParens: CstToken<")">, +} + +--- A parenthesized type annotation node. +export type CstTypeGroup = { + read location: Span, + read kind: "type", + read tag: "group", + read openParens: CstToken<"(">, + read type: CstType, + read closeParens: CstToken<")">, +} + +--- An optional type annotation node (e.g., `T?`). +export type CstTypeOptional = + { read location: Span, read kind: "type", read tag: "optional", read token: CstToken<"?"> } + +--- A union type annotation node (e.g., `A | B`). +export type CstTypeUnion = { + read location: Span, + read kind: "type", + read tag: "union", + read leading: CstToken<"|">?, + -- Separator may be nil for CstTypeOptional + read types: CstPunctuated, +} + +--- An intersection type annotation node (e.g., `A & B`). +export type CstTypeIntersection = { + read location: Span, + read kind: "type", + read tag: "intersection", + read leading: CstToken<"&">?, + read types: CstPunctuated, +} + +--- An array type annotation node (e.g., `{ T }`). +export type CstTypeArray = { + read location: Span, + read kind: "type", + read tag: "array", + read openBrace: CstToken<"{">, + read access: CstToken<"read" | "write">?, + read type: CstType, + read closeBrace: CstToken<"}">, +} + +--- A computed-key indexer entry in a table type (e.g., `[K]: V`). +export type CstTableTypeItemIndexer = { + read kind: "indexer", + read access: CstToken<"read" | "write">?, + read indexerOpen: CstToken<"[">, + read key: CstType, + read indexerClose: CstToken<"]">, + read colon: CstToken<":">, + read value: CstType, + read separator: CstToken<"," | ";">?, +} + +--- A named property entry in a table type (e.g., `key: T`). +export type CstTableTypeItemProperty = { + read kind: "property", + read access: CstToken<"read" | "write">?, + read indexerOpen: CstToken<"[">?, + read quoteStyle: ("single" | "double")?, + read key: CstToken, + read indexerClose: CstToken<"]">?, + read colon: CstToken<":">, + read value: CstType, + read separator: CstToken<"," | ";">?, +} + +--- Union of all table type entry kinds. +export type CstTableTypeItem = CstTableTypeItemIndexer | CstTableTypeItemProperty + +--- A table type annotation node. +export type CstTypeTable = { + read location: Span, + read kind: "type", + read tag: "table", + read openBrace: CstToken<"{">, + read entries: { CstTableTypeItem }, + read closeBrace: CstToken<"}">, +} + +--- A parameter in a function type annotation. +export type CstFunctionTypeParameter = { + read name: CstToken?, + read colon: CstToken<":">?, + read type: CstType, +} + +-- A type representing a Luau function +--- A function type annotation node. +export type CstTypeFunction = { + read location: Span, + read kind: "type", + read tag: "function", + read openGenerics: CstToken<"<">?, + read generics: CstPunctuated?, + read genericPacks: CstPunctuated?, + read closeGenerics: CstToken<">">?, + read openParens: CstToken<"(">, + read parameters: CstPunctuated, + read vararg: CstTypePack?, + read closeParens: CstToken<")">, + read returnArrow: CstToken<"->">, + read returnTypes: CstTypePack, +} + +--- Union of all type annotation node types in the Luau CST. +export type CstType = + | CstTypeReference + | CstTypeSingletonBool + | CstTypeSingletonString + | CstTypeTypeof + | CstTypeGroup + | CstTypeUnion + | CstTypeIntersection + | CstTypeOptional + | CstTypeArray + | CstTypeTable + | CstTypeFunction + +--- An explicit type pack annotation node (e.g., `(T, U)`). +export type CstTypePackExplicit = { + read location: Span, + read kind: "typepack", + read tag: "explicit", + read openParens: CstToken<"(">?, + read types: CstPunctuated, + read tailType: CstTypePack?, + read closeParens: CstToken<")">?, +} + +--- A generic type pack annotation node (e.g., `T...`). +export type CstTypePackGeneric = { + read location: Span, + read kind: "typepack", + read tag: "generic", + read name: CstToken, + read ellipsis: CstToken<"...">, +} + +--- A variadic type pack annotation node (e.g., `...T`). +export type CstTypePackVariadic = { + read location: Span, + read kind: "typepack", + read tag: "variadic", + --- May be nil when present as the vararg annotation in a function body + read ellipsis: CstToken<"...">?, + read type: CstType, +} + +--- Union of all type pack annotation node types in the Luau CST. +export type CstTypePack = CstTypePackExplicit | CstTypePackGeneric | CstTypePackVariadic + +--- Union of all node types in the Luau CST. +export type CstNode = CstExpr | CstStat | CstType | CstTypePack | CstLocal | CstAttribute | CstToken + +--- The result of parsing a Luau script, containing the root block, EOF token, and source location info. +export type CstParseResult = { + read root: CstStatBlock, + read eof: CstEof, + read lines: number, + read lineOffsets: { number }, +} + +return { span = table.freeze(span) } diff --git a/definitions/syntax/parser.luau b/definitions/syntax/parser.luau new file mode 100644 index 000000000..983ab101c --- /dev/null +++ b/definitions/syntax/parser.luau @@ -0,0 +1,16 @@ +-- lute-lint-global-ignore(unused_variable) +local cst = require("./cst") + +local parser = {} + +--- Parses `source` as a Luau script and returns the resulting CST. +function parser.parse(source: string): cst.CstParseResult + error("not implemented") +end + +--- Parses `source` as a single Luau expression and returns the resulting CST node. +function parser.parseExpr(source: string): cst.CstExpr + error("not implemented") +end + +return parser diff --git a/definitions/system.luau b/definitions/system.luau index b51285996..23dd1094e 100644 --- a/definitions/system.luau +++ b/definitions/system.luau @@ -1,3 +1,4 @@ +--- CPU information for a single logical processor, including model, speed, and time-usage breakdown. export type CpuInfo = { model: string, speed: number, @@ -13,31 +14,44 @@ export type CpuInfo = { local system = {} -function system.threadcount(): number +--- Returns the number of logical CPU threads available on the current machine. +function system.threadCount(): number error("unimplemented") end -function system.hostname(): string +--- Returns the hostname of the current machine. +function system.hostName(): string error("unimplemented") end -function system.totalmemory(): number +--- Returns the path to the system's temporary directory. +function system.tmpdir(): string error("unimplemented") end -function system.freememory(): number +--- Returns the total amount of system memory in bytes. +function system.totalMemory(): number error("unimplemented") end +--- Returns the amount of free (available) system memory in bytes. +function system.freeMemory(): number + error("unimplemented") +end + +--- Returns the system uptime in seconds. function system.uptime(): number error("unimplemented") end +--- Returns CPU information for each logical processor on the current machine. function system.cpus(): { CpuInfo } error("unimplemented") end +--- The current operating system, e.g. `"linux"`, `"macos"`, `"windows"`. system.os = "" :: string +--- The CPU architecture of the current machine, e.g. `"x64"`, `"arm64"`. system.arch = "" :: string return system diff --git a/definitions/task.luau b/definitions/task.luau index 5b280aea2..ecb08a0a6 100644 --- a/definitions/task.luau +++ b/definitions/task.luau @@ -1,20 +1,35 @@ +-- lute-lint-global-ignore(unused_variable) local time = require("./time") local task = {} -function task.defer() +--- Schedules `routine` to run immediately on the next resumption cycle, passing any additional arguments to it. Returns the thread. +function task.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread error("unimplemented") end -function task.wait(dur: (number | time.Duration)?) +--- Schedules `routine` to run after the current thread yields, passing any additional arguments to it. Returns the thread. +function task.defer(routine: ((T...) -> U...) | thread, ...: T...): thread error("unimplemented") end -function task.spawn(routine: ((T...) -> U...) | thread, ...: T...) +--- Resumes `thread`, running it until it yields or completes. Returns the thread. +function task.resume(thread: thread): thread error("unimplemented") end -function task.resume(thread: thread) +--- Yields the current thread and re-schedules it for the next resumption cycle. +function task.deferSelf() + error("unimplemented") +end + +--- Yields the current thread for `dur` seconds (or until the next cycle if `dur` is omitted). Returns the actual time waited. +function task.wait(dur: (number | time.Duration)?): number + error("unimplemented") +end + +--- Schedules `routine` to run after `dur` seconds, passing any additional arguments to it. Returns the thread. +function task.delay(dur: number | time.Duration, routine: thread | ((T...) -> U...), ...: T...): thread error("unimplemented") end diff --git a/definitions/time.luau b/definitions/time.luau index 163a35061..2b011c9c3 100644 --- a/definitions/time.luau +++ b/definitions/time.luau @@ -1,46 +1,62 @@ +-- lute-lint-global-ignore(unused_variable) local time = {} local duration_ops = {} local instant = {} +--- Returns the current time as an `Instant`. function time.now(): Instant error("not implemented") end +--- Returns the number of seconds elapsed since `instant`. function time.since(instant: Instant): number error("not implemented") end time.duration = {} +--- Creates a `Duration` from `seconds` and a fractional `subsecnanos` nanosecond component. +function time.duration.create(seconds: number, subsecnanos: number): Duration + error("not implemented") +end + +--- Creates a `Duration` from a number of nanoseconds. function time.duration.nanoseconds(nanoseconds: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of microseconds. function time.duration.microseconds(microseconds: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of milliseconds. function time.duration.milliseconds(milliseconds: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of seconds. function time.duration.seconds(seconds: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of minutes. function time.duration.minutes(minutes: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of hours. function time.duration.hours(hours: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of days. function time.duration.days(days: number): Duration error("not implemented") end +--- Creates a `Duration` from a number of weeks. function time.duration.weeks(weeks: number): Duration error("not implemented") end @@ -49,35 +65,35 @@ function instant.elapsed(self: Instant): number error("not implemented") end -function duration_ops.tonanoseconds(self: Duration): number +function duration_ops.toNanoseconds(self: Duration): number error("not implemented") end -function duration_ops.tomicroseconds(self: Duration): number +function duration_ops.toMicroseconds(self: Duration): number error("not implemented") end -function duration_ops.tomilliseconds(self: Duration): number +function duration_ops.toMilliseconds(self: Duration): number error("not implemented") end -function duration_ops.toseconds(self: Duration): number +function duration_ops.toSeconds(self: Duration): number error("not implemented") end -function duration_ops.tominutes(self: Duration): number +function duration_ops.toMinutes(self: Duration): number error("not implemented") end -function duration_ops.tohours(self: Duration): number +function duration_ops.toHours(self: Duration): number error("not implemented") end -function duration_ops.todays(self: Duration): number +function duration_ops.toDays(self: Duration): number error("not implemented") end -function duration_ops.toweeks(self: Duration): number +function duration_ops.toWeeks(self: Duration): number error("not implemented") end @@ -94,20 +110,27 @@ function duration_ops.subsecmillis(self: Duration): number end type Frozen = typeof(table.freeze({})) +--- A time duration with nanosecond precision. Supports arithmetic via `+`, `-`, `*`, `/`, and comparison operators. export type Duration = setmetatable Duration, read __sub: (Duration, Duration) -> Duration, + read __mul: (Duration, number) -> Duration, + read __div: (Duration, number) -> Duration, read __eq: (Duration, Duration) -> boolean, read __lt: (Duration, Duration) -> boolean, read __le: (Duration, Duration) -> boolean, }> +--- A point in time with nanosecond precision. Can be subtracted from another `Instant` to produce a `Duration`. export type Instant = setmetatable Duration, + read __eq: (Instant, Instant) -> boolean, + read __lt: (Instant, Instant) -> boolean, + read __le: (Instant, Instant) -> boolean, }> return time diff --git a/definitions/vm.luau b/definitions/vm.luau index 4132d748b..066c5405f 100644 --- a/definitions/vm.luau +++ b/definitions/vm.luau @@ -1,5 +1,7 @@ +-- lute-lint-global-ignore(unused_variable) local vm = {} +--- Creates a new Luau VM from the module at `path` and returns its exported table. function vm.create(path: string): { [any]: any } error("not implemented") end diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..10843c858 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,22 @@ +# Lute Docker Sources + +Source Dockerfiles for the official Lute container images. The images are built and published to `ghcr.io/luau-lang/lute` by the `release` workflow on every stable (non-nightly) release. + +See the [Docker guide](../docs/guide/docker.md) for the list of image variants, tags, and usage examples. + +## Building locally + +The Dockerfiles fetch the `lute` binary from the GitHub Releases page for the requested version, so any released version can be built locally: + +```bash +docker buildx build \ + --build-arg LUTE_VERSION=1.0.0 \ + -f docker/bin.dockerfile \ + -t lute:bin docker + +docker buildx build \ + --build-arg LUTE_VERSION=1.0.0 \ + --build-arg BIN_IMAGE=lute:bin \ + -f docker/debian.dockerfile \ + -t lute:debian docker +``` diff --git a/docker/bin.dockerfile b/docker/bin.dockerfile new file mode 100644 index 000000000..af508b18c --- /dev/null +++ b/docker/bin.dockerfile @@ -0,0 +1,21 @@ +ARG LUTE_VERSION + +FROM buildpack-deps:curl AS download + +RUN apt-get update && apt-get install -y --no-install-recommends unzip \ + && rm -rf /var/lib/apt/lists/* + +ARG LUTE_VERSION +ARG TARGETARCH + +RUN ARCH=$(echo "$TARGETARCH" | sed -e 's/arm64/aarch64/' -e 's/amd64/x86_64/') \ + && curl -fsSL "https://github.com/luau-lang/lute/releases/download/v${LUTE_VERSION}/lute-linux-${ARCH}.zip" --output lute.zip \ + && unzip lute.zip \ + && chmod +x lute + +FROM scratch + +ARG LUTE_VERSION +ENV LUTE_VERSION=${LUTE_VERSION} + +COPY --from=download /lute /lute diff --git a/docker/debian.dockerfile b/docker/debian.dockerfile new file mode 100644 index 000000000..aa9db7bff --- /dev/null +++ b/docker/debian.dockerfile @@ -0,0 +1,20 @@ +ARG LUTE_VERSION +ARG BIN_IMAGE=ghcr.io/luau-lang/lute:bin-${LUTE_VERSION} + +FROM ${BIN_IMAGE} AS bin + +FROM debian:stable-slim + +ARG LUTE_VERSION +ENV LUTE_VERSION=${LUTE_VERSION} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=bin /lute /usr/local/bin/lute + +COPY docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] + +CMD ["--help"] diff --git a/docker/distroless.dockerfile b/docker/distroless.dockerfile new file mode 100644 index 000000000..dfc6ceb7a --- /dev/null +++ b/docker/distroless.dockerfile @@ -0,0 +1,15 @@ +ARG LUTE_VERSION +ARG BIN_IMAGE=ghcr.io/luau-lang/lute:bin-${LUTE_VERSION} + +FROM ${BIN_IMAGE} AS bin + +FROM gcr.io/distroless/cc-debian13 + +ARG LUTE_VERSION +ENV LUTE_VERSION=${LUTE_VERSION} + +COPY --from=bin /lute /usr/local/bin/lute + +ENTRYPOINT ["lute"] + +CMD ["--help"] diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 000000000..c545f01e4 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# If the first argument starts with `-` or is not a system command, prepend +# `lute` so users can run e.g. `docker run lute run script.luau` directly. +if [ "$1" != "${1#-}" ] || [ -z "$(command -v "${1}")" ]; then + set -- lute "$@" +fi + +exec "$@" diff --git a/docs/.gitignore b/docs/.gitignore index f035fb0c1..50a12445e 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,3 +2,10 @@ node_modules .vitepress/cache dist reference +docs/ + +# Auto-generated API docs (keep root index.md as table of contents) +lute/* +!lute/index.md +std/* +!std/index.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c00548994..93438bd4f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,44 +1,32 @@ 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: 'Standard Library', link: '/std' }, + ], + 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, + sortMenusByFrontmatterOrder: true, + frontmatterOrderDefaultValue: 100, + excludeByGlobPattern: ['README.md'], } -}) +) diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 000000000..f97b690c9 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,20 @@ +import DefaultTheme from 'vitepress/theme' +import { redirects } from './redirects' +import './style.css' + +export default { + ...DefaultTheme, + enhanceApp({ router }: { router: any }) { + router.onBeforeRouteChange = (to: string) => { + const path = to.replace(/\.html$/i, ''), + toPath = redirects[path] + + if (toPath) { + setTimeout(() => { router.go(toPath) }) + return false + } else { + return true + } + } + }, +} diff --git a/docs/.vitepress/theme/redirects.ts b/docs/.vitepress/theme/redirects.ts new file mode 100644 index 000000000..067bf6c9b --- /dev/null +++ b/docs/.vitepress/theme/redirects.ts @@ -0,0 +1,67 @@ +export const redirects: Record = { + '/reference/': '/std/', + '/reference/index': '/std/', + // lute runtime redirects + '/reference/lute/': '/lute/', + '/reference/lute/index': '/lute/', + '/reference/lute/crypto': '/lute/crypto', + '/reference/lute/fs': '/lute/fs', + '/reference/lute/io': '/lute/io', + '/reference/lute/luau': '/lute/luau', + '/reference/lute/net': '/lute/net/', + '/reference/lute/net/': '/lute/net/', + '/reference/lute/net/index': '/lute/net/', + '/reference/lute/net/client': '/lute/net/client', + '/reference/lute/net/server': '/lute/net/server', + '/reference/lute/process': '/lute/process', + '/reference/lute/system': '/lute/system', + '/reference/lute/task': '/lute/task', + '/reference/lute/time': '/lute/time', + '/reference/lute/vm': '/lute/vm', + // std library redirects + '/reference/std/': '/std/', + '/reference/std/index': '/std/', + '/reference/std/fs': '/std/fs', + '/reference/std/io': '/std/io', + '/reference/std/json': '/std/json', + '/reference/std/luau': '/std/luau', + '/reference/std/net': '/std/net', + '/reference/std/path/': '/std/path/', + '/reference/std/path/index': '/std/path/', + '/reference/std/path/pathinterface': '/std/path/pathinterface', + '/reference/std/path/posix/': '/std/path/posix/', + '/reference/std/path/posix/index': '/std/path/posix/', + '/reference/std/path/posix/types': '/std/path/posix/types', + '/reference/std/path/types': '/std/path/types', + '/reference/std/path/win32/': '/std/path/win32/', + '/reference/std/path/win32/index': '/std/path/win32/', + '/reference/std/path/win32/types': '/std/path/win32/types', + '/reference/std/process': '/std/process', + '/reference/std/stringext': '/std/stringext', + '/reference/std/syntax/': '/std/syntax/', + '/reference/std/syntax/index': '/std/syntax/', + '/reference/std/syntax/parser': '/std/syntax/parser', + '/reference/std/syntax/printer': '/std/syntax/printer', + '/reference/std/syntax/query': '/std/syntax/query', + '/reference/std/syntax/types': '/std/syntax/types', + '/reference/std/syntax/utils/': '/std/syntax/utils/', + '/reference/std/syntax/utils/index': '/std/syntax/utils/', + '/reference/std/syntax/utils/trivia': '/std/syntax/utils/trivia', + '/reference/std/syntax/visitor': '/std/syntax/visitor', + '/reference/std/system/': '/std/system/', + '/reference/std/system/index': '/std/system/', + '/reference/std/system/platform': '/std/system/platform', + '/reference/std/tableext': '/std/tableext', + '/reference/std/task': '/std/task', + '/reference/std/test/': '/std/test/', + '/reference/std/test/index': '/std/test/', + '/reference/std/test/assert': '/std/test/assert', + '/reference/std/test/env': '/std/test/env', + '/reference/std/test/failure': '/std/test/failure', + '/reference/std/test/reporter': '/std/test/reporter', + '/reference/std/test/runner': '/std/test/runner', + '/reference/std/test/types': '/std/test/types', + '/reference/std/time/': '/std/time/', + '/reference/std/time/index': '/std/time/', + '/reference/std/time/duration': '/std/time/duration', +} diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 000000000..42b77cb0e --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,6 @@ +:root { + --vp-c-brand-1: #2E65B0; + --vp-c-brand-2: #3574C4; + --vp-c-brand-3: #2E65B0; + --vp-c-brand-soft: rgba(46, 101, 176, 0.14); +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..84597225a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Editing Lute Documentation + +Lute's docs are generated from Markdown files using [VitePress](https://vitepress.dev/). +To run the docsite locally, you should have Node.js and NPM installed. +Then, use the following to start running it locally: + +1. `cd docs` +2. `npm install` +3. `npm run dev` + +The terminal output should then tell you which `localhost` port the docsite is running at. + +Note: Running `npm run dev` requires that you have `lute` on your `$PATH`. +If you don't, (or have `lute` set as an alias in your shell), you can run ` doc -m docgen.luau && vitepress dev` directly instead. + +## Regenerating documentation files + +Run `npm run gen` from the `docs` folder to regenerate the reference content in `docs/std` and `docs/lute`. This uses `docgen.luau`, which maps `../lute/std/libs` to `docs/std` and `../definitions` to `docs/lute`. 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..55e6597e9 --- /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/index.md) | 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/almost_swapped.md b/docs/cli/lint/almost_swapped.md new file mode 100644 index 000000000..560727583 --- /dev/null +++ b/docs/cli/lint/almost_swapped.md @@ -0,0 +1,25 @@ +# almost_swapped + +This lint rule checks for instances of attempted swaps (i.e. `a = b; b = a`). + +## Why this is discouraged + +This will never result in the expected behavior. +In the example above, the value of `b` will be assigned to `a`. +Then, this same value gets assigned back to `b` (a no-op), resulting in both identifiers being bound the original value of `b`. +You should instead use Luau's multiple assigment syntax, which will swap as expected (i.e. `a, b = b, a`). + +## Example violations + +`almost_swapped` will warn on the following: + +```luau +a = b +b = a +``` + +You should instead do: + +```luau +a, b = b, a +``` diff --git a/docs/cli/lint/constant_table_comparison.md b/docs/cli/lint/constant_table_comparison.md new file mode 100644 index 000000000..29725dae2 --- /dev/null +++ b/docs/cli/lint/constant_table_comparison.md @@ -0,0 +1,36 @@ +--- +title: constant_table_comparison +--- +# constant_table_comparison + +This lint rule checks for instances of attempting to compare to table literals. + +## Why this is discouraged + +This will never result in the expected behavior. +In Luau, tables are stored and compared by *reference*, rather than by value. +This means that when you compare two variables containing tables, Luau will compare whether the two variables point to the same table in memory, rather than recursively comparing their keys and values. +When you compare to a table literal (i.e. `if x == { a = 3 } then ... end`), Luau will create a new table with a single entry (`a = 3`) in memory and check whether `x` references that table. +Since the table is newly created (and there wasn't even the chance to reassign `x` to that table) this comparison will always evaluate to `false`. + +## Example violations + +`constant_table_comparison` will warn on the following: + +```luau +if x == {} then + ... +elseif x ~= {} then + ... +end +``` + +You should instead do: + +```luau +if next(x) == nil then + ... +elseif next(x) ~= nil then + ... +end +``` diff --git a/docs/cli/lint/divide_by_zero.md b/docs/cli/lint/divide_by_zero.md new file mode 100644 index 000000000..a5466f671 --- /dev/null +++ b/docs/cli/lint/divide_by_zero.md @@ -0,0 +1,31 @@ +--- +title: divide_by_zero +--- +# divide_by_zero + +This lint rule checks for instances of divison, floor division, or taking the remainder with respect to the literal value 0. + +## Why this is discouraged + +Division and floor division by 0 will generally give `inf` or `-inf` (unless the dividend is 0). +The direct use of `math.huge` or `-math.huge` instead is encouraged. +Taking remainder with respect to 0 (i.e. `3 % 0`) will give `NaN`. +We instead encourage the use of `math.nan`. + +## Example violations + +`divide_by_zero` will warn on the following: + +```luau +local x = 3 / 0 +local y = -4 // 0 +local z = 24 % 0 +``` + +You should instead do: + +```luau +local x = math.huge +local y = -math.huge +local z = math.nan +``` diff --git a/docs/cli/lint/duplicate_keys.md b/docs/cli/lint/duplicate_keys.md new file mode 100644 index 000000000..6ffdbd1ac --- /dev/null +++ b/docs/cli/lint/duplicate_keys.md @@ -0,0 +1,22 @@ +# duplicate_keys + +This lint rule checks for uses of duplicate keys in table literals. +This includes map-like entries that may collide with array-like entries. + +## Why this is discouraged + +This pattern is most likely the result of a mistake, and the final table value will only use one of the values assigned to a duplicate key. + +## Example violations + +`duplicate_keys` will warn on the following: + +```luau +local t = { + key = 1, + ["key"] = 2, + "array-like-value1", + "array-like-value2", + [2] = "map-like-value" +} +``` diff --git a/docs/cli/lint/empty_if_block.md b/docs/cli/lint/empty_if_block.md new file mode 100644 index 000000000..4b46d8e80 --- /dev/null +++ b/docs/cli/lint/empty_if_block.md @@ -0,0 +1,65 @@ +--- +title: empty_if_block +--- +# empty_if_block + +This lint rule checks for empty blocks in `if`, `elseif`, and `else` statements. + +## Why this is discouraged + +Empty conditionals are indicative of incomplete or poorly written code and detract from readability. + +## Options + +This rule accepts the following configuration options: + +- `comments_count` (boolean, default: `false`): When set to `true`, blocks that contain only comments are not considered empty and will not trigger a warning. + +## Example violations + +`empty_if_block` will warn on the following: + +```luau +if condition then +end + +if condition then + -- Empty then block +elseif otherCondition then + doSomething() +end + +if condition then + doSomething() +else + -- Empty else block +end +``` + +## Configuration + +To treat blocks with only comments as non-empty, configure the rule with `comments_count: true`: + +```luau +-- This will not trigger a warning when comments_count is true +if condition then + -- TODO: implement this later +end +``` + +### Sample `.config.luau` file +```luau +return { + lute = { + lint = { + rules = { + empty_if_block = { + options = { + comments_count = true, + }, + }, + } + } + }, +} +``` diff --git a/docs/cli/lint/global_function_in_scope.md b/docs/cli/lint/global_function_in_scope.md new file mode 100644 index 000000000..5016b5f0d --- /dev/null +++ b/docs/cli/lint/global_function_in_scope.md @@ -0,0 +1,46 @@ +--- +title: global_function_in_scope +--- +# global_function_in_scope + +This lint rule warns when a non-local `function` declaration appears inside a nested scope (e.g. inside an `if`, `for`, `while`, `do`, or another function body) rather than at the top level of the file. + +## Why this is discouraged + +A `function foo() end` declaration without the `local` keyword inside a nested scope implicitly creates or assigns to a global variable. This is almost always unintentional and can lead to: + +- Accidental pollution of the global namespace, where the function leaks out of the scope in which its being defined. +- Confusing behavior, because the assignment only happens when the enclosing scope executes. +- Harder-to-debug shadowing issues when another scope references the same global name. + +If the function is only needed locally, use `local function` instead. If it genuinely needs to be a top-level declaration, move it out of the nested scope. + +## Example violations + +`global_function_in_scope` will warn on the following: + +```luau +if true then + function foo() + end +end + +local function outer() + function inner() + end +end +``` + +You should instead do: + +```luau +if true then + local function foo() + end +end + +local function outer() + local function inner() + end +end +``` diff --git a/docs/cli/lint/index.md b/docs/cli/lint/index.md new file mode 100755 index 000000000..390ad85d8 --- /dev/null +++ b/docs/cli/lint/index.md @@ -0,0 +1,98 @@ +# 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. +You can find the full suite of `lute lint`'s default rules documented as sub-pages of this page and their source code [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. + +### `-c, --config [CONFIG]` + +Path to file containing lute lint configuration table. If unspecified, lute lint will look for a `.config.luau` file in the working directory from which it is invoked. + +Configuration expects the following structure: +```luau +type RuleName = string + +type Config = { + lute: { + lint: { + -- Array of globs (.gitignore style) to EXEMPT from linting + ignores: { string }?, + -- Table of string:unknown pairs; globals are passed down to each rule + globals: { [string]: unknown }?, + -- Array of paths from which to load local lint rules (similar to -r CLI option) + rulepaths: { [string] }?, + -- Specify per-rule options and overrides + ruleconfigs: { + [RuleName]: { + -- Array of globs (.gitignore style) that are EXEMPT from this rule + ignores: { string }?, + -- Override a rule's default severity + severity: ("warning" | "error" | "info" | "hint")?, + -- Pass custom options through to a rule; + -- see specific rule's implementation / docs for expected options structure + options: { [string]: unknown }?, + -- Disable a rule + off: boolean?, + }, + }?, + }?, + }?, +} +``` + +### `-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. + +### `--sequential` + +Lint files sequentially instead of in parallel. + +### `--auto-fix` + +Automatically apply fixes for lint violations that provide a suggested fix. Assumes that suggested fixes do not overlap. Does nothing when linting string input. + +### `--no-default-lints` + +Disables the default lint rules. + +## 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/lint/no_any.md b/docs/cli/lint/no_any.md new file mode 100644 index 000000000..eab9552c6 --- /dev/null +++ b/docs/cli/lint/no_any.md @@ -0,0 +1,36 @@ +--- +title: no_any +--- +# no_any + +This lint rule checks for explicit uses of the `any` type annotation. + +## Why this is discouraged + +The `any` type disables type checking entirely for any value it touches, allowing unsafe operations without compiler errors. +This defeats the purpose of Luau's type system and can mask bugs that would otherwise be caught at analysis time. +Use `unknown` instead, which is type-safe: it requires you to narrow the type (e.g. via `typeof` checks) before performing operations on the value. +As an escape hatch, you may perform an unsound cast (`value :: T`), which both narrows the type and explicitly annotates the assumed runtime type contract. + +## Example violations + +`no_any` will warn on the following: + +```luau +local x: any = getValue() +local function process(input: any): any + return input.field +end +type Callback = (any) -> any +``` + +You should instead do: + +```luau +local x: unknown = getValue() +local function process(input: unknown): unknown + assert(typeof(input) == "table") + return (input :: { field: unknown }).field +end +type Callback = (unknown) -> unknown +``` diff --git a/docs/cli/lint/parenthesized_conditions.md b/docs/cli/lint/parenthesized_conditions.md new file mode 100644 index 000000000..7dd0ceeff --- /dev/null +++ b/docs/cli/lint/parenthesized_conditions.md @@ -0,0 +1,25 @@ +# parenthesized_conditions + +This lint rule checks for instances where the conditions of syntactic structures like if statements, while loops, and repeat loops have parenthesized conditions. + +## Why this is discouraged + +Although there is no semantic difference, the parentheses are unidiomatic and may detract from readability. + +## Example violations + +`parenthesized_conditions` will warn on the following: + +```luau +if (x > 5) then + ... +end +``` + +You should instead do: + +```luau +if x > 5 then + ... +end +``` diff --git a/docs/cli/lint/reassigned_parameter.md b/docs/cli/lint/reassigned_parameter.md new file mode 100755 index 000000000..774aa6455 --- /dev/null +++ b/docs/cli/lint/reassigned_parameter.md @@ -0,0 +1,18 @@ +# reassigned_parameter + +This lint rule checks for reassignment of function parameters. + +## Why this is discouraged + +A reassigned parameter may lead to hard-to-spot bugs where code downstream of the assignment does not expect the parameter to have changed. + +## Example violations + +`reassigned_parameter` will warn on the following: + +```luau +local function _(x) + x = 2 + x += 3 +end +``` \ No newline at end of file diff --git a/docs/cli/lint/unused_variable.md b/docs/cli/lint/unused_variable.md new file mode 100644 index 000000000..fd0efc0cd --- /dev/null +++ b/docs/cli/lint/unused_variable.md @@ -0,0 +1,34 @@ +# unused_variable + +This lint rule checks for instances of unused locals. +Unlike Luau's built-in linter, this also flags instances of unused function parameters and loop variables. +To silence instances of deliberately unused variables, lint warnings can be silenced by prefixing the variable name with `_`. + +## Why this is discouraged + +Unused variables can result in less readable code in the best case, and worse performance in the worst case. + +## Example violations + +`unused_variable` will warn on the following: + +```luau +local function _(x) + return nil +end +``` + +You should instead consider: + +```luau +local function _() + return nil +end +``` + +Or simply silence the warning: +```luau +local function _(_x) + return nil +end +``` \ No newline at end of file diff --git a/docs/cli/run.md b/docs/cli/run.md new file mode 100755 index 000000000..4be89933b --- /dev/null +++ b/docs/cli/run.md @@ -0,0 +1,45 @@ +# run + +Run a Luau script. Can be omitted if you just pass the name of the script +to Lute. `lute run` also comes with a sampling profiler, that outputs json traces that can be +viewed at ui.perfetto.dev. The profiler currently only works with single threaded code. + +## Usage + +```bash +lute run [args...] +``` +is equivalent to: + +```bash +lute [args...] +``` + +```bash +lute --profile [--profile-output somefile] [--frequency ] [args...] +``` + +will profile the execution of `script.luau` at the provided frequency, outputting the trace to a file called +somefile. + + +## Options + +### `-h, --help` + +Display this usage message. + +### `--profile` + +Enable profiling for the script being passed to `lute run`. Default sampling frequency is +10000Hz, and default output filename is `_.json`. + +### `--profile-output ` + +Sets the desired filename for the profile trace dump; + +### `--frequency ` + +Sets the frequency at which the profiler will sample the callstack. + + 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..40570aad6 --- /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: ./) + +## 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/docgen.luau b/docs/docgen.luau new file mode 100644 index 000000000..3198d411e --- /dev/null +++ b/docs/docgen.luau @@ -0,0 +1,4 @@ +return { + ["../lute/std/libs"] = { alias = "std", destination = "std" }, + ["../definitions"] = { alias = "lute", destination = "lute" }, +} diff --git a/docs/guide/code-transforms.md b/docs/guide/code-transforms.md new file mode 100644 index 000000000..fa438d75f --- /dev/null +++ b/docs/guide/code-transforms.md @@ -0,0 +1,176 @@ +--- +order: 6 +--- + +# Code Transformations +One of the coolest parts of Lute is that we've exposed many parts of the Luau programming language like types, require-resolution, the Luau concrete syntax tree (CST), and Luau bytecode. +We've used these APIs to build `lute transform`: A tool to write automated and deterministic code transformations. +This means that you can now use Lute libraries to programmatically describe how to change Luau source code and apply these changes using `lute transform`. +In the following sections, we'll walk through writing a simple code transformation which doubles any numbers in the source program (we assume some basic level of familiarity with what an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) is; a CST is just an AST with some extra information). +As in the previous library, we'll be creating a new directory, with one file - `transform.luau`. Make sure to run `lute setup` in this directory! + +## Defining a code transformation + +At the most basic level, a code transformation is a function that accepts a Context, as defined below, and returns a table of replacements. This table should map from `CstNode`'s in the original CST, to `string`'s they should be replaced with. + +```luau +export type Context = { + path: string, + source: string, + parseresult: syntax.ParseResult, + options: Options, +} +``` + +Note that CST's are immutable - rather than mutating them in place, we specify the changes we would like to make using a table of replacements and `lute transform` applies them for us. + +:::info +The returned table of replacements should have type `{ [CstNode] : string }`, ie map `CstNode`'s to the strings we would like to replace them with. +For now, we only support string replacements since manually constructing `CstNode` tables is more error prone. +If you still want to construct replacement `CstNode`'s by hand, you can use the `@std/printer` library to serialize them before inserting them into your replacement map. +::: + +Lute provides two ways of writing transforms: A declarative, query based approach, and a visitor based approach. +Most transformations can be written with the query approach. We'll start with that and discuss a visitor approach later. + +### Query Based CST transforms + +In this section, we build up an example of a query based implementation which doubles any numbers found in the source program. +We begin by selecting every CST node we're interested in - in this case, every number: + +```luau +query.findAllFromRoot(cstRoot, utils.isExprConstantNumber) +``` + +`findAllFromRoot` is a general entry point function into queries which filters all the nodes which match a certain condition. We specify this condition using a function which takes in an `CstNode` and returns either an `CstNode` or `nil`. In this case, `utils.isExprConstantNumber` has the following definition: + +```luau +function utils.isExprConstantNumber(n: types.CstNode): types.CstExprConstantNumber? + return if n.kind == "expr" and n.tag == "number" then n else nil +end +``` + +All `CstNode` objects have a `kind` field which denotes the broader category that the node falls into and a `tag` field which specifies the exact node type. So in this case, the `query.findAllFromRoot` call above will return all nodes which are number literals in our source CST. + +Since we want to double all of these numbers, we extend our program to construct a table which maps all the number literals we've just collected to a string that each one should be replaced with: + +```luau +local replacements = query.findAllFromRoot(cstRoot, utils.isExprConstantNumber) + :replace(function(numLiteral: CstExprConstantNumber) + return `{ numLiteral.value * 2 }` + end) +``` + +The manual work of collecting these replacements into a single table is handled by the original query's `:replace` method. All we need to do is pass a function which specifies how an CST node should be transformed into the string which will replace it. + +To get this code into a format that `lute transform` can understand and run, we return the replacements from a function which takes the `Context` mentioned above, and create a file which returns that function. Here's what that looks like with the relevant imports included: + +```luau +local cst = require("@std/syntax") +local query = require("@std/syntax/query") +local utils = require("@std/syntax/utils") + +local function transformQuery(ctx) + return query.findAllFromRoot(ctx.parseresult, utils.isExprConstantNumber) + :replace(function(numLiteral: cst.CstExprConstantNumber) + return `{ numLiteral.value * 2 }` + end) +end + +return transformQuery +``` + +We now have a completed code transformation! + +Next, we paste the above into `transform.luau`, and create another file, `subject.luau` containing just `local x = 2`. + +If we now run: +```bash +lute transform transform.luau subject.luau +``` +and open `subject.luau`, it should now contain `local x = 4` instead. + +### Visitor Based CST tranforms + +In this section, we build up another code transformation which doubles all numbers. This example will use the [CST visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern). + +We begin by instantiating a new visitor and a table to hold our replacements: + +```luau +local myVisitor = visitor.create() +local replacements : { [cst.CstNode] : string } = {} +``` + +By default, visitors will "visit" every node of an CST. If we want to customize their behavior for certain CST node types, we do it by overloading the method for that node type: + +```luau +local myVisitor = visitor.create() +local replacements : { [cst.CstNode] : string } = {} + +function myVisitor.visitExprConstantNumber(numberLiteral: cst.CstExprConstantNumber) + -- do something + return false +end +``` + +Although it may seem innocuous, the value returned from a visitor method is quite important: it tells the visitor whether it should recurse into the subnodes of the node being visited (by default the visitor will recurse into every subnode). So the following would cause the visitor to skip the conditions and branch bodies of all if statements in the visited CST: + +```luau +local anotherVisitor = visitor.create() + +function anotherVisitor.visitStatIf(ifStatement: cst.CstStatIf) + return false +end +``` + +Going back to our code transformation, we would like our visitor to add a new replacement to our collection of replacements each time it visits a new number literal. Additionally, in the Lute CST API, number literals have a single subnode: the token containing the number in the program text. However, for our purposes, we don't need the visitor to visit it, so we can skip it by returning `false`. + +```luau +local myVisitor = visitor.create() +local replacements : { [cst.CstNode] : string } = {} + +function myVisitor.visitExprConstantNumber(numberLiteral: cst.CstExprConstantNumber) + replacements[numberLiteral] = `{ numberLiteral.value * 2 }` + + return false +end +``` + +Finally, we have to tell the visitor library to start visiting the CST using our visitor: + +```luau +local myVisitor = visitorLib.create() +local replacements : { [cst.CstNode] : string } = {} + +function myVisitor.visitExprConstantNumber(numberLiteral: cst.CstExprConstantNumber) + replacements[numberLiteral] = `{ numberLiteral.value * 2 }` + + return false +end + +visitorLib.visit(cst, myVisitor) +``` + +Here's what that looks like in the format `lute transform` expects: + +```luau +local cst = require("@std/syntax") +local visitorLib = require("@std/syntax/visitor") + +local function visitorTransformation(ctx) + local myVisitor = visitorLib.create() + local replacements : { [cst.CstNode] : string } = {} + + function myVisitor.visitExprConstantNumber(numberLiteral:cst.CstExprConstantNumber) + replacements[numberLiteral] = `{ numberLiteral.value * 2 }` + + return false + end + + visitorLib.visit(ctx.parseresult.root, myVisitor) + + return replacements +end + +return visitorTransformation +``` diff --git a/docs/guide/dev-tooling/index.md b/docs/guide/dev-tooling/index.md new file mode 100644 index 000000000..797471194 --- /dev/null +++ b/docs/guide/dev-tooling/index.md @@ -0,0 +1,22 @@ +--- +order: 7 +--- + +# Developer Tooling + +We've invested considerable effort in the developer tooling experience for users +writing Luau with Lute. This section summarizes some of the tools you may want +to consider using on a day to day basis. + +## [Testing](../../cli/test) + +`lute test` is a builtin utility for discovering and running tests. Tests are +written using the `@std/test` library in `.spec.luau` or `.test.luau` files, in +a `tests/` directory and `lute test` will handle discovering and running these +tests for you. + + + +## [Linting](../../cli/lint/index.md) + +`lute lint` is a programmable linter for Luau code, shipped as part of Lute. It is programmable - you can write custom rules for your repository in Luau. It ships with a number of builtin rules, which you can run on your current directory with `lute lint`. For more information, check out the [lint documentation](../../cli/lint/index.md)! diff --git a/docs/guide/docker.md b/docs/guide/docker.md new file mode 100644 index 000000000..f4e9d5e69 --- /dev/null +++ b/docs/guide/docker.md @@ -0,0 +1,70 @@ +--- +order: 8 +--- + +# Docker + +Lute publishes official container images to the [GitHub Container Registry](https://github.com/luau-lang/lute/pkgs/container/lute) on every stable (non-nightly) release. They cover both `linux/amd64` and `linux/arm64`. + +## Available images + +| Image | Base | When to use it | +| --- | --- | --- | +| `ghcr.io/luau-lang/lute` | `debian:stable-slim` | The default. Includes a shell and common utilities, suitable for most use cases. | +| `ghcr.io/luau-lang/lute:distroless` | `gcr.io/distroless/cc-debian13` | Minimal runtime image. No shell or package manager. Smallest attack surface. | +| `ghcr.io/luau-lang/lute:bin` | `scratch` | Just the `lute` binary, intended for use as a build stage in your own Dockerfiles. | + +## Tags + +For a release of version `X.Y.Z`, the workflow publishes the following tags: + +- Default (debian) variant: `latest`, `X`, `X.Y`, `X.Y.Z`, plus the same set prefixed with `debian-` (e.g. `debian`, `debian-X`, `debian-X.Y`, `debian-X.Y.Z`). +- Distroless variant: `distroless`, `distroless-X`, `distroless-X.Y`, `distroless-X.Y.Z`. +- Binary-only variant: `bin`, `bin-X`, `bin-X.Y`, `bin-X.Y.Z`. + +Pin to `X.Y.Z` for reproducible builds, `X.Y` to receive patch updates, or `X` to receive minor and patch updates within a major version. + +## Running Lute + +Print the Lute CLI help (the default `CMD`): + +```bash +docker run --rm ghcr.io/luau-lang/lute +``` + +Run a `script.luau` file from your current directory: + +```bash +docker run --init --rm -it -v "$PWD:/app" -w /app ghcr.io/luau-lang/lute run script.luau +``` + +Open a shell inside the container (debian variant only): + +```bash +docker run --rm -it ghcr.io/luau-lang/lute sh +``` + +The `--init` flag is recommended so signals like `Ctrl+C` are forwarded to `lute` correctly. `-v "$PWD:/app"` mounts the current directory into the container, and `-w /app` makes it the working directory. + +## Using Lute in your own Dockerfile + +You can extend the default image directly: + +```dockerfile +FROM ghcr.io/luau-lang/lute:1 + +WORKDIR /app +COPY . . + +CMD ["run", "server.luau"] +``` + +Or use the binary-only image to install Lute into any base image you want: + +```dockerfile +FROM ubuntu:24.04 + +COPY --from=ghcr.io/luau-lang/lute:bin /lute /usr/local/bin/ + +CMD ["lute", "--help"] +``` diff --git a/docs/guide/hello-world.md b/docs/guide/hello-world.md new file mode 100644 index 000000000..8cfd43611 --- /dev/null +++ b/docs/guide/hello-world.md @@ -0,0 +1,48 @@ +--- +order: 3 +--- + +# Hello World! + +We're going to walk through the creation of a simple program using Lute. To +start with, create a folder for your project with: +```bash +mkdir hello-world +cd hello-world +``` + +Create a file in this project with: +```bash +touch main.luau +``` + +Before we continue, let's ensure that we setup a good autocomplete experience +for your favorite editor of choice. Run: +```bash +lute setup +``` + +This will add some files to a folder called `.lute/` in your home directory. +These type definition files are then made available to `luau-lsp`, the most +popular Luau Language Server, in order to provide high quality autocomplete and +typechecking. + +Finally, open up the file `main.luau` and add: +```luau +print("Hello World") +``` + +You can now run this program by running: +```bash +lute run main.luau +``` + +or just: +```bash +lute main.luau +``` + +Note, that any Luau script can be run like this, not just a file named `main`. + +In the next chapter, we're going to see how we can use some of the tools Lute +provides to help you write some simple programs. diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 000000000..0312eda3b --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,13 @@ +--- +order: 1 +--- + +# Guide + +- [Installation](./installation) +- [Hello World!](./hello-world) +- [Writing a Guessing Game](./writing-a-guessing-game) +- [Writing Tests](./writing-tests/index) +- [Code Transforms](./code-transforms) +- [Developer Tooling](./dev-tooling/index) +- [Docker](./docker) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 6bc6fc3e2..64e9d960c 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -1,17 +1,47 @@ +--- +order: 2 +--- # Installation -:::warning -Lute is pre-0.1.0. There are no release versions yet. The API may change at any time. -::: +## Stable Releases -## Nightly Builds +Lute has stable releases versioned according to [semantic versioning](https://semver.org/). You can find and download the latest stable release on the +[releases page](https://github.com/luau-lang/lute/releases). They can also be installed with toolchain managers like [Rokit](https://github.com/rojo-rbx/rokit) +and [Foreman](https://github.com/Roblox/foreman). + +## Install with Rokit + +You can use [Rokit](https://github.com/rojo-rbx/rokit) to manage your Lute installation. If you have Rokit installed, simply run the following command in your project to install the latest stable version of Lute: + +```bash +rokit add luau-lang/lute@1.0.0 +``` + +## Install with Foreman + +You can also use [Foreman](https://github.com/Roblox/foreman) to manage your Lute installation. If you have Foreman installed, you must first create a `foreman.toml` file in your project with the following content: -Lute is available as a nightly build. You can find and download the latest build on [releases page](https://github.com/luau-lang/lute/releases). +```toml +[tools] +lute = { github = "luau-lang/lute", version = "1.0.0" } +``` + +After creating the `foreman.toml` file, run the following command to install the latest stable version of Lute: +```bash +foreman install +``` -### Install with Rokit +## Docker -You can download nightly builds with [Rokit](https://github.com/rojo-rbx/rokit). After installing Rokit, you can run the following command in your terminal: +Official container images are published to the GitHub Container Registry on every stable release. The default image is based on `debian:stable-slim`: ```bash -rokit add luau-lang/lute@0.1.0-nightly.[date] +docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/luau-lang/lute run script.luau ``` + +See the [Docker guide](./docker) for the full list of image variants and tags. + +## Nightly Builds + +Unstable versions of Lute are available as a nightly build. You can find and download the latest build on [releases page](https://github.com/luau-lang/lute/releases). +They can be installed according to the instructions above with either Rokit or Foreman by specifying the desired version as `0.1.0-nightly.[date]` (e.g. `0.1.0-nightly.2024-06-01`). diff --git a/docs/guide/writing-a-guessing-game.md b/docs/guide/writing-a-guessing-game.md new file mode 100644 index 000000000..6f8ca87cc --- /dev/null +++ b/docs/guide/writing-a-guessing-game.md @@ -0,0 +1,225 @@ +--- +order: 4 +--- + +# Writing a Guessing Game + +### Introduction +We're going to walk through how you might build a small guessing game using Lute. Like we did in the previous chapter, make a new folder +for this project, along with a script called `main.luau`. + +This is a program that will take a user's input and compare it to a secret number that the program will generate. The user will then be prompted to input their guess, +and the program will tell them "Too high!" or "Too low!" until they guess right. + + +### Using the Lute standard library +To start, we'll need some way of getting user input. Thankfully, Lute provides a utility for this in the [`io`](../std/io) library. In `main.luau`, add: +```luau +local io = require("@std/io") +``` + +This is a require statement - the `@std` is an alias, and `io` is the name of the library. The `@std` and `@lute` aliases, have been reserved by Lute to refer to the standard library and Lute internal libraries respectively. You can `require` any set of scripts that return values, even ones you write locally. +This module returns a table with one field - `input`, which waits for the user to type something into the terminal. We can assign this table a value so we can refer to it later. + +```luau +local io = require("@std/io") + +print("Say something! ") +local input : string = io.input() +print(input) +``` + +Now, run this with `lute main.luau`. The program will pause until you input something, then print it back to you: +```bash +lute main.luau +>>> Say something! hello +hello +``` + +### Generating random numbers with Luau builtins +Let's move on to implementing this guessing game. We need to generate a random number, which Luau already provides via the `random` function on the `math` table. + +This highlights an important distinction between Luau and Lute - **Luau** provides builtin functions that operate on the primitive types, e.g. `string`, `math`, `buffer`, etc, while **Lute** provides utilities for interacting with the outside world e.g. user input, the file system, http requests. In this case, the `math.random` function is provided by Luau and will suffice for our purposes. We'll set a limit of 100, so that math.random will not generate numbers that are too small. + +```luau +local io = require("@std/io") +local randomNumber : number = math.random(100) + +print("Guess a number: ") +local input : string = io.input() +local guess = tonumber(input) +``` + +So now we have the guessed value - we should try to let the user know if they got it right or wrong. + +::: info +The output of `input()` is a string, and we want to compare it to a number. If you try to ask `x \< randomNumber`, this won't work correctly. We'll want to convert this using the [`tonumber`](https://www.lua.org/manual/5.1/manual.html#pdf-tonumber) function. +::: + + +```luau +local io = require("@std/io") +local randomNumber = math.random(100) + +print("Guess a number: ") +local input : string = io.input() + + +if guess > randomNumber then + print("Too high!") +elseif guess < randomNumber then + print("Too low!") +else + print("Got it!") +end +``` + +So far so good! Ideally though, the user would be able to guess multiple times - in this example, the user has one chance. Let's even the odds a bit - we'll need to re-prompt the user if they got it wrong and exit out if they guess right. + +### Adding a Loop + +```luau +local io = require("@std/io") +local randomNumber = math.random(100) + +local guessedRight = false +while not guessedRight do + print("Guess a number: ") + local input = io.input() + local guess = tonumber(input) + if guess > randomNumber then + print("Too high!") + elseif guess < randomNumber then + print("Too low!") + else + print("Got it!") + guessedRight = true + end +end +``` + +You can see that here, we've added a variable and a loop to check whether or not the user got the guess right. After checking at the beginning of the loop, we prompt them, they respond, and if they get it right, we update the value of guessedRight. Try running this program to see if you can guess the number! + +### Input Validation +We glanced over what happens if the input provided by the user isn't a number. In this case, `tonumber` will return nil - and we forgot to check for that. Let's add that right now! + +```luau +local io = require("@std/io") +local randomNumber = math.random(100) + +local guessedRight = false +while not guessedRight do + print("Guess a number: ") + local input = io.input() + local guess : number? = tonumber(input) + if not guess then + print(`Not a number: {input}`) + continue + end + if guess > randomNumber then + print("Too high!") + elseif guess < randomNumber then + print("Too low!") + else + print("Got it!") + guessedRight = true + end +end +``` +::: info +Notice the `continue` in the `if not guess then` check. If a user types "hello" instead of "5", tonumber returns nil. Without this check, the program would crash. Using continue allows us to skip the rest of the loop and ask the user again. +::: + +### Handling command line arguments +To finish up, let's see how we can pass arguments to programs in Lute to configure their behaviour. + +For our game, let's try to configure the maximum size of the random number being generated. In Luau, command line arguments can be unpacked at the top of the script using the syntax (`...`), which unpacks the variadic list of arguments passed to the script. For example: + +```luau +local args = {...} -- ... are the arguments passed to this script, and they're being unpacked into an array. +``` + +The first argument, stored in `args[1]` is always the script name, with the remaining arguments following after. If the user passes a `--max ` argument, we should set the maximum value of the number being generated to that value, defaulting to a 100. We can write a small helper function to validate this information: + +```luau +local io = require("@std/io") +local args = {...} +local function getArgs(args) : number + if #args < 3 then + error("Didn't pass enough arguments") + elseif #args \< 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + end + end + return 100 +end +... +``` + +Here, we've written a function that will error when the user inputs not enough arguments, the number passed to the `--max` argument, or the default value of 100 in all other cases. Putting it all together gets us: + +```luau +local io = require("@std/io") +local args = { ... } + +local function getArgs(args) : number + if #args == 2 then + error("Didn't pass enough arguments") + elseif #args < 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + end + end + return 100 +end + +local maxValue = getArgs(args) -- figure out what the --max argument is or use a 100 +local randomNumber = math.random(maxValue) + +local guessedRight = false +while not guessedRight do + print("Guess a number: ") + local input = io.input() + local guess : number? = tonumber(input) + if not guess then + print(`Not a number: {input}`) + continue + end + if guess > randomNumber then + print("Too high!") + elseif guess < randomNumber then + print("Too low!") + else + print("Got it!") + guessedRight = true + end +end +``` + +If you try this program out now, you should be able to run it like: +```bash +lute main.luau --max 50 +``` +to re-run the guessing game with maximum 50 guesses. + +### Conclusion +Congratulations! You've build your first Lute program. In this chapter we covered: +1. How to use `require` to use standard library functionality +2. The difference between functions and libraries provided by Lute and Luau +3. How to write a program that talks to the outside world (your terminal) +4. How to do some basic input validation +5. The structure and parsing of command line arguments in Luau programs. + +## TODOS: +- Perhaps implement a lute new \ command, that creates a main.luau file, setups a luaurc, creates a tests directory? diff --git a/docs/guide/writing-tests/index.md b/docs/guide/writing-tests/index.md new file mode 100644 index 000000000..baabd3e4b --- /dev/null +++ b/docs/guide/writing-tests/index.md @@ -0,0 +1,279 @@ +--- +order: 5 +--- + +# Writing Tests + +As mentioned [here](../dev-tooling/index), `lute` features a builtin utility +for discovering and running tests. In this chapter, we'll see how you can write +tests against this framework and execute them. This can help you improve your +confidence in the correctness of your code. Specifically we'll look at the code +from the guessing game chapter, and test the code that handles argument parsing. + +### Setting up your project + +To get started, set up a project structure that has these files: +``` +project/ + utils.luau + args.test.luau +``` + +Inside of `utils.luau`, add the following code: +```luau +local function getArgs(args) : number + if #args == 2 then + error("Didn't pass enough arguments") + elseif #args \< 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + end + end + return 100 +end + +return table.freeze({ getArgs = getArgs}) +``` + +This defines a module that exports a single frozen (i.e. immutable or read-only) table that has a +single function on it --- the `getArgs` function from the last chapter. + +This function (purportedly) parses a set of command line arguments and either +errors, returns a number if a `--max ` was passed, or 100. Let's try to test this! + + +### Using @std/test +First, open up `args.test.luau` in your favorite text editor. + +We will need to require `@std/test` as well as the code we are trying to test: + +```luau +local test = require("@std/test") +local utils = require("./utils") +``` + +Next up, a simple test case: +```luau +local test = require("@std/test") +local utils = require("./utils") + +test.case("maxOverridesValue", function(asserts) + -- What should happen here ? +end) +``` + +To start, let's write a simple test that asserts that when `getArgs` is invoked +with a `--max` argument, that it returns that value as a number. In order to +match the behaviour of the command line in our testing code, we'll want to +include an extra argument with the name of the script being called. + +```luau +local test = require("@std/test") +local utils = require("./utils") + +test.case("maxOverridesValue", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max", "20"} + local result = utils.getArgs(fakeArgs) + + asserts.eq(20, result) +end) +``` + +:::info +Command line arguments are conventionally passed with the following +format: ` ` + +For example, when you run: +```bash +lute args.test.luau +``` +your shell will pass `lute`, `args.test.luau` as the arguments to `lute`. + +Following that same convention, `lute` will pass `args.test.luau`as well as the +remainder of the arguments as the first argument to the script being run. +::: + +### Running test cases +To run this test, from the root of your project directory run `lute test`. You +should see output like this: +```bash +────────────────────────────────────────────────── +Results: 1 passed, 0 failed of 1 +``` + +Excellent! Our first test passed, but we should write more. + +### Adding more tests + +In this next test, we'll add a test for the case where we don't pass a `--max` +argument: +```luau +local test = require("@std/test") +local utils = require("./utils") + +... + +test.case("noPassingMax", function(asserts) + local fakeArgs = { "fakeScript.luau"} + local result = utils.getArgs(fakeArgs) + + asserts.eq(100, result) +end) +``` + +Great! It looks like these tests pass too. Let's keep going! + +What happens if you pass `--max` without a corresponding number argument? If we +look at the implementation of the `getArgs` function, it looks like it raises an +exception, using the builtin Luau function `error`. Raising an exception is a +reasonable thing to do here, and we'll want to test that our program correctly +raises an exception when `--max` doesn't get passed a number. + +In order to assert this behaviour we'll need to use the `errors` assertion. + +```luau +local test = require("@std/test") +local utils = require("./utils") +... + +test.case("noArgToMax", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max"} + assert.errors(function() + utils.getArgs(fakeArgs) + end) + +end) +``` +This assertion will run the code `utils.getArgs(fakeArgs)`. If it raises an +error, then the assertion will intercept it and succeed. If not, the assertion +will fail, which will be reported as a failed test case. + +### Using Test Suites to organize tests +While we're at it, we can also wrap all of these tests into a single test +suite, which will group these tests together. Test suites also allow you to use +lifecycle methods like `beforeEach`, `beforeAll`, `afterEach`, and `afterAll` to control setup and +tear down for tests. + +```luau +local test = require("@std/test") +local utils = require("./utils") + +test.suite("GetArgsTest", function(suite) + suite:case("maxOverridesValue", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max", "20" } + local result = utils.getArgs(fakeArgs) + + asserts.eq(20, result) + end) + + suite:case("noPassingMax", function(asserts) + local fakeArgs = { "fakeScript.luau" } + local result = utils.getArgs(fakeArgs) + + asserts.eq(100, result) + end) + + suite:case("noArgToMax", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max" } + asserts.errors(function() + utils.getArgs(fakeArgs) + end) + end) + +end) +``` + +Re-running the tests produces: +```bash +────────────────────────────────────────────────── +Results: 3 passed, 0 failed of 3 +``` + +:::info You can re-run individual or groups of tests using: +```bash +lute test -c caseName # run tests with the name caseName +lute test -s suiteName # run all tests with the name suiteName +lute test tests/path/to/.test.luau # run the tests in a particular file +``` +::: + +:::info +One situation where you might to use the aforementioned test suite lifecycle methods is when your tests operate on files on +files in a temporary directory. For example, testing that some code can create a +set of files. When subsequent tests execute, they may be operating in a +directory filled with files leftover from a previous test. + +In this situation, you could use the `beforeEach` method to execute some cleanup +of the temporary directory: + +```luau +local test = require("@std/test") +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") + +-- a temporary directory for tests to operate in +local testDir = path.join(system.tmpdir(), "test") + +test.suite("FileCreation", function() + test.beforeEach(function() + --Deletes the contents of the test directory before each test + fs.removedirectory(testDir, {recursive = true}) + --Recreates the directory so it exists for the next test to use it + fs.createdirectory(testDir) + end) +end) + +``` +::: + +### Fixing failed tests +So far, so good, let's take at what happens when a test case fails! Try this one out: +```luau +suite:case("unsupportedArgument", function(asserts) + local fakeArgs = { "fakeScript.luau", "--what", "foo"} + asserts.errors(function() + utils.getArgs(fakeArgs) + end) +end) +``` + +If you run this, you'll see that it doesn't throw, and `lute test` provides a +stacktrace showing what went wrong: +``` +Failures: + + FAIL getArgsTest.unsupportedArgument + .../args.test.luau:26 + errors: function: 0x000000012f859740 did not throw error. +``` + +This shows us a bug in our `getArgs` implementation - what if the user passes the +right number of arguments but supplies an unsupported option? In this case, the +fix is to error in the case that the second argument to `getArgs` isn't `--max`: +```luau +local function getArgs(args) : number + if #args == 2 then + error("Didn't pass enough arguments") + elseif #args \< 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + else + error(`Expected flag --max, but got {args[2]}`) + end + end + return 100 +end +``` + +If you try to re-run the tests, they should all pass now. diff --git a/docs/index.md b/docs/index.md index 07de07168..2e66f15c6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,23 +4,38 @@ layout: home hero: name: "Lute" - text: "Luau for General-Purpose Programming" - # tagline: My great project tagline + text: "Run Luau Anywhere" + # tagline: "A standalone runtime for general-purpose programming in Luau" + image: + src: /lute-logo.png + alt: Lute logo actions: - theme: brand text: Getting Started link: /guide/installation - theme: alt text: Reference - link: /reference/fs + link: /std -# TODO: add buzz words -# features: -# - title: Feature A -# details: Lorem ipsum dolor sit amet, consectetur adipiscing elit -# - title: Feature B -# details: Lorem ipsum dolor sit amet, consectetur adipiscing elit -# - title: Feature C -# details: Lorem ipsum dolor sit amet, consectetur adipiscing elit +features: + - title: 🖥️ General-Purpose APIs + details: "Lute provides a rich set of built-in APIs for common tasks: file system access, HTTP networking, cryptography, process management, and more." + - title: 🛠️ First-Class Tooling + details: "Lute includes a suite of tools, including a test runner, a linter, and the Luau type checker — all accessible through the `lute` CLI." + - title: 👾 Compatible with Roblox + details: "Lute runs Luau code, just like Roblox, allowing you to easily run and test modules that don't depend on the game engine itself." --- +## What is Lute? + +While [Luau](https://luau.org) is a powerful scripting language, it is sandboxed and primarily embedded in a larger program, like the Roblox game engine. This means +it lacks built-in capabilities for interacting with the outside world. Lute fills the gap by providing a standalone runtime for Luau, designed for general-purpose +programming outside of game engines. _Think of it like [Node.js](https://nodejs.org/) or [Deno](https://deno.land/), but for Luau._ + +## How can I use it? + +Lute provides a rich set of built-in APIs for common programming tasks: file system access, HTTP networking, cryptography, process management, and more. +You can use these APIs to build a wide variety of applications, from command-line tools to web servers to automation scripts and more. These capabilities come +in the form of a set of low-level libraries exposed to Luau under the `@lute` require alias, and a higher-level standard library built on top of those, exposed +under the `@std` alias. For Roblox developers, we're working hard to ensure the Roblox game engine will support this same set of `@std` APIs in the future, so you +can write code that runs both in Lute and Roblox with minimal changes. diff --git a/docs/lute/index.md b/docs/lute/index.md new file mode 100644 index 000000000..e5c126c2a --- /dev/null +++ b/docs/lute/index.md @@ -0,0 +1,6 @@ +--- +order: 4 +--- + +# Lute Runtime Builtins (@lute) +In addition to the [standard library](../std/), Lute also exposes a set of libraries under the `@lute` alias. These APIs are written natively as part of the runtime, and provide the foundational capabilities (e.g. file system and network access, access to Luau internals, etc.) on which the standard library is build. In general, you should prefer to use the standard library modules in `@std` as those libraries are high-level enough that we believe they can be widely supported across Luau runtimes such as Roblox, Lute (this tool), and other game engines. Using `@lute` libraries will make your code less portable as they will not be supported in Roblox or other environments. diff --git a/docs/package-lock.json b/docs/package-lock.json index d5b18208c..9ea74ed8b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -4,10 +4,30 @@ "requires": true, "packages": { "": { + "name": "docs", + "dependencies": { + "vitepress-sidebar": "^1.33.0" + }, "devDependencies": { "vitepress": "^1.6.3" } }, + "node_modules/@algolia/abtesting": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.16.2.tgz", + "integrity": "sha512-n9s6bEV6imdtIEd+BGP7WkA4pEZ5YTdgQ05JQhHwWawHg3hyjpNwC0TShGz6zWhv+jfLDGA/6FFNbySFS0P9cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" + }, + "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 +78,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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.50.2.tgz", + "integrity": "sha512-52iq0vHy1sphgnwoZyx5PmbEt8hsh+m7jD123LmBs6qy4GK7LbYZIeKd+nSnSipN2zvKRZ2zScS6h9PW3J7SXg==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.50.2.tgz", + "integrity": "sha512-WpPIUg+cSG2aPUG0gS8Ko9DwRgbRPUZxJkolhL2aCsmSlcEEZT65dILrfg5ovcxtx0Kvr+xtBVsTMtsQWRtPDQ==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.50.2.tgz", + "integrity": "sha512-Gj2MgtArGcsr82kIqRlo6/dCAFjrs2gLByEqyRENuT7ugrSMFuqg1vDzeBjRL1t3EJEJCFtT0PLX3gB8A6Hq4Q==", "dev": true, "license": "MIT", "engines": { @@ -100,160 +120,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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.50.2.tgz", + "integrity": "sha512-CUqoid5jDpmrc0oK3/xuZXFt6kwT0P9Lw7/nsM14YTr6puvmi+OUKmURpmebQF22S2vCG8L1DAoXXujxQUi/ug==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.50.2.tgz", + "integrity": "sha512-AndZWFoc0gbP5901OeQJ73BazgGgSGiBEba4ohdoJuZwHTO2Gio8Q4L1VLmytMBYcviVigB0iICToMvEJxI4ug==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.50.2.tgz", + "integrity": "sha512-NWoL+psEkz5dIzweaByVXuEB45wS8/rk0E0AhMMnaVJdVs7TcACPH2/OURm+N0xRDITkTHqCna823rd6Uqntdg==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.50.2.tgz", + "integrity": "sha512-ypSboUJ3XJoQz5DeDo82hCnrRuwq3q9ZdFhVKAik9TnZh1DvLqoQsrbBjXg7C7zQOtV/Qbge/HmyoV6V5L7MhQ==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.50.2.tgz", + "integrity": "sha512-VlR2FRXLw2bCB94SQo6zxg/Qi+547aOji6Pb+dKE7h1DMCCY317St+OpjpmgzE+bT2O9ALIc0V4nVIBOd7Gy+Q==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.50.2.tgz", + "integrity": "sha512-Cmvfp2+qopzQt8OilU97rhLhosq7ZrB6uieok3EwFUqG/aalPg6DgfCmu0yJMrYe+KMC1qRVt1MTRAUwLknUMQ==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.50.2.tgz", + "integrity": "sha512-jrkuyKoOM7dFWQ/6Y4hQAse2SC3L/RldG6GnPjMvAj65h+7Ubb51S0pKk4ofSStF0xm4LCNe0C4T6XX4nOFDiQ==", "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.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.50.2.tgz", + "integrity": "sha512-4107YLJqCudPiBUlwnk6oTSUVwU7ab+qL1SfQGEDYI8DZH5gsf1ekPt9JykXRKYXf2IfouFL5GiCY/PHTFIjYw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.50.2.tgz", + "integrity": "sha512-vOrd3MQpLgmf6wXAueTuZ/cA0W4uRwIHHaxNy3h+a6YcNn6bCV/gFdZuv3F13v593zRU2k5R75NmvRWLenvMrw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.50.2" }, "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.50.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.50.2.tgz", + "integrity": "sha512-Mu9BFtgzGqDUy5Bcs2nMyoILIFSN13GKQaklKAFIsd0K3/9CpNyfeBc+/+Qs6mFZLlxG9qzullO7h+bjcTBuGQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.50.2" }, "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 +281,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 +291,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.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -287,14 +307,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.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "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 +763,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.78", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.78.tgz", + "integrity": "sha512-I3lkNp0Qu7q2iZWkdcf/I2hqGhzK6qxdILh9T7XqowQrnpmG/BayDsiCf6PktDoWlW0U971xA5g+panm+NFrfQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -759,17 +779,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "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/@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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -781,9 +810,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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -795,9 +824,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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -809,9 +838,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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -823,9 +852,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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -837,9 +866,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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -851,13 +880,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -865,13 +897,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -879,13 +914,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -893,41 +931,84 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "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-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -935,13 +1016,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -949,13 +1033,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -963,13 +1050,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -977,13 +1067,16 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -991,23 +1084,54 @@ ] }, "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -1019,9 +1143,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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -1032,10 +1156,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -1134,9 +1272,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 +1345,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 +1359,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.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", - "entities": "^4.5.0", + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", "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.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "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.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "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.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", "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.8", + "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.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" } }, "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.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.7.5" + "@vue/devtools-kit": "^7.7.9" } }, "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.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.7.5", + "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1301,9 +1439,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.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", "dev": true, "license": "MIT", "dependencies": { @@ -1311,57 +1449,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.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.32" } }, "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.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" } }, "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.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", - "csstype": "^3.1.3" + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.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.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.32" } }, "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.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "dev": true, "license": "MIT" }, @@ -1472,40 +1610,71 @@ } }, "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.50.2", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.2.tgz", + "integrity": "sha512-Tfp26yoNWurUjfgK4GOrVJQhSNXu9tJtHfFFNosgT2YClG+vPyUjX/gbC8rG39qLncnZg8Fj34iarQWpMkqefw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.16.2", + "@algolia/client-abtesting": "5.50.2", + "@algolia/client-analytics": "5.50.2", + "@algolia/client-common": "5.50.2", + "@algolia/client-insights": "5.50.2", + "@algolia/client-personalization": "5.50.2", + "@algolia/client-query-suggestions": "5.50.2", + "@algolia/client-search": "5.50.2", + "@algolia/ingestion": "1.50.2", + "@algolia/monitoring": "1.50.2", + "@algolia/recommend": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, + "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": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "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.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1551,25 +1720,39 @@ } }, "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", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, @@ -1605,9 +1788,9 @@ "license": "MIT" }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -1656,6 +1839,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 +1859,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.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.4.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 +1912,45 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "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 +2007,88 @@ "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-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": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "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": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "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": { @@ -1775,9 +2099,9 @@ "license": "MIT" }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dev": true, "license": "MIT", "dependencies": { @@ -1890,10 +2214,34 @@ ], "license": "MIT" }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/minisearch": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz", - "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==", + "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 +2283,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": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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 +2329,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.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -1970,7 +2349,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1979,9 +2358,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.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", "dev": true, "license": "MIT", "funding": { @@ -1990,9 +2369,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,10 +2379,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/qsu": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/qsu/-/qsu-1.12.0.tgz", + "integrity": "sha512-kcB2A7XS+ApkxAIc63yOqF2mmvqyA4AezObM1RxAhOB6O0bRA8xCzm6PLwU70CVW9R+0Rozqd0pgOFskNtX2eQ==", + "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", - "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "dev": true, "license": "MIT", "dependencies": { @@ -2035,13 +2423,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.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2051,26 +2439,31 @@ "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.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -2082,6 +2475,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 +2526,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 +2569,12 @@ "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/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -2145,23 +2590,32 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", "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.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "dev": true, "license": "MIT" }, @@ -2177,9 +2631,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": { @@ -2219,9 +2673,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dev": true, "license": "MIT", "dependencies": { @@ -2235,9 +2689,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 +2719,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 +2734,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 +2794,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 +2835,32 @@ } } }, + "node_modules/vitepress-sidebar": { + "version": "1.33.1", + "resolved": "https://registry.npmjs.org/vitepress-sidebar/-/vitepress-sidebar-1.33.1.tgz", + "integrity": "sha512-wPUbXezGakVldawixeRW5tKQTLKoMj2t4nWoThKfCltBM/9a38IE+wCXmmRNW22ZKC32SD/X/sG6NyCTK8QBRg==", + "license": "MIT", + "dependencies": { + "glob": "11.1.0", + "gray-matter": "4.0.3", + "qsu": "^1.10.4" + }, + "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.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "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.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" @@ -2403,6 +2871,21 @@ } } }, + "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/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..247252875 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,11 +1,14 @@ { "scripts": { - "gen": "lute run scripts/reference.luau", + "gen": "lute doc -m docgen.luau", "dev": "npm run gen && vitepress dev", "build": "npm run gen && vitepress build", "preview": "vitepress preview" }, "devDependencies": { "vitepress": "^1.6.3" + }, + "dependencies": { + "vitepress-sidebar": "^1.33.0" } } diff --git a/docs/public/lute-logo.png b/docs/public/lute-logo.png new file mode 100644 index 000000000..ccaff4cff Binary files /dev/null and b/docs/public/lute-logo.png differ diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau deleted file mode 100644 index 7ec155498..000000000 --- a/docs/scripts/reference.luau +++ /dev/null @@ -1,97 +0,0 @@ -local fs = require("@lute/fs") -local process = require("@lute/process") - -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 function extractPropertySignature(module: string, line: string): (string?, string?) - -- 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 - end - - -- 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) - - if name and signature then - table.insert(definitions, { - name = name, - signature = signature, - }) - end - end - - return definitions -end - -local function generateMarkdown(moduleName: string, definitions: { { name: string, signature: string } }): string - local lines = { - "# " .. moduleName, - "", - "```luau", - "local " .. moduleName .. ' = require("@lute/' .. moduleName .. '")', - "```", - "", - } - - table.sort(definitions, function(a, b) - return a.name < b.name - end) - - for _, def in definitions do - table.insert(lines, "## " .. def.name) - table.insert(lines, "```luau") - table.insert(lines, def.signature) - table.insert(lines, "```") - table.insert(lines, "") - end - - return table.concat(lines, "\n") -end - -local definitionsPath = projectRelative("definitions") -local referencePath = projectRelative("docs", "reference") - -pcall(fs.mkdir, referencePath) - -local definitionFiles = fs.listdir(definitionsPath) - -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" - - print("Processing " .. moduleName .. "...") - - local definitions = parseDefinitionFile(moduleName, definitionFilePath) - local markdown = generateMarkdown(moduleName, definitions) - - fs.writestringtofile(referenceFilePath, markdown) - - print("Generated " .. referenceFilePath) - end -end - -print("Reference documentation generation complete!") diff --git a/docs/std/index.md b/docs/std/index.md new file mode 100644 index 000000000..0d188148c --- /dev/null +++ b/docs/std/index.md @@ -0,0 +1,29 @@ +--- +order: 3 +--- + +# Standard Library (@std) +The Lute Standard Library is a set of common utilities that you will almost +certainly want to reach for when writing programs in Luau. In order to make Luau +useful for general-purpose programming, Lute provides a standard library, under +the alias `@std`, that aims to provide all the necessary utilities to help you +author useful programs! Beyond being useful, we aim for this collection of +libraries to be high-level and readily implementable by other Luau runtime +environments to enable more compatibility within our ecosystem. We are actively +working towards integrating these libraries into the most-used Luau runtime, +Roblox. The modules in this library are broadly categorized as follows: + +1. Interacting with the language -- parsing Luau, interacting with the compiler, + virtual machine, and type checker. +2. I/O -- talking to the world outside of the program, including the command + line, the file system, external processes, the network, etc. +3. Platform abstractions -- e.g. file system path, measures of time, and + cooperative multitasking +4. General programming utilities -- libraries for interacting with tables, + strings, json, and testing your code + +When writing programs against the standard library, you can require them using +the reserved alias `@std`: +```luau +local _ = require("@std/json") +``` diff --git a/examples/.luaurc b/examples/.luaurc new file mode 100644 index 000000000..18f8631d1 --- /dev/null +++ b/examples/.luaurc @@ -0,0 +1,8 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../batteries", + "lint": "@std/commands/lint/types", + "transform": "@std/commands/transform/types" + } +} diff --git a/examples/a.luau b/examples/a.luau index f55243aad..120f018f9 100644 --- a/examples/a.luau +++ b/examples/a.luau @@ -1 +1 @@ -local x = require("./b") +local _ = require("./b") 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 deleted file mode 100644 index 6bdeba554..000000000 --- a/examples/badisnan.luau +++ /dev/null @@ -1,3 +0,0 @@ -return function(n) - return n == 0 / 0 -end diff --git a/examples/colorful.luau b/examples/colorful.luau index e2f0d2021..abfd1b4b4 100644 --- a/examples/colorful.luau +++ b/examples/colorful.luau @@ -1,11 +1,11 @@ -local colorful = require("@batteries/colorful") +local richterm = require("@batteries/richterm") -print(colorful.red("This is red text!")) -print(colorful.bgRed("This is red background!")) -print(colorful.bold("This is bold text!")) -print(colorful.underline("This is underlined text!")) -print(colorful.dim(colorful.italic("This is dim italic text!"))) +print(richterm.red("This is red text!")) +print(richterm.bgRed("This is red background!")) +print(richterm.bold("This is bold text!")) +print(richterm.underline("This is underlined text!")) +print(richterm.dim(richterm.italic("This is dim italic text!"))) -print(colorful.red(`red({colorful.blue(`blue({colorful.green("green")})`)})`)) +print(richterm.red(`red({richterm.blue(`blue({richterm.green("green")})`)})`)) -print(colorful.combine(colorful.red, colorful.bold)("This is bold red text!")) +print(richterm.combine(richterm.red, richterm.bold)("This is bold red text!")) 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/coroutine_error_isolation.luau b/examples/coroutine_error_isolation.luau index a792f2288..7184270b9 100644 --- a/examples/coroutine_error_isolation.luau +++ b/examples/coroutine_error_isolation.luau @@ -2,7 +2,7 @@ local task = require("@lute/task") local function loop() while true do - task.defer() + task.deferSelf() -- It may be useful to remove this print because it gets spammed, or implement another way -- in which you can determinatively tell whether or not the thread is still active despite @@ -13,7 +13,7 @@ end local function otherloop() while true do - task.defer() + task.deferSelf() if math.random(1, 2) == 1 then error("oops") diff --git a/examples/create_directory.luau b/examples/create_directory.luau new file mode 100644 index 000000000..8c587822a --- /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..41afa4012 100644 --- a/examples/directories.luau +++ b/examples/directories.luau @@ -1,5 +1,5 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") -for _, file in fs.listdir("./examples") do +for _, file in fs.listDirectory("./examples") do print(`Example {file.name} is a {file.type}`) end diff --git a/examples/docs/test_module.luau b/examples/docs/test_module.luau new file mode 100644 index 000000000..b0987532c --- /dev/null +++ b/examples/docs/test_module.luau @@ -0,0 +1,41 @@ +-- lute-lint-global-ignore(unused_variable) +-- 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/examples/docs/test_module.md b/examples/docs/test_module.md new file mode 100755 index 000000000..5d688c460 --- /dev/null +++ b/examples/docs/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/fs_metadata.luau b/examples/fs_metadata.luau new file mode 100644 index 000000000..e9f939d47 --- /dev/null +++ b/examples/fs_metadata.luau @@ -0,0 +1,10 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") + +local scriptPath = path.resolve(process.cwd(), process.args[1]) + +local metadata = fs.metadata(scriptPath) +local created = metadata.created + +print(tostring(created)) diff --git a/examples/greeter.luau b/examples/greeter.luau new file mode 100644 index 000000000..77eacde78 --- /dev/null +++ b/examples/greeter.luau @@ -0,0 +1,13 @@ +local io = require("@lute/io") +local process = require("@lute/process") + +process.onSignal("SIGINT", function() + print("\nOh, goodbye then!") + process.exit(0) +end) + +while true do + io.write("What's your name? ") + local name = io.read():gsub("\r?\n$", "") + print(`Hi, {name}!`) +end 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..2376be8d5 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", @@ -9,5 +9,5 @@ print(serialize) local deserialized = json.deserialize([[{ "hello": "world" }]]) - +deserialized = assert(json.asObject(deserialized)) print(deserialized.hello) 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..09e5bae37 --- /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("@lint") +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.CstExprLocal, b: syntax.CstExprLocal): boolean + return a["local"] == b["local"] +end + +function compFuncs.exprGlobalsSame(a: syntax.CstExprGlobal, b: syntax.CstExprGlobal): boolean + return a.name.text == b.name.text +end + +function compFuncs.exprIndexNamesSame(a: syntax.CstExprIndexName, b: syntax.CstExprIndexName): 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.CstExprIndexExpr, b: syntax.CstExprIndexExpr): boolean + if a.index.tag == "string" and b.index.tag == "string" then + if a.index.value.text ~= b.index.value.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.CstExpr, b: syntax.CstExpr): boolean + if a.tag ~= b.tag then + return false + end + + if a.tag == "local" then + return compFuncs.exprLocalsSame(a, b :: syntax.CstExprLocal) + elseif a.tag == "global" then + return compFuncs.exprGlobalsSame(a, b :: syntax.CstExprGlobal) + elseif a.tag == "indexname" then + return compFuncs.exprIndexNamesSame(a, b :: syntax.CstExprIndexName) + elseif a.tag == "index" then + return compFuncs.exprIndexExprsSame(a, b :: syntax.CstExprIndexExpr) + else + return false + end +end + +-- Report instances of attempted swaps like: +-- a = b; b = a +local function lint(cst: syntax.CstStatBlock, sourcepath: path.Path?): { lintTypes.LintViolation } + local violations = {} + + local nodes = query.findAllFromRoot(cst, 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], currStat.values[1] + local nextVar, nextVal = nextStat.variables[1], nextStat.values[1] + + if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then + 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, + }) + 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..b6391e73a --- /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("@lint") +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 = "Dividing by zero will always result in NaN, consider using `math.nan` instead." + +local function lint(cst: syntax.CstStatBlock, sourcepath: path.Path?): { lintTypes.LintViolation } + return query + .findAllFromRoot(cst, 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( + -- FIXME(Luau): We would need bidirectional inference of generics + -- and return types to remove this annotation. + function(n): lintTypes.LintViolation + 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..770df4269 --- /dev/null +++ b/examples/module_return_type/get_module_return_type.luau @@ -0,0 +1,51 @@ +local luau = require("@std/luau") +local path = require("@std/path") +local process = require("@std/process") + +local filePath = path.join(process.cwd(), "examples", "module_return_type", "mainmodule.luau") +local returnType = luau.typeofModule(filePath) + +if not returnType then + error("Failed to get module return type") +end + +if not returnType.head then + error("Return type head is nil") +end + +for _, retType in ipairs(returnType.head) do + if retType.tag ~= "table" then + error("Expected return type to be a table") + end + + if not retType.properties then + error("Return type table has no properties") + end + + for name, prop in pairs(retType.properties) do + if prop.read then + print(name .. ": " .. prop.read.tag) + elseif prop.write then + print(name .. ": " .. prop.write.tag) + else + print(name .. ": (no read or write type)") + end + end +end + +-- Output: +-- func1: function +-- strings: table +-- _metadata: table + +-- Actual 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/net_example.luau b/examples/net_example.luau index 1935e695e..8c1569527 100644 --- a/examples/net_example.luau +++ b/examples/net_example.luau @@ -1,32 +1,33 @@ -local net = require("@lute/net") +local net = require("@std/net") local task = require("@std/task") local pp = require("@batteries/pp") -local t = task.create(function() - return net.request("https://en.wikipedia.org/") -end) - -print(task.await(t).status) +print(net.request("https://en.wikipedia.org/").status) -print(pp(task.await(task.create(net.request, "https://en.wikipedia.org/")).headers)) +print(pp(net.request("https://en.wikipedia.org/").headers)) -local t1 = task.create(net.request, "https://en.wikipedia.org/") -local t2 = task.create(net.request, "https://www.google.com/") -local t3 = task.create(net.request, "https://www.bing.com/") +local start = os.clock() --- print(task.awaitAll(t1, t2, t3)) +local results = {} +local done = 0 -local together = true -local start = os.clock() +task.spawn(function() + results[1] = net.request("https://en.wikipedia.org/") + done += 1 +end) +task.spawn(function() + results[2] = net.request("https://www.google.com/") + done += 1 +end) +task.spawn(function() + results[3] = net.request("https://www.bing.com/") + done += 1 +end) -if together then - task.awaitall(t1, t2, t3) -else - task.await(t1) - task.await(t2) - task.await(t3) +while done < 3 do + task.wait() end print(os.clock() - start) diff --git a/examples/options_transformer.luau b/examples/options_transformer.luau new file mode 100644 index 000000000..39ff94a4e --- /dev/null +++ b/examples/options_transformer.luau @@ -0,0 +1,25 @@ +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntax_utils = require("@std/syntax/utils") + +local transformTypes = require("@transform") + +return { + options = { + { kind = "string", name = "functionName", default = "math.isnan" }, + }, + transform = function(ctx: transformTypes.Context) + local functionName = ctx.options.functionName + return query + .findAllFromRoot(ctx.parseresult :: syntax.CstParseResult, 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 `{functionName}({(bin.lhsOperand :: syntax.CstExprLocal).token.text})` + end) + end, +} diff --git a/examples/parallel_serve.luau b/examples/parallel_serve.luau index 917693d6a..edb285de3 100644 --- a/examples/parallel_serve.luau +++ b/examples/parallel_serve.luau @@ -3,8 +3,8 @@ local task = require("@std/task") local threadCount = 8 -for i = 1, threadCount do - task.create(vm.create("./parallel_serve_helper").serve) +for _ = 1, threadCount do + task.spawn(vm.create("./parallel_serve_helper").serve) end print(`Created {threadCount} server threads`) diff --git a/examples/parallel_serve_helper.luau b/examples/parallel_serve_helper.luau index 3b9fad02e..6772e0615 100644 --- a/examples/parallel_serve_helper.luau +++ b/examples/parallel_serve_helper.luau @@ -1,9 +1,9 @@ -local net = require("@lute/net") +local server = require("@lute/net/server") return { serve = function() - net.serve({ - handler = function(req) + server.serve({ + handler = function(_) return "Hello, lute!" end, reuseport = true, diff --git a/examples/parallel_sort.luau b/examples/parallel_sort.luau index 17636f05d..a609d9db4 100644 --- a/examples/parallel_sort.luau +++ b/examples/parallel_sort.luau @@ -2,7 +2,7 @@ local vm = require("@lute/vm") local task = require("@std/task") -local function getslice(t, n, m) +local function getslice(t, n: number, m: number) local size = math.ceil(#t / m) local slice = table.create(size) @@ -10,7 +10,7 @@ local function getslice(t, n, m) return slice end -local function merge(t1, t2, comp) +local function merge(t1: { T }, t2: { T }, comp: (T, T) -> lt) local t = table.create(#t1 + #t2) local pos1, pos2, pos = 1, 1, 1 @@ -42,26 +42,36 @@ local function merge(t1, t2, comp) end local threadCount = 8 -local threads = {} +local threads: { { [any]: any } } = {} -for i = 1, threadCount do +for _ = 1, threadCount do table.insert(threads, vm.create("./parallel_sort_helper")) end -local function parallelMergeSort(t, comp) - local slices = {} +local function parallelMergeSort(t: { T }, comp: (T, T) -> lt): { T } + local results: { { T } } = {} + local done = 0 for i = 1, threadCount do - table.insert(slices, task.create(threads[i].sort, getslice(t, i, threadCount))) + local idx = i + local slice = getslice(t, idx, threadCount) + task.spawn(function() + results[idx] = threads[idx].sort(slice) + done += 1 + end) end - local tomerge = table.pack(task.awaitall(table.unpack(slices))) - tomerge.n = nil + while done < threadCount do + task.wait() + end + + local tomerge = results while #tomerge > 1 do local next = {} for i = 1, #tomerge, 2 do - table.insert(next, merge(tomerge[i], tomerge[i + 1], comp)) + -- LUAUFIX: we should not need to explicitly cast these arrays to {T} + table.insert(next, merge(tomerge[i] :: { T }, tomerge[i + 1] :: { T }, comp)) end tomerge = next @@ -81,13 +91,14 @@ end local t2 = table.clone(t) local startseq = os.clock() -table.sort(t, function(a, b) +table.sort(t, function(a: number, b: number) return a < b end) print("sequential in:", os.clock() - startseq) local startparallel = os.clock() -t2 = parallelMergeSort(t2, function(a, b) +-- LUAUFIX: we should not need to explicitly instantiate the type here +t2 = parallelMergeSort<>(t2, function(a, b) return a < b end) print("parallel in:", os.clock() - startparallel) diff --git a/examples/parallel_sort_helper.luau b/examples/parallel_sort_helper.luau index 95cad9793..69da3ced4 100644 --- a/examples/parallel_sort_helper.luau +++ b/examples/parallel_sort_helper.luau @@ -1,6 +1,7 @@ return { sort = function(t) - table.sort(t, function(a, b) + -- LUAUFIX - we cannot figure out the type of the array elements, even with an annotation on t. + table.sort(t, function(a: number, b: number) return a < b end) return t diff --git a/examples/parsing.luau b/examples/parsing.luau index 55c37f6a9..177a510a3 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..fcfb8ca6b 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -1,32 +1,33 @@ -local process = require("@lute/process") -local task = require("@std/task") +local process = require("@std/process") + +process.system("echo hello", {}) local result = process.run({ "echo", "Hello, lute!" }) print(result.exitcode) print(result.stdout) -local t1 = task.create(process.run, { "echo", "-n", "One" }) -local t2 = task.create(process.run, { "echo", "-n", "Two" }) -local t3 = task.create(process.run, { "echo", "-n", "Three" }) - -local r1, r2, r3 = task.awaitall(t1, t2, t3) +local r1 = process.run({ "echo", "-n", "One" }) +local r2 = process.run({ "echo", "-n", "Two" }) +local r3 = process.run({ "echo", "-n", "Three" }) 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/profile_test.json b/examples/profile_test.json new file mode 100644 index 000000000..25fad11ff --- /dev/null +++ b/examples/profile_test.json @@ -0,0 +1,2788 @@ +{"traceEvents":[ +{"ph":"B","name":"(anonymous)","cat":"function","pid":1,"tid":1,"ts":20,"args":{"file":"./examples/profile_test.luau","line":1}}, +{"ph":"B","name":"runTests","cat":"function","pid":1,"tid":1,"ts":90,"args":{"file":"./examples/profile_test.luau","line":71}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":100,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":114,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"format","cat":"function","pid":1,"tid":1,"ts":114,"args":{"file":"[C]","line":-1}}, +{"ph":"E","name":"format","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"[C]","line":-1}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":183,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":208,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":208,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":208,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":298,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":318,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":328,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":358,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":358,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":368,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":368,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":398,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":428,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":428,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":428,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":484,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":484,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":494,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":494,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":514,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":514,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":524,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":524,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":554,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":594,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":594,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":594,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":604,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":644,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":644,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":644,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":654,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":654,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":654,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":754,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":754,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":754,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":764,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":764,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":764,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":814,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":844,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":844,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":864,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":884,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":884,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":904,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":997,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":997,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1007,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1027,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1047,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1077,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1097,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1137,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1177,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1197,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1207,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1207,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1217,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1227,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1237,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1237,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1247,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1267,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1277,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1277,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1287,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1287,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1307,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1307,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1307,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1347,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1347,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1357,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1357,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1357,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1477,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1497,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1507,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1517,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1517,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1517,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1537,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1557,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1567,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1577,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1627,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1627,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1627,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1681,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1681,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1681,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1691,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1721,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1721,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1731,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1731,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1741,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1761,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1761,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1761,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1771,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1781,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1781,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1781,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1801,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1821,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1821,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1821,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1831,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1831,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1831,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1841,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1861,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1861,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1861,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1905,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1915,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1915,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1927,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1927,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1937,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1997,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2007,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2007,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2027,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2047,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2047,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2067,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2077,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2077,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2170,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2170,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2190,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2190,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2230,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2260,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2260,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2280,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2412,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2422,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2442,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2442,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2452,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2452,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2482,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2492,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2502,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2552,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2562,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2562,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2612,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2642,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2652,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2652,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2842,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2892,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2952,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2952,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2982,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3052,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3104,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3114,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3114,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3144,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3184,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3194,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3204,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3204,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3214,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3224,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3224,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3234,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3234,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3234,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3244,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3244,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3254,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3284,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3284,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3284,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3328,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3398,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3398,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3468,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3468,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3478,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3488,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3488,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3488,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3498,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3548,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3568,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3568,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3593,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3603,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3603,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3613,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3613,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3613,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3623,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3623,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3633,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3633,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3643,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3663,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3733,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3793,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3793,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3813,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3813,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3833,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3833,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3853,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3863,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3903,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3923,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4048,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4078,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4078,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4088,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4088,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4088,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4138,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4138,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4148,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4188,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4218,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4218,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4218,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4261,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4261,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4271,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4271,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4281,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4281,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4331,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4341,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4351,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4351,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4361,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4361,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4361,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4371,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4371,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4381,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4381,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4381,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4401,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4401,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4411,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4411,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4451,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4451,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4461,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4471,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4471,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4471,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4531,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4541,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4551,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4551,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4561,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4571,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4571,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4571,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4591,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4601,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4621,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4621,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4621,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4631,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4631,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4641,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4641,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4641,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4651,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4651,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4661,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4794,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4794,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4794,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4844,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4864,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4864,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4874,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4874,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4884,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4904,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4914,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4924,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4924,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4954,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5018,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5048,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5148,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5148,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5188,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5188,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5188,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5232,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5232,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5242,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5252,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5252,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5363,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5373,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5383,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5403,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5403,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5465,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5465,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5475,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5475,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5495,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5495,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5495,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5505,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5515,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5535,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5555,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5555,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5575,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5595,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5595,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5635,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5635,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5635,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5655,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5655,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5655,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5699,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5699,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5709,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5709,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5709,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5719,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5719,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5739,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5739,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5749,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5749,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5759,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5779,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5779,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5789,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5789,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5809,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5829,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5839,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5839,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5839,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5849,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5859,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5859,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5869,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5933,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5933,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6063,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6083,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6083,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6103,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6123,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6123,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6123,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6153,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6173,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6173,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6173,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6183,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6193,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6193,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6193,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6203,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6213,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6223,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6223,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6223,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6243,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6243,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6253,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6253,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6253,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6363,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6363,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6363,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6426,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6426,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6426,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6436,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6436,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6446,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6446,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6456,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6456,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6456,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6466,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6476,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6486,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6496,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6496,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6526,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6526,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6536,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6536,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6546,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6546,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6566,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6566,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6576,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6586,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6586,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6586,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6626,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6646,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6646,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6646,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6852,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6892,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6892,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6952,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6982,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7062,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7062,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7062,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7104,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7104,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7144,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7184,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7204,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7214,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7224,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7244,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7254,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7314,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7334,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7334,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7334,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7344,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7344,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7364,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7404,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7424,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7434,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7444,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7454,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7454,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7464,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7464,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7464,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7484,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7494,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7514,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7514,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7514,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7555,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7575,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7575,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7585,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7585,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7595,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7655,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7665,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7675,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7695,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7695,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7695,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7705,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7705,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7715,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7755,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7775,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7795,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7795,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7815,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7815,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7825,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7825,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7845,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7855,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7855,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7875,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7895,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7905,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7925,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7925,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7945,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7945,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8005,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8025,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8025,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8025,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8055,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8065,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8065,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8075,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8075,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8095,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8105,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8230,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8230,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8280,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8300,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8330,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8350,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8350,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8360,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8370,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8370,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8410,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8430,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8470,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8470,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8470,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8550,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8550,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8560,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8560,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8560,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8580,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8580,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8670,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8670,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8670,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8832,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8832,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8842,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8842,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8912,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9062,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9072,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9102,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9102,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9102,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9152,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9152,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9152,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9162,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9162,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9162,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9172,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9192,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9202,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9212,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9232,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9242,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9262,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9272,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9272,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9282,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9352,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9372,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9412,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9412,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9422,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9432,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9432,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9442,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9452,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9492,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9502,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9542,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9542,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9542,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9592,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9592,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9592,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9602,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9602,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9612,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9612,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9612,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9652,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9807,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9817,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9827,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9827,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9847,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9847,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9847,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9857,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9867,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9867,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9887,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9897,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9897,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9907,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9907,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9907,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9927,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9967,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9967,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9987,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9987,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9987,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10048,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10078,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10138,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10188,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10228,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10298,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10318,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10328,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10358,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10368,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":10418,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10418,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10459,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10469,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10480,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10490,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10500,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10510,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10510,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10540,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10550,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10580,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10610,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10610,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10620,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10620,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10693,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10733,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10753,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10763,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10763,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10793,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10803,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10803,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10803,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10813,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10833,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10873,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10893,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10893,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10903,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10903,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10923,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10923,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":11063,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11063,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11103,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11103,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11125,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11125,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11175,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11175,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11185,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11205,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11215,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11215,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11225,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11235,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11235,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11275,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11317,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11347,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11397,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11407,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11407,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11417,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11417,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11477,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11477,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11517,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11547,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11567,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11577,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11607,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11607,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11607,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11627,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11637,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11647,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11667,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11667,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11667,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11677,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11677,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11677,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11687,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11687,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11697,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"(anonymous)","cat":"function","pid":1,"tid":1,"ts":11737,"args":{"file":"./examples/profile_test.luau","line":1}}, +{"ph":"E","name":"runTests","cat":"function","pid":1,"tid":1,"ts":11737,"args":{"file":"./examples/profile_test.luau","line":71}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11737,"args":{"file":"./examples/profile_test.luau","line":37}} +]} diff --git a/examples/profile_test.luau b/examples/profile_test.luau new file mode 100644 index 000000000..3d062c4e3 --- /dev/null +++ b/examples/profile_test.luau @@ -0,0 +1,92 @@ +-- Test program for profiler with deep call stacks and recursion + +-- Fibonacci with recursion (creates deep stacks) +local function fibonacci(n: number): number + if n <= 1 then + return n + end + return fibonacci(n - 1) + fibonacci(n - 2) +end + +-- Array processing with nested calls +local function processArray(arr: { number }, depth: number): number + if depth == 0 then + local sum = 0 + for _, v in arr do + sum += v * v + end + return sum + else + local result = 0 + for i = 1, #arr do + result += processArray({ arr[i], arr[i] * 2 }, depth - 1) + end + return result + end +end + +-- Nested function calls +local function innerWork(x: number): number + local result = 0 + for i = 1, x do + result += math.sin(i) * math.cos(i) + end + return result +end + +local function middleWork(x: number): number + local sum = 0 + for _ = 1, 10 do + sum += innerWork(x) + end + return sum +end + +-- Tree traversal (creates interesting call patterns) +type TreeNode = { + value: number, + left: TreeNode?, + right: TreeNode?, +} + +local function createTree(depth: number, value: number): TreeNode + if depth == 0 then + return { value = value, left = nil, right = nil } + end + return { + value = value, + left = createTree(depth - 1, value * 2), + right = createTree(depth - 1, value * 2 + 1), + } +end + +local function sumTree(node: TreeNode?): number + if node == nil then + return 0 + end + return node.value + sumTree(node.left) + sumTree(node.right) +end + +-- Main test function +local function runTests() + print("Starting profiler test...") + + print("Tree traversal") + for i = 1, 5 do + local tree = createTree(i, 1) + local sum = sumTree(tree) + print(` Tree sum = {sum}`) + end + + print("Mixed workload") + for round = 1, 50 do + local fib = fibonacci(15) + local arr_result = processArray({ 1, 2, 3 }, 2) + local work = middleWork(50) + print(` Round {round}: fib={fib}, arr={arr_result}, work={work}`) + end + + print("\nProfiler test complete!") +end + +runTests() diff --git a/examples/query.luau b/examples/query.luau new file mode 100644 index 000000000..aaee201a7 --- /dev/null +++ b/examples/query.luau @@ -0,0 +1,18 @@ +local syntax = require("@std/syntax") +local query = require("@std/syntax/query") +local utils = require("@std/syntax/utils") + +local cst = syntax.parseExpr("require(OldComponent)") + +local p = query.findAllFromRoot(cst, utils.isRequireCall):filter(function(call) + local firstArg = call.arguments[1] + return firstArg.tag == "indexname" and firstArg.index.text == "OldComponent" +end) + +local argIndexNames = query.map(p, function(call: syntax.CstExprCall) + return call.arguments[1] +end) + +local _replacements = argIndexNames:replace(function(_) + return "NewComponent" +end) diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau new file mode 100644 index 000000000..ed4595c80 --- /dev/null +++ b/examples/query_transformer.luau @@ -0,0 +1,21 @@ +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntax_utils = require("@std/syntax/utils") + +local transformTypes = require("@transform") + +local function transform(ctx: transformTypes.Context) + return query + .findAllFromRoot(ctx.parseresult :: syntax.CstParseResult, 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.CstExprLocal).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.luau b/examples/serve.luau index 828b93f6c..78dba3af5 100644 --- a/examples/serve.luau +++ b/examples/serve.luau @@ -1,13 +1,13 @@ -local net = require("@lute/net") +local server = require("@lute/net/server") print("Starting server...") -- Start the server (non-blocking) -local server = net.serve(function(req) +local instance = server.serve(function(_) return "Hello, lute!" end) -print(`Server listening on http://{server.hostname}:{server.port}`) +print(`Server listening on http://{instance.hostname}:{instance.port}`) print("Program ending, but server will continue running in the background") diff --git a/examples/serve_html.luau b/examples/serve_html.luau index cecccc9da..48ade17e5 100644 --- a/examples/serve_html.luau +++ b/examples/serve_html.luau @@ -1,8 +1,8 @@ -local net = require("@lute/net") +local server = require("@lute/net/server") -local server = net.serve({ +local instance = server.serve({ port = 8080, - handler = function(req) + handler = function(req: server.ReceivedRequest): server.ServerResponse local headers = "" for key, value in req.headers do headers ..= `\n {key}: {value}` @@ -36,4 +36,4 @@ local server = net.serve({ end, }) -print(`Server listening on http://{server.hostname}:{server.port}`) +print(`Server listening on http://{instance.hostname}:{instance.port}`) diff --git a/examples/serve_websocket.luau b/examples/serve_websocket.luau new file mode 100644 index 000000000..543ccaa53 --- /dev/null +++ b/examples/serve_websocket.luau @@ -0,0 +1,43 @@ +local server = require("@lute/net/server") + +print("starting server on ws://127.0.0.1:3000") + +local instance = server.serve({ + hostname = "127.0.0.1", + port = 3000, + handler = function(req: server.ReceivedRequest, instance: server.Server): server.ServerResponse? + if instance:upgrade(req) then + return nil + end + + return { + status = 200, + body = "Hello over HTTP. Try websocket upgrade.", + } + end, + websocket = { + open = function(ws: server.ServerWebSocket) + print("ws open") + ws:send("welcome") + end, + message = function(ws, message) + if type(message) == "buffer" then + print("ws binary message len:", buffer.len(message)) + else + print("ws message:", message) + if message == "close" then + ws:close() + end + end + ws:send(message) + end, + close = function(_ws, code, message) + print("ws close:", code, message) + end, + drain = function(_ws) + print("ws drain") + end, + }, +}) + +print(`listening on {instance.hostname}:{instance.port}`) diff --git a/examples/spawn_example.luau b/examples/spawn_example.luau index 32019a45a..3641819bc 100644 --- a/examples/spawn_example.luau +++ b/examples/spawn_example.luau @@ -15,11 +15,9 @@ print("done inline") do local start = os.clock() - task.await(task.create(function() - print("fib(10):", vm1.fib(10)) - print("fib(20):", vm1.fib(20)) - print("fib(33):", vm1.fib(33)) - end)) + print("fib(10):", vm1.fib(10)) + print("fib(20):", vm1.fib(20)) + print("fib(33):", vm1.fib(33)) print("multiple direct fib in", os.clock() - start) end @@ -27,7 +25,7 @@ end do local start = os.clock() - print("fib(33):", task.await(task.create(vm1.fib, 33))) + print("fib(33):", vm1.fib(33)) print("single task fib in", os.clock() - start) end @@ -35,7 +33,7 @@ end do local start = os.clock() - print("fib(33):", task.await(task.create(vm1.fibTable, { n = 33 }))) + print("fib(33):", vm1.fibTable({ n = 33 })) print("single task fibTable in", os.clock() - start) end @@ -43,9 +41,27 @@ end do local start = os.clock() - local s1, s2, s3 = task.create(vm1.fib, 33), task.create(vm1.fib, 33), task.create(vm1.fib, 33) + local results = {} + local done = 0 - print("fib(33):", task.awaitall(s1, s2, s3)) + task.spawn(function() + results[1] = vm1.fib(33) + done += 1 + end) + task.spawn(function() + results[2] = vm1.fib(33) + done += 1 + end) + task.spawn(function() + results[3] = vm1.fib(33) + done += 1 + end) + + while done < 3 do + task.wait() + end + + print("fib(33):", results[1], results[2], results[3]) print("3 serial fibs in", os.clock() - start) end @@ -53,9 +69,27 @@ end do local start = os.clock() - local p1, p2, p3 = task.create(vm1.fib, 33), task.create(vm2.fib, 33), task.create(vm3.fib, 33) - - print("fib(33):", task.awaitall(p1, p2, p3)) + local results = {} + local done = 0 + + task.spawn(function() + results[1] = vm1.fib(33) + done += 1 + end) + task.spawn(function() + results[2] = vm2.fib(33) + done += 1 + end) + task.spawn(function() + results[3] = vm3.fib(33) + done += 1 + end) + + while done < 3 do + task.wait() + end + + print("fib(33):", results[1], results[2], results[3]) print("3 parallel fibs in", os.clock() - start) end 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..7ee7b8a80 100644 --- a/examples/system_lib.luau +++ b/examples/system_lib.luau @@ -1,19 +1,19 @@ -local system = require("@lute/system") +local system = require("@std/system") print( string.format( "%s (%s-%s) - %d cores, uptime for %ss | freemem: %dMB | totalmem: %dMB", - system.hostname(), + system.hostName(), system.os, system.arch, - system.threadcount(), - system.uptime(), - system.freememory(), - system.totalmemory() + system.threadCount(), + tostring(system.uptime()), + system.freeMemory(), + system.totalMemory() ) ) -for i, cpu in system.cpus() do +for _i, _cpu in system.cpus() do -- print(`[core {string.format("%.02i", i)}] speed: {cpu.speed}, model: {cpu.model}`) -- for key, time in cpu.times do 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-resume.luau b/examples/task-resume.luau index dc245bbc8..7cc22edf0 100644 --- a/examples/task-resume.luau +++ b/examples/task-resume.luau @@ -2,13 +2,14 @@ local task = require("@lute/task") local c = coroutine.create(function() print("hello") - print(coroutine.yield()) + -- We don't know what subsequent calls to coroutine.yield produce. + print(coroutine.yield() :: any) print("wtf") end) task.resume(c) task.resume(c, "world", "meow for good measure") -local t = coroutine.running() +local _ = coroutine.running() task.spawn(print, 3, 2, 1) 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..452c23fe8 --- /dev/null +++ b/examples/testing.luau @@ -0,0 +1,71 @@ +local test = require("@std/test") +local runner = require("@std/test/runner") + +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.eq(table1, table2) + assert.eq(table1, { a = 1 }) + end) + + suite:case("expect_error", function(assert) + local function foo(v: boolean) + if v then + error("error") + end + end + + assert.throws(function() -- demonstrates success (callback throws err) + foo(true) + end) + assert.throws(function() -- demonstrates failure (callback doesn't throw) + foo(false) + end) + end) +end) + +runner.runRegisteredTests() diff --git a/examples/time_instants.luau b/examples/time_instants.luau index 75afd5e2a..ad71d4f45 100644 --- a/examples/time_instants.luau +++ b/examples/time_instants.luau @@ -1,9 +1,12 @@ local time = require("@lute/time") local start = time.now() -for i = 1, 10000 do +for _ = 1, 10000 do vector.dot(vector.one, vector.zero) end local stop = time.now() -print((stop - start):tomicroseconds()) +assert(stop > start) +assert(stop ~= start) +assert(start == start) +print((stop - start):toMicroseconds()) diff --git a/examples/transformee.luau b/examples/transformee.luau new file mode 100644 index 000000000..7c79e57bb --- /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/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..a97889f46 --- /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..5530e6887 --- /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/websocket_echo.luau b/examples/websocket_echo.luau new file mode 100644 index 000000000..0fe4c25c5 --- /dev/null +++ b/examples/websocket_echo.luau @@ -0,0 +1,37 @@ +local client = require("@lute/net/client") + +print("connecting to echo server...") + +local _ws: client.WebSocket? +_ws = client.websocket("wss://echo.websocket.org", { + onopen = function() + print("websocket opened") + if _ws then + _ws:send("hello from lute over websockets") + _ws:send(buffer.create(4)) + _ws:send("close") + end + end, + onmessage = function(message) + if type(message) == "buffer" then + print("received binary message len:", buffer.len(message)) + return + end + + print("received:", message) + if message == "close" then + print("closing...") + if _ws then + _ws:close() + end + end + end, + onclose = function() + print("websocket closed") + end, + onerror = function(err) + print("websocket error:", err) + end, +}) + +print("connected, waiting for messages...") 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/boringssl.tune b/extern/boringssl.tune index ebdc484bb..c599ac9e7 100644 --- a/extern/boringssl.tune +++ b/extern/boringssl.tune @@ -1,5 +1,5 @@ [dependency] name = "boringssl" remote = "https://github.com/google/boringssl.git" -branch = "0.20250415.0" -revision = "2b44a3701a4788e1ef866ddc7f143060a3d196c9" \ No newline at end of file +branch = "0.20260413.0" +revision = "78f7fbeec98a2fb15acea6f7a9f1def7f2e6d9a0" diff --git a/extern/curl.tune b/extern/curl.tune index 45bed6c65..1f6161793 100644 --- a/extern/curl.tune +++ b/extern/curl.tune @@ -1,5 +1,5 @@ [dependency] name = "curl" remote = "https://github.com/curl/curl.git" -branch = "curl-8_13_0" -revision = "1c3149881769e7bd79b072e48374e4c2b3678b2f" +branch = "curl-8_21_0" +revision = "68720b4837284335b2d63cb358f8f6ce65f5bc55" diff --git a/extern/libuv.tune b/extern/libuv.tune index 017020587..ba193948c 100644 --- a/extern/libuv.tune +++ b/extern/libuv.tune @@ -1,5 +1,5 @@ [dependency] name = "libuv" remote = "https://github.com/libuv/libuv.git" -branch = "v1.50.0" -revision = "8fb9cb919489a48880680a56efecff6a7dfb4504" +branch = "v1.51.0" +revision = "5152db2cbfeb5582e9c27c5ea1dba2cd9e10759b" diff --git a/extern/luau.tune b/extern/luau.tune index 7073c6944..7c6d0fb47 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.729" +revision = "6e9b580e2e24643214caf0f4bbbb3db911ca30f3" diff --git a/extern/toml-test.tune b/extern/toml-test.tune new file mode 100644 index 000000000..3a0dd44bd --- /dev/null +++ b/extern/toml-test.tune @@ -0,0 +1,5 @@ +[dependency] +name = "toml-test" +remote = "https://github.com/toml-lang/toml-test.git" +branch = "v2.1.0" +revision = "08ed8697864548b3cdb4b8decbf496bef47e1c82" diff --git a/extern/uWebSockets.tune b/extern/uWebSockets.tune index c9b3e7e3d..00d1f3fcc 100644 --- a/extern/uWebSockets.tune +++ b/extern/uWebSockets.tune @@ -1,5 +1,5 @@ [dependency] name = "uWebSockets" remote = "https://github.com/uNetworking/uWebSockets.git" -branch = "v20.74.0" -revision = "c445faa38125bf782eed3fec97f83b4733c7fb91" +branch = "v20.77.0" +revision = "6b7f3540cec801a7f1cb01141eae8ab989dd4e70" diff --git a/extern/zlib.tune b/extern/zlib.tune index 1d58f261d..b65dd9b2a 100644 --- a/extern/zlib.tune +++ b/extern/zlib.tune @@ -1,5 +1,5 @@ [dependency] name = "zlib" remote = "https://github.com/madler/zlib.git" -branch = "v1.3.1" -revision = "51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf" +branch = "v1.3.2" +revision = "da607da739fa6047df13e66a2af6b8bec7c2a498" diff --git a/foreman.toml b/foreman.toml index 822ead819..01b1a6aa2 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.4.0" } +lute = { github = "luau-lang/lute", version = "=1.0.1-nightly.20260710" } diff --git a/lint.config.luau b/lint.config.luau new file mode 100644 index 000000000..c26c4101d --- /dev/null +++ b/lint.config.luau @@ -0,0 +1,7 @@ +return { + lute = { + lint = { + ignores = { "**/*.snap.luau" }, + }, + }, +} diff --git a/lute.code-workspace b/lute.code-workspace index b1a682196..c754f489c 100644 --- a/lute.code-workspace +++ b/lute.code-workspace @@ -15,5 +15,9 @@ "luau-lsp.fflags.enableNewSolver": true, "luau-lsp.sourcemap.enabled": false, "luau-lsp.platform.type": "standard", + "luau-lsp.fflags.enableByDefault": true, }, + "extensions": { + "recommendations": ["johnnymorganz.luau-lsp"] + } } diff --git a/lute/batteries/CMakeLists.txt b/lute/batteries/CMakeLists.txt new file mode 100644 index 000000000..a9e0aad26 --- /dev/null +++ b/lute/batteries/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(Lute.Batteries STATIC) + +if (LUTE_STDLESS) + target_sources(Lute.Batteries PRIVATE + include/lute/clibatteries.h + src/clibatteries_stub.cpp + ) + target_compile_features(Lute.Batteries PUBLIC cxx_std_17) + target_include_directories(Lute.Batteries PUBLIC include) +else() + target_sources(Lute.Batteries PRIVATE + generated/batteries.h + generated/batteries.cpp + include/lute/clibatteries.h + src/clibatteries.cpp + ) + target_compile_features(Lute.Batteries PUBLIC cxx_std_17) + target_include_directories(Lute.Batteries PUBLIC include generated) +endif() +target_link_libraries(Lute.Batteries PRIVATE uv_a) +target_compile_options(Lute.Batteries PRIVATE ${LUTE_OPTIONS}) + +set(BATTERIES_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${BATTERIES_GENERATED_INCLUDE_DIR}/lute") + diff --git a/lute/batteries/include/lute/clibatteries.h b/lute/batteries/include/lute/clibatteries.h new file mode 100644 index 000000000..55d8ea760 --- /dev/null +++ b/lute/batteries/include/lute/clibatteries.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +enum class BatteryModuleType +{ + Module, + Directory, + NotFound, +}; + +struct BatteryModuleResult +{ + BatteryModuleType type; + std::string_view contents; +}; + +BatteryModuleResult getBatteryModule(std::string_view path); diff --git a/lute/batteries/src/clibatteries.cpp b/lute/batteries/src/clibatteries.cpp new file mode 100644 index 000000000..041b4144b --- /dev/null +++ b/lute/batteries/src/clibatteries.cpp @@ -0,0 +1,23 @@ +#include "lute/clibatteries.h" + +// This file provides batteries for the cli commands + +#include + +#include "batteries.h" + +BatteryModuleResult getBatteryModule(std::string_view path) +{ + for (const auto& [pathInLib, contents] : lutebatteries) + { + if (path != pathInLib) + continue; + + if (contents == "#directory") + return {BatteryModuleType::Directory}; + else + return {BatteryModuleType::Module, contents}; + } + + return {BatteryModuleType::NotFound}; +} diff --git a/lute/batteries/src/clibatteries_stub.cpp b/lute/batteries/src/clibatteries_stub.cpp new file mode 100644 index 000000000..801ae5bbc --- /dev/null +++ b/lute/batteries/src/clibatteries_stub.cpp @@ -0,0 +1,6 @@ +#include "lute/clibatteries.h" + +BatteryModuleResult getBatteryModule(std::string_view) +{ + return {BatteryModuleType::NotFound}; +} diff --git a/lute/cli/.luaurc b/lute/cli/.luaurc new file mode 100644 index 000000000..b63cf2185 --- /dev/null +++ b/lute/cli/.luaurc @@ -0,0 +1,8 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../../batteries", + "lint": "@std/commands/lint/types", + "transform": "@std/commands/transform/types" + } +} diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index b0b8e88e3..215789703 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -1,41 +1,87 @@ add_library(Lute.CLI.Commands STATIC) -target_sources(Lute.CLI.Commands PRIVATE - generated/commands.h - generated/commands.cpp +if (LUTE_STDLESS) + target_sources(Lute.CLI.Commands PRIVATE + include/lute/clicommands.h + src/clicommands_stub.cpp + ) + target_compile_features(Lute.CLI.Commands PUBLIC cxx_std_17) + target_include_directories(Lute.CLI.Commands PUBLIC include) +else() + target_sources(Lute.CLI.Commands PRIVATE + generated/commands.h + generated/commands.cpp + include/lute/clicommands.h + src/clicommands.cpp + ) + target_compile_features(Lute.CLI.Commands PUBLIC cxx_std_17) + target_include_directories(Lute.CLI.Commands PUBLIC include generated) +endif() +target_compile_options(Lute.CLI.Commands PRIVATE ${LUTE_OPTIONS}) - include/lute/clicommands.h - src/clicommands.cpp +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 ) -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}) - 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/luauflags.h + include/lute/packagerun.h + include/lute/profiler.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/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 Lute.Common 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 ) set_target_properties(Lute.CLI PROPERTIES OUTPUT_NAME lute) target_compile_features(Lute.CLI PUBLIC cxx_std_17) -target_link_libraries(Lute.CLI PRIVATE Lute.CLI.lib) +target_link_libraries(Lute.CLI PRIVATE Lute.CLI.lib Lute.Common) target_compile_options(Lute.CLI PRIVATE ${LUTE_OPTIONS}) + +if (LUTE_OPTIMIZE_SIZE AND NOT LUTE_ENABLE_SANITIZERS AND NOT MSVC AND CMAKE_STRIP) + if (APPLE) + # -x keeps dynamic symbols so dladdr backtraces still work. + set(_lute_strip_args -x) + else() + set(_lute_strip_args "") + endif() + add_custom_command(TARGET Lute.CLI POST_BUILD + COMMAND "$,true,${CMAKE_STRIP}>" ${_lute_strip_args} "$" + COMMENT "Stripping symbols from $ (skipped in Debug)" + VERBATIM + ) +endif() diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau new file mode 100644 index 000000000..ddc25bf69 --- /dev/null +++ b/lute/cli/commands/doc/init.luau @@ -0,0 +1,697 @@ +local cli = require("@batteries/cli") +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local process = require("@std/process") +local stringext = require("@std/stringext") + +local USAGE = "Usage: lute doc [OPTIONS] [...MODULE_PATHS]" +local DEFAULT_OUTPUT_DIR = path.resolve(process.cwd(), "docs") + +local STDLIB_MODULE_ALIASES = { + assert = "assertions", + fs = "fslib", + io = "iolib", + process = "processlib", + path = "pathlib", + system = "systemlib", + trivia = "retrieverLib", + visitor = "visitorlib", + query = "queryLib", + printer = "printerLib", + failure = "failures", +} + +local tripleDashCommentPattern = "^%s*%-%-%-%s?(.*)$" +local blockCommentStartPattern = "^%s*%-%-%[(=+)%[" +local whitespacePattern = "^%s*$" +local exportTypePattern = "^%s*export%s+type%s+([%w_]+)%s*=(.*)$" + +type PropertySignature = { + name: string?, + signature: string?, + doc: string?, + isType: boolean?, + pendingDoc: { string }, + inBlock: boolean, + blockEq: string?, + -- multiline export type accumulation state + inTypeDef: boolean?, + typeDefName: string?, + typeDefLines: { string }?, + typeDefDepth: number?, + typeDefDoc: { string }?, +} + +type Mapping = { + [string]: { alias: string, destination: string }, +} + +local function printHelp() + print(USAGE) + print([[ +Generate Markdown documentation for Luau module(s). + +OPTIONS: + -h, --help Show this help message + -o, --output=DIR Output directory for generated docs (default: ./docs in current working directory) + +MODULE_PATHS: + Path(s) to Luau module(s) to generate documentation for. Only files with .luau or .lua extensions will be processed. + Paths can be absolute or relative to the current working directory. + +EXAMPLES: + lute doc definitions Generate docs for definitions module to the default directory ./docs + lute doc --output ./my-docs-folder definitions Generate docs for definitions module to custom directory ./my-docs-folder + lute doc definitions lute/std/libs Generate docs for specific modules located at ./definitions and ./lute/std/libs + lute doc ./absolute/module/path Generate docs for module at the specified absolute path +]]) +end + +-- 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) +-- inTypeDef (are we currently accumulating a multiline export type?) +-- typeDefName (the name of the type being accumulated) +-- typeDefLines (accumulated lines of the type definition) +-- typeDefDepth (current brace depth inside the type) +-- typeDefDoc (saved pendingDoc to attach once the type is complete) +local function extractPropertySignature( + module: string, + line: string, + pendingDoc: { string }, + inBlock: boolean, + blockEq: string?, + inTypeDef: boolean?, + typeDefName: string?, + typeDefLines: { string }?, + typeDefDepth: number?, + typeDefDoc: { string }? +): PropertySignature + -- 1) If we're accumulating a multiline export type, keep collecting until braces balance + if inTypeDef then + local tLines = typeDefLines or {} + local depth = typeDefDepth or 0 + local name = typeDefName or "" + local savedDoc = typeDefDoc or {} + + table.insert(tLines, line) + + for char in string.gmatch(line, ".") do + if char == "{" then + depth += 1 + elseif char == "}" then + depth -= 1 + end + end + + if depth <= 0 then + return { + name = name, + signature = table.concat(tLines, "\n"), + doc = if #savedDoc > 0 then table.concat(savedDoc, "\n\n") else nil, + isType = true, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + inTypeDef = false, + typeDefName = nil, + typeDefLines = {}, + typeDefDepth = 0, + typeDefDoc = nil, + } + else + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + inTypeDef = true, + typeDefName = name, + typeDefLines = tLines, + typeDefDepth = depth, + typeDefDoc = savedDoc, + } + end + end + + -- 2) 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 + + -- 3) 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 + + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 4) 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 + + -- 5) Export type declaration (exported types only; unexported types are internal) + local typeName, typeRest = string.match(line, exportTypePattern) + if typeName then + local trimmedRest = stringext.trim(typeRest) + + local depth = 0 + for char in string.gmatch(trimmedRest, ".") do + if char == "{" then + depth += 1 + elseif char == "}" then + depth -= 1 + end + end + + local firstTypeLine = `type {typeName} = {trimmedRest}` + + if depth <= 0 then + -- Single-line or already-balanced type + return { + name = typeName, + signature = firstTypeLine, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + isType = true, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + inTypeDef = false, + typeDefLines = {}, + typeDefDepth = 0, + } + else + -- Multiline type — begin accumulation, save pending doc for later + return { + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + inTypeDef = true, + typeDefName = typeName, + typeDefLines = { firstTypeLine }, + typeDefDepth = depth, + typeDefDoc = pendingDoc, + } + end + end + + -- 6) 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 { + name = propName, + signature = propType, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 7) 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 + + return { + name = fullName, + signature = `{params} -> {returnType}`, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 8) 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 + + -- nothing found on this line + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } +end + +type Definition = { name: string, signature: string, doc: string? } +type TypeDefinition = { name: string, signature: string, doc: string? } + +type ParseResult = { + definitions: { Definition }, + typeDefinitions: { TypeDefinition }, +} + +local function parseModule(module: path.Pathlike, filePath: path.Pathlike): ParseResult + local content = fs.readFileToString(filePath) + local lines = string.split(content, "\n") + + local moduleDefinitions: { Definition } = {} + local typeDefinitions: { TypeDefinition } = {} + local pendingDoc: { string } = {} + local inBlock = false + local blockEq = nil + local inTypeDef = false + local typeDefName = nil + local typeDefLines: { string } = {} + local typeDefDepth = 0 + local typeDefDoc: { string } = {} + + for _, line in lines do + local ps = extractPropertySignature( + tostring(module), + line, + pendingDoc, + inBlock, + blockEq, + inTypeDef, + typeDefName, + typeDefLines, + typeDefDepth, + typeDefDoc + ) + + -- update state carried across lines + pendingDoc = ps.pendingDoc + inBlock = ps.inBlock + blockEq = ps.blockEq + inTypeDef = ps.inTypeDef or false + typeDefName = ps.typeDefName + typeDefLines = ps.typeDefLines or {} + typeDefDepth = ps.typeDefDepth or 0 + typeDefDoc = ps.typeDefDoc or {} + + if ps.name and ps.signature then + if ps.isType then + table.insert(typeDefinitions, { + name = ps.name, + signature = ps.signature, + doc = ps.doc, + }) + else + table.insert(moduleDefinitions, { + name = ps.name, + signature = ps.signature, + doc = ps.doc, + }) + end + end + end + + return { + definitions = moduleDefinitions, + typeDefinitions = typeDefinitions, + } +end + +-- Returns the first line of a doc string, for use in summary tables. +local function firstLine(doc: string?): string + if not doc then + return "" + end + return string.match(doc, "^([^\n]+)") or doc +end + +-- Generates a GitHub-compatible anchor from a heading string. +local function toAnchor(text: string): string + text = string.lower(text) + text = string.gsub(text, "[^%w%-]", "") + return text +end + +local function generateMarkdown( + moduleName: string, + definitions: { Definition }, + typeDefinitions: { TypeDefinition }, + 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 + + -- LUAUFIX: we should not need to explicitly instantiate the type of `table.sort` here + table.sort<>(definitions, function(a, b) + return a.name < b.name + end) + table.sort<>(typeDefinitions, function(a, b) + return a.name < b.name + end) + + local lines = { + `# {moduleName}`, + "", + "```luau", + `local {moduleName} = require("@{requireLibraryAlias}/{requirePath}")`, + "```", + ":::warning", + "These APIs are still open to future evolution. In new major versions, they may change in backwards incompatible ways.", + ":::", + "", + } + + -- Summary table + if #definitions > 0 or #typeDefinitions > 0 then + table.insert(lines, "## Summary") + table.insert(lines, "") + table.insert(lines, "| Entry | Description |") + table.insert(lines, "| :--- | :--- |") + + for _, typeDef in typeDefinitions do + local anchor = toAnchor(typeDef.name) + table.insert(lines, `| [{typeDef.name}](#{anchor}) | {firstLine(typeDef.doc)} |`) + end + + for _, def in definitions do + local anchor = toAnchor(`{moduleName}.{def.name}`) + table.insert(lines, `| [{def.name}](#{anchor}) | {firstLine(def.doc)} |`) + end + + table.insert(lines, "") + table.insert(lines, "---") + table.insert(lines, "") + end + + -- Types section + if #typeDefinitions > 0 then + table.insert(lines, "## Types") + table.insert(lines, "") + + for _, typeDef in typeDefinitions do + table.insert(lines, `### {typeDef.name}`) + table.insert(lines, "") + if typeDef.doc then + table.insert(lines, typeDef.doc) + table.insert(lines, "") + end + table.insert(lines, "```luau") + table.insert(lines, typeDef.signature) + table.insert(lines, "```") + table.insert(lines, "") + table.insert(lines, "---") + table.insert(lines, "") + end + end + + -- Functions and Properties section + if #definitions > 0 then + table.insert(lines, "## Functions and Properties") + table.insert(lines, "") + + for _, def in definitions do + table.insert(lines, `### {moduleName}.{def.name}`) + table.insert(lines, "") + if def.doc then + table.insert(lines, def.doc) + table.insert(lines, "") + end + table.insert(lines, "```luau") + table.insert(lines, def.signature) + table.insert(lines, "```") + table.insert(lines, "") + table.insert(lines, "---") + table.insert(lines, "") + end + end + + return table.concat(lines, "\n") +end + +local function processDirectory( + modulePath: path.Pathlike, + documentationPath: path.Pathlike, + libraryPath: path.Pathlike, + requireLibraryAlias: string +) + fs.createDirectory(documentationPath, { makeParents = true }) + + 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 result = parseModule(moduleAlias, entryPath) + local markdown = generateMarkdown( + moduleName, + result.definitions, + result.typeDefinitions, + entryPath, + libraryPath, + requireLibraryAlias + ) + + fs.writeStringToFile(documentationFilePath, markdown) + + print(`Generated {tostring(documentationFilePath)}`) + end + end +end + +type Module = { name: string, isDir: boolean } + +-- 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: { Module } = {} + + 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 + + -- LUAUFIX: we should not need to explicitly instantiate the type of `table.sort` + 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 function generateDocsFromMapping(mapping: Mapping): () + for sourcePath, info in pairs(mapping) do + local absSourcePath = if path.isAbsolute(sourcePath) + then path.parse(sourcePath) + else path.resolve(process.cwd(), sourcePath) + + if not fs.exists(absSourcePath) then + print(`Warning: Source path does not exist: {tostring(absSourcePath)}`) + continue + end + + local destinationPath = if path.isAbsolute(info.destination) + then path.parse(info.destination) + else path.resolve(process.cwd(), info.destination) + local alias = info.alias + + print( + `Generating docs for {tostring(absSourcePath)} with alias @{alias} to destination {tostring(destinationPath)}...` + ) + processDirectory(absSourcePath, destinationPath, absSourcePath, alias) + print(`Completed generating docs for {tostring(absSourcePath)}.`) + end +end + +local function generateAllDocs(outputDir: path.Path, modulePaths: { string }): () + -- Create top-level reference index page + local referenceBasePath = path.join(outputDir, "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. +]] + + -- Generate docs for each specified module path + for _, modulePath in modulePaths do + local modulePathObj = path.parse(modulePath) + local absModulePath = if path.isAbsolute(modulePathObj) + then modulePathObj + else path.resolve(process.cwd(), modulePathObj) + + -- Explicitly match the aliases for lute and std, otherwise use the last part of the path as the alias + local libraryAlias = absModulePath.parts[#absModulePath.parts - 1] + if string.match(tostring(absModulePath), "/lute/std/libs") then + libraryAlias = "std" + elseif string.match(tostring(absModulePath), "/definitions") then + libraryAlias = "lute" + end + + local documentationPath = path.join(referenceBasePath, libraryAlias) + processDirectory(absModulePath, documentationPath, absModulePath, libraryAlias) + + -- Generate module index with table of children + local moduleIndex = generateLibraryIndex(libraryAlias, libraryAlias, absModulePath) + fs.writeStringToFile(path.join(documentationPath, "index.md"), moduleIndex) + + -- Add to the reference index with the correct alias + referenceIndex ..= "\n\n- [" .. libraryAlias .. "](./" .. libraryAlias .. "/index.md)" + + -- Add the existing hard-coded info for lute and std + if libraryAlias == "lute" then + referenceIndex ..= " - Core runtime library (`@lute/*`)" + elseif libraryAlias == "std" then + referenceIndex ..= " - Standard library modules (`@std/*`)" + end + end + + fs.writeStringToFile(path.join(referenceBasePath, "index.md"), referenceIndex) +end + +local function main(...: string) + local args = cli.parser() + + args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) + args:add("mapping", "option", { + help = "Custom mapping for generating documentation", + aliases = { "m" }, + }) + args:add( + "output", + "option", + { help = "Output directory for generated docs", default = tostring(DEFAULT_OUTPUT_DIR), aliases = { "o" } } + ) + + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local outputDir = path.parse(args:get("output") or tostring(DEFAULT_OUTPUT_DIR)) + if not path.isAbsolute(outputDir) then + outputDir = path.resolve(process.cwd(), outputDir) + end + + local mapFile = args:get("mapping") + if mapFile then + local mapPath = path.join(process.cwd(), mapFile) + local mod = luau.loadModule(mapPath) :: Mapping + print(`Using custom mapping from file {tostring(mapPath)}`) + generateDocsFromMapping(mod) + return + end + + local modulePaths = args:forwarded() + assert( + modulePaths ~= nil and #modulePaths > 0, + "Error: No module paths specified.\n\n" .. USAGE .. "\n\nUse --help for more information." + ) + + print("Generating documentation...") + generateAllDocs(outputDir, modulePaths) + print("Documentation generation complete! Files written to: ", tostring(outputDir)) +end + +main(...) diff --git a/lute/cli/commands/helloworld/helloworld.luau b/lute/cli/commands/helloworld/helloworld.luau deleted file mode 100644 index 6312118cf..000000000 --- a/lute/cli/commands/helloworld/helloworld.luau +++ /dev/null @@ -1 +0,0 @@ -return { "Hello, world!" } diff --git a/lute/cli/commands/helloworld/init.luau b/lute/cli/commands/helloworld/init.luau deleted file mode 100644 index d412b61e2..000000000 --- a/lute/cli/commands/helloworld/init.luau +++ /dev/null @@ -1,2 +0,0 @@ -local result = require("@self/helloworld") -print(result[1]) diff --git a/lute/cli/commands/lib/files.luau b/lute/cli/commands/lib/files.luau new file mode 100644 index 000000000..d682956d9 --- /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..15bd0be08 --- /dev/null +++ b/lute/cli/commands/lib/ignore.luau @@ -0,0 +1,83 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local parseIgnores = require("./parseIgnores") +local isIgnored = parseIgnores.isIgnored +local parseGitignore = parseIgnores.parseGitignore + +--- Performs ignore resolution on a set of +local IgnoreResolver = {} +IgnoreResolver.__index = IgnoreResolver + +type Glob = parseIgnores.Glob + +type GitignoreData = parseIgnores.GitignoreData + +type IgnoreResolverData = { + _ignoreCache: { [string]: GitignoreData }, +} + +export type IgnoreResolver = setmetatable + +function IgnoreResolver.new(): IgnoreResolver + local self = {} + + self._ignoreCache = {} + + return setmetatable(self, IgnoreResolver) +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/lib/parseIgnores.luau b/lute/cli/commands/lib/parseIgnores.luau new file mode 100644 index 000000000..2dc3c79dd --- /dev/null +++ b/lute/cli/commands/lib/parseIgnores.luau @@ -0,0 +1,190 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local system = require("@std/system") + +export type Glob = { pattern: string, onlyMatchDirectories: boolean, matchAnywhere: boolean } + +export type GitignoreData = { + location: string, + ignores: { Glob }, + whitelists: { Glob }, + next: GitignoreData?, +} + +local separator = if system.win32 then "\\" else "/" + +local parseIgnores = {} + +--- 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 + +function parseIgnores.parseIgnoreContents(location: string, contents: string | { string }): GitignoreData + local lines = if typeof(contents) == "string" then contents:split("\n") else contents + + 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 + +function parseIgnores.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 parseIgnores.parseIgnoreContents(parentDirectory, contents) +end + +function parseIgnores.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 + +function parseIgnores.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 parseIgnores.matchesGlob(ignore, relativePath, isDirectory) then + matched = true + ignored = true + break + end + end + + if ignored then + for _, whitelist in ignoreData.whitelists do + if parseIgnores.matchesGlob(whitelist, filepath, isDirectory) then + matched = true + ignored = false + break + end + end + end + + if not matched and ignoreData.next then + return parseIgnores.isIgnored(ignoreData.next, filepath, isDirectory) + end + + return ignored +end + +return table.freeze(parseIgnores) diff --git a/lute/cli/commands/lib/typedefs.luau b/lute/cli/commands/lib/typedefs.luau new file mode 100644 index 000000000..764cfe3e1 --- /dev/null +++ b/lute/cli/commands/lib/typedefs.luau @@ -0,0 +1,113 @@ +local path = require("@std/path") +local process = require("@std/process") + +type version = { version: string, versionSuffix: string, versionFull: string } +local version: version = _G["version"] + +local TYPEDEFS_VERSION = version.version +local TYPEDEFS_BASE_DIR = `.lute/typedefs/{TYPEDEFS_VERSION}` +local TYPEDEFS_RELATIVE_DIR = `~/{TYPEDEFS_BASE_DIR}` +local TYPEDEFS_PATH = path.join(process.homedir(), TYPEDEFS_BASE_DIR) + +local TEMPLATE_RC_FILE: { [any]: any } = { + aliases = { + lute = `{TYPEDEFS_RELATIVE_DIR}/lute`, + std = `{TYPEDEFS_RELATIVE_DIR}/std`, + lint = "@std/commands/lint/types", + transform = "@std/commands/transform/types", + }, +} + +local function indent(level: number) + return string.rep("\t", level) +end + +local function nest(src, parenIndent) + return `\{\n{src}\n{parenIndent}\}` +end + +local function kv(k, v, desc: string?) + local kvpair = `{k} = {v}` + if desc then + kvpair = kvpair .. `, -- {desc}` + end + return kvpair +end + +local function tbl(fields: { { value: string, description: string } } | { string }, level) + local buf: { string } = {} + local fieldIndent = indent(level + 1) + local parenIndent = indent(level) + for _, f in fields do + if typeof(f) == "string" then + table.insert(buf, `{fieldIndent}{f},`) + else + table.insert(buf, `{fieldIndent}-- {f.description}`) + table.insert(buf, `{fieldIndent}{f.value},`) + end + end + + return nest(table.concat(buf, "\n"), parenIndent) +end + +local function generateLuauConfig(languageMode: "strict" | "nonstrict"): string + local aliases = { + kv("lute", `"{TYPEDEFS_RELATIVE_DIR}/lute"`), + kv("std", `"{TYPEDEFS_RELATIVE_DIR}/std"`), + kv("lint", `"{TYPEDEFS_RELATIVE_DIR}/lint"`), + } + local aliasTable = tbl(aliases, 2) + local luau = tbl({ + { + value = kv("languagemode", `"{languageMode}"`), + description = "Default language mode to use for typechecking", + }, + { value = kv("aliases", aliasTable), description = "Aliases for builtin libraries" }, + }, 1) + return luau +end + +local function generateLuteConfig(): string + local ruleconfigs = tbl({ + `-- [RuleName] : \{`, + `-- Array of globbed strings, representing paths exempt from this rule`, + `-- ignores: \{string\}?`, + `-- Override a rule's default severity`, + `-- severity: ("warn" | "error" | "info" | "hint")?`, + `-- Pass custom options through to a rule`, + `-- options: \{ [string]: unknown \}?`, + `-- Disable this rule`, + `-- off: boolean?`, + `-- \}`, + }, 3) + local lint = tbl({ + { + value = kv("ignores", "{}"), + description = "Array of globbed strings, representing paths to exempt from linting", + }, + { value = kv("globals", "{}"), description = "Global values passed to each linting rule" }, + { value = kv("rulepaths", "{}"), description = "Array of strings from which to load local lint rules" }, + { + value = kv("ruleconfigs", ruleconfigs), + description = "Per rule overrides, with schema [RuleName] : {...}, where the schema is described below ", + }, + }, 2) + local lute = tbl({ kv("lint", lint) }, 1) + return lute +end + +local typedefs = {} + +typedefs.TYPEDEFS_PATH = TYPEDEFS_PATH +typedefs.TYPEDEFS_RELATIVE_DIR = TYPEDEFS_RELATIVE_DIR +typedefs.LUAURC_TEMPLATE = TEMPLATE_RC_FILE + +function typedefs.generateConfigDotLuau(languageMode: "strict" | "nonstrict"): string + local luauConfig = generateLuauConfig(languageMode) + local luteConfig = generateLuteConfig() + + local configDotLuau = "return " .. tbl({ kv("lute", luteConfig), kv("luau", luauConfig) }, 0) + return configDotLuau +end + +return table.freeze(typedefs) 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/configutils.luau b/lute/cli/commands/lint/configutils.luau new file mode 100644 index 000000000..40e473edf --- /dev/null +++ b/lute/cli/commands/lint/configutils.luau @@ -0,0 +1,66 @@ +local internalTypes = require("./internaltypes") + +local function assertTableStructure(tbl: { [unknown]: unknown }, keytype: string, valuetype: string?, err: string) + for k, v in tbl do + assert(typeof(k) == keytype, err) + if valuetype then + assert(typeof(v) == valuetype, err) + end + end +end + +local function assertIsArray(tbl: { [unknown]: unknown }, valuetype: string, err: string) + return assertTableStructure(tbl, "number", valuetype, err) +end + +-- we expect .config.luau file structured s.t top level table has: table.lute.lint field +local function assertValidConfig(candidate: any) + if candidate.ignores then + assert(typeof(candidate.ignores) == "table", "config.lute.lint.ignores must be an array of filepath globs") + assertIsArray(candidate.ignores, "string", "config.lute.lint.ignores must be an array of filepath globs") + end + + if candidate.globals then + assert(typeof(candidate.globals) == "table", "config.lute.lint.globals must be a table with string keys") + assertTableStructure( + candidate.globals, + "string", + nil, + "config.lute.lint.globals must be a table with string keys" + ) + end + + if candidate.rulepaths then + assert(typeof(candidate.rulepaths) == "table", "config.lute.lint.rulepaths must be an array of filepath globs") + assertIsArray(candidate.rulepaths, "string", "config.lute.lint.rulepaths must be an array of filepath globs") + end + + if candidate.ruleconfigs then + assert( + typeof(candidate.ruleconfigs) == "table", + "config.lute.lint.ruleconfigs must be a table with string keys" + ) + assertTableStructure( + candidate.ruleconfigs, + "string", + nil, + "config.lute.lint.ruleconfigs must be a table with string keys" + ) + end +end + +local function extractConfig(candidate: any): internalTypes.LintConfig + if candidate.lute == nil or candidate.lute.lint == nil then + return {} + end + + local luteLintConfig = candidate.lute.lint + + assertValidConfig(luteLintConfig) + + return luteLintConfig :: internalTypes.LintConfig +end + +return { + extractConfig = extractConfig, +} diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau new file mode 100644 index 000000000..750013603 --- /dev/null +++ b/lute/cli/commands/lint/init.luau @@ -0,0 +1,203 @@ +local cli = require("@batteries/cli") +local files = require("./lib/files") +local fs = require("@std/fs") +local internalTypes = require("@self/internaltypes") +local json = require("@std/json") +local lint = require("@self/lint") +local lintCore = require("@self/lintCore") +local lsp = require("@self/lsp") +local parseIgnores = require("./lib/parseIgnores") +local pathLib = require("@std/path") +local printer = require("@self/printer") +local process = require("@std/process") +local tableext = require("@std/tableext") +local types = require("@lint") + +local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" +local VERBOSE = false +local SEQUENTIAL = false + +local function printHelp() + print(USAGE) + -- Ordering of options: help + verbose fist, then options with aliases in alphabetical order, then options without aliases in alphabetical order + 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 + -c, --config [PATH] Path to configuration file. By default, lute lint will look for a + the .config.luau file in the calling directory. + -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. + -o, --output [PATH] Write lint violation or JSON output to the specified file instead of stdout. + -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. + -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. + --no-default-lints Disables the default lint rules. + --sequential Lint files sequentially. Files are linted in parallel by default. + +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 lintString = lintCore.lintString + +type Job = { + inputFile: pathLib.Path, + autoFixEnabled: boolean, +} + +local function lintPaths( + inputFilePaths: { string }, + lintRules: { types.LintRule }, + configData: internalTypes.RuleConfigData, + rulesPath: string?, + noDefaults: boolean, + passedConfigVal: string?, + autofixEnabled: boolean, + ignoreData: parseIgnores.GitignoreData +): { [pathLib.Path]: { types.LintViolation } } + if VERBOSE then + print("Filtering input files by gitignore") + end + local inputFiles = files.getSourceFiles(inputFilePaths, VERBOSE) + + if SEQUENTIAL then + return lint.lintSequential(inputFiles, lintRules, configData, autofixEnabled, ignoreData, VERBOSE) + end + + return lint.lintParallel(inputFiles, rulesPath, noDefaults, passedConfigVal, autofixEnabled, ignoreData, VERBOSE) +end + +local function main(...: string) + local args = cli.parser() + + args:add( + "auto-fix", + "flag", + { help = "Automatically apply fixes for lint violations that provide a suggested fix" } + ) + args:add("config", "option", { help = "Path to configuration file", aliases = { "c" } }) + args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) + args:add("json", "flag", { help = "Output lint violations in JSON format", aliases = { "j" } }) + args:add("no-default-lints", "flag", { help = "Disables the default lint rules" }) + args:add("output", "option", { help = "Write diagnostics output to file", aliases = { "o" } }) + args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) + args:add( + "string-input", + "option", + { help = "Lint the provided string input instead of reading from files", aliases = { "s" } } + ) + args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) + args:add("sequential", "flag", { help = "Lint files sequentially (parallel by default)" }) + + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + VERBOSE = args:has("verbose") + SEQUENTIAL = args:has("sequential") + + local inputFiles = args:forwarded() + local stringInput = args:get("string-input") + if (inputFiles == nil or #inputFiles == 0) and not stringInput then + inputFiles = { pathLib.format(process.cwd()) } + end + + local passedConfigVal = args:get("config") + local noDefaultLints = args:has("no-default-lints") + local rulesPath = args:get("rules") + local outputFile = args:get("output") + + local function output(content: string) + if outputFile then + fs.writeStringToFile(outputFile, content) + else + print(content) + end + end + + local lintRules, configContextData, ignoreData = + lintCore.initialize(passedConfigVal, noDefaultLints, rulesPath, VERBOSE) + + 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, nil, configContextData, VERBOSE) + + if args:has("json") then + local diagnostics = tableext.map(violations, lsp.diagnostic) + output(json.serialize(diagnostics :: json.Array, true)) + elseif #violations > 0 then + if VERBOSE then + print(`Printing violations from string input\n`) + end + + output(printer.formatLintsWithSource(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, + configContextData, + rulesPath, + noDefaultLints, + passedConfigVal, + autofixEnabled, + ignoreData + ) + + if args:has("json") then + output(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.Object, true)) + else + if VERBOSE then + print(`Printing violations from {#allViolations} files\n`) + end + + local violationsFound = false + local parts = {} + + for sourcePath, violations in allViolations do + if #violations > 0 then + violationsFound = true + table.insert(parts, printer.formatLintsWithPath(violations, sourcePath)) + end + end + + if #parts > 0 then + output(table.concat(parts)) + end + + process.exit(if violationsFound then 1 else 0) + end + end +end + +main(...) diff --git a/lute/cli/commands/lint/internaltypes.luau b/lute/cli/commands/lint/internaltypes.luau new file mode 100644 index 000000000..dbbd8a19a --- /dev/null +++ b/lute/cli/commands/lint/internaltypes.luau @@ -0,0 +1,37 @@ +local types = require("@lint") +local parseIgnores = require("../lib/parseIgnores") +local path = require("@std/path") +local syntax = require("@std/syntax") + +export type RuleConfig = { + ignores: { string }?, + severity: types.severity?, + off: true?, + options: types.RuleOptions?, +} + +export type LintConfig = { + ignores: { string }?, -- globs/paths to ignore as we walk lintee files + globals: types.GlobalsConfig?, -- globals to pass down to all rules via context + rulepaths: { path.Pathlike }?, -- paths to local rules to use + ruleconfigs: { + [string]: RuleConfig?, + }?, +} + +export type RuleConfigurations = { [string]: RuleConfig } + +export type RuleIgnores = { [string]: parseIgnores.GitignoreData } + +export type RuleConfigData = { + globals: types.GlobalsConfig, + ruleConfigs: RuleConfigurations, + ruleIgnores: RuleIgnores, +} + +export type fix = { + location: syntax.Span, + replacement: string, +} + +return {} diff --git a/lute/cli/commands/lint/lint.luau b/lute/cli/commands/lint/lint.luau new file mode 100644 index 000000000..f88fd0f67 --- /dev/null +++ b/lute/cli/commands/lint/lint.luau @@ -0,0 +1,132 @@ +local fs = require("@std/fs") +local internalTypes = require("./internaltypes") +local lintCore = require("./lintCore") +local parseIgnores = require("../lib/parseIgnores") +local pathLib = require("@std/path") +local system = require("@std/system") +local task = require("@std/task") +local types = require("@lint") +local vm = require("@lute/vm") + +local lint = {} + +function lint.lintSequential( + inputFiles: { pathLib.Path }, + lintRules: { types.LintRule }, + configData: internalTypes.RuleConfigData, + autofixEnabled: boolean, + ignoreData: parseIgnores.GitignoreData, + verbose: boolean +) + local allViolations = {} + local hasIgnores = #ignoreData.ignores > 0 + for _, inputPath in inputFiles do + local walker = fs.walk(inputPath, { recursive = true }) + + local curr = walker() + while curr ~= nil do + if hasIgnores then + local inputFilePathString = pathLib.format(curr) + + if parseIgnores.isIgnored(ignoreData, inputFilePathString, fs.type(inputFilePathString) == "dir") then + curr = walker() + continue + end + end + + local success, err = pcall(lintCore.lintFile, curr, lintRules, autofixEnabled, configData, verbose) + if success then + -- Violations are returned as the second return value from pcall + allViolations[curr] = err + else + print(`Error linting file '{curr}': {err}`) + end + + curr = walker() + end + end + return allViolations +end + +type Job = { + inputFile: pathLib.Path, + autoFixEnabled: boolean, +} + +function lint.lintParallel( + inputFiles: { pathLib.Path }, + rulesPath: string?, + noDefaults: boolean, + passedConfigVal: string?, + autofixEnabled: boolean, + ignoreData: parseIgnores.GitignoreData, + verbose: boolean +) + local allViolations = {} + local hasIgnores = #ignoreData.ignores > 0 + local maxWorkers = math.min(system.threadCount(), #inputFiles) + local curJob = 1 + local doneWorkers = 0 + + local function lintNext(): Job? + if curJob > #inputFiles then + return nil + end + + local nextJob: Job = { + inputFile = inputFiles[curJob], + autoFixEnabled = autofixEnabled, + } + + curJob += 1 + + return nextJob + end + + local function spawnWorker() + task.spawn(function() + xpcall(function() + local worker = vm.create("./worker") + -- Initialize will be UNNECESSARY once we can pass functions to separate VM + -- Once that is the case: + -- remove initialize + -- refactor to omit rulesPath / passedConfigVal and instead pass lintRules / configData directly to lintFile + worker.initialize(rulesPath, noDefaults, passedConfigVal, verbose) + + local activeJob = lintNext() + while activeJob ~= nil do + local job = activeJob :: Job + local inputFilePathString = pathLib.format(activeJob.inputFile) + local isIgnored = hasIgnores and parseIgnores.isIgnored(ignoreData, inputFilePathString, false) + if not isIgnored then + local success, res = pcall(function() + return worker.lintFile(inputFilePathString, job.autoFixEnabled, verbose) + end) + if success then + allViolations[activeJob.inputFile] = res :: { types.LintViolation } + else + print(`(worker) Error linting file '{activeJob.inputFile}': {res}`) + end + end + activeJob = lintNext() + end + end, function(err: any) + print(`worker errored: {tostring(err)}`) + end) + + doneWorkers += 1 + end) + end + + for _ = 1, maxWorkers do + spawnWorker() + end + + while doneWorkers < maxWorkers do + task.wait() -- wait for all jobs to run + end + + return allViolations +end + +return table.freeze(lint) diff --git a/lute/cli/commands/lint/lintCore.luau b/lute/cli/commands/lint/lintCore.luau new file mode 100644 index 000000000..ab5744683 --- /dev/null +++ b/lute/cli/commands/lint/lintCore.luau @@ -0,0 +1,473 @@ +local configUtils = require("./configutils") +local fs = require("@std/fs") +local internalTypes = require("./internaltypes") +local luau = require("@std/luau") +local parseIgnores = require("../lib/parseIgnores") +local pathLib = require("@std/path") +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("@lint") +local visitor = require("@std/syntax/visitor") + +local DEFAULT_RULES = { + "almost_swapped", + "constant_table_comparison", + "divide_by_zero", + "duplicate_keys", + "empty_if_block", + "global_function_in_scope", + "parenthesized_conditions", + "unused_variable", +} + +local function isLintRule(rule: unknown, path: string, VERBOSE: boolean): boolean + if not rule then + if VERBOSE then + print(`Error loading lint rule from {path}: failed to require`) + end + return false + end + + if typeof(rule) ~= "table" then + if VERBOSE then + print(`Error loading lint rule from {path}: must return a table`) + end + return false + end + + if typeof(rule.name) ~= "string" then + if VERBOSE then + print(`Error loading lint rule from {path}: must return a table with a 'name' string property`) + end + return false + end + + if typeof((rule :: { lint: unknown }).lint) ~= "function" then -- LUAUFIX: rule got refined to { read name : string } so accessing prop lint errors + if VERBOSE then + print(`Error loading lint rule from {path}: must return a table with a 'lint' function property`) + end + return false + end + + return true +end + +local function loadLintRule(path: string, VERBOSE: boolean): types.LintRule? + local loaded = luau.loadModule(path) + return if isLintRule(loaded, path, VERBOSE) then loaded else nil -- Typecast isn't needed because loaded is refined to any bc of type error in isLintRule +end + +local function registerRule( + rule: types.LintRule, + config: internalTypes.LintConfig, + rootLocation: string, + rules: { types.LintRule }, + ruleConfigs: internalTypes.RuleConfigurations, + ruleIgnores: internalTypes.RuleIgnores +) + local ruleConfig: internalTypes.RuleConfig = if config.ruleconfigs ~= nil + then config.ruleconfigs[rule.name] or {} + else {} + if ruleConfig.off then + return + end + + table.insert(rules, rule) + ruleConfigs[rule.name] = ruleConfig + if ruleConfig.ignores ~= nil then + ruleIgnores[rule.name] = parseIgnores.parseIgnoreContents(rootLocation, ruleConfig.ignores) + end +end + +local function loadLintRules( + path: string, + config: internalTypes.LintConfig, + rootLocation: string, + VERBOSE: boolean +): ({ types.LintRule }, internalTypes.RuleConfigurations, internalTypes.RuleIgnores) + assert(fs.exists(path), `Lint rule at path '{path}' does not exist`) + + local rules, ruleConfigs: internalTypes.RuleConfigurations, ruleIgnores = {}, {}, {} + + 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 + local rule = loadLintRule(initPath, VERBOSE) + if rule then + registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) + end + 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 + local rule = loadLintRule(currentPath, VERBOSE) + if rule then + registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) + end + end + end + + return rules, ruleConfigs, ruleIgnores +end + +local function loadDefaultRules( + config: internalTypes.LintConfig, + rootLocation: string, + VERBOSE: boolean +): ({ types.LintRule }, internalTypes.RuleConfigurations, internalTypes.RuleIgnores) + local rules, ruleConfigs, ruleIgnores = {}, {}, {} + for _, ruleName in DEFAULT_RULES do + local path = `./rules/{ruleName}` + local rule: types.LintRule = require(path) :: any -- Anycast to silence error from dynamic require + assert(isLintRule(rule, path, VERBOSE), `Invalid default lint rule: {path}`) + registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) + end + return rules, ruleConfigs, ruleIgnores +end + +local function parseDirectives(node: syntax.CstNode): { [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.CstNode, + cache: { [syntax.CstNode]: { [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, + cst: syntax.CstStatBlock, + directiveCache: { [syntax.CstNode]: { [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.CstNode): 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) + + -- CstStatBlock 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(cst, 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 + +type fix = internalTypes.fix + +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 lintCore = {} + +function lintCore.loadConfig(configPath: pathLib.Pathlike, VERBOSE: boolean): internalTypes.LintConfig + local lintConfig: internalTypes.LintConfig = {} + + if fs.exists(configPath) then + local success, loadedConfig = pcall(luau.loadModule, configPath, nil) + if success then + lintConfig = configUtils.extractConfig(loadedConfig) + elseif VERBOSE then + print(`Error loading config:\n{loadedConfig}\nProceeding with defaults.`) + end + end + return lintConfig +end + +function lintCore.lintString( + source: string, + lintRules: { types.LintRule }, + inputFilePath: pathLib.Path?, + configData: internalTypes.RuleConfigData, + VERBOSE: boolean +): { types.LintViolation } + if VERBOSE then + print(`Parsing input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) + end + local cst = 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(cst)) + + if VERBOSE then + print("Applying lint rules") + end + local violations: { types.LintViolation } = {} + local directiveCache = {} + for _, rule in lintRules do + if inputFilePath then + -- check if rule-specific ignores omit the current path + local ruleIgnores = configData.ruleIgnores[rule.name] + if ruleIgnores ~= nil and parseIgnores.isIgnored(ruleIgnores, pathLib.format(inputFilePath), false) then + continue + end + end + + local ruleConfig = configData.ruleConfigs[rule.name] + local context: types.RuleContext = { + globals = configData.globals, + options = if ruleConfig.options then ruleConfig.options else {}, + } + + local success, err = pcall(rule.lint, cst, inputFilePath, context) + local overwrittenSeverity = ruleConfig.severity + if success then + -- On success, err contains the returned violations + for _, violation in err do + if not violationIsSuppressed(violation, cst, directiveCache, globalIgnores) then + violation.severity = if overwrittenSeverity then overwrittenSeverity else violation.severity + 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: bidirectional inference should mean that annotations on a and b aren't needed + return a.location < b.location + end + ) + + return violations +end + +function lintCore.lintFile( + inputFilePath: pathLib.Path, + lintRules: { types.LintRule }, + autofixEnabled: boolean, + configData: internalTypes.RuleConfigData, + verbose: boolean +): { types.LintViolation } + if verbose then + print(`Reading input file '{inputFilePath}'`) + end + local fileContent = fs.readFileToString(inputFilePath) + + local violations = lintCore.lintString(fileContent, lintRules, inputFilePath, configData, verbose) + + 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 lintCore.lintString(newContent, lintRules, inputFilePath, configData, verbose) +end + +function lintCore.initialize( + passedConfigVal: string?, + noDefaults: boolean, + rulePath: string?, + VERBOSE: boolean +): ({ types.LintRule }, internalTypes.RuleConfigData, parseIgnores.GitignoreData) + if passedConfigVal and fs.type(passedConfigVal) ~= "file" then + print(`Error: Configuration path must point to a file, not {fs.type(passedConfigVal)}`) + process.exit(1) + end + + local configPath: pathLib.Pathlike = if passedConfigVal + then passedConfigVal + else pathLib.join(process.cwd(), ".config.luau") + local rootLocation = if fs.exists(configPath) + then pathLib.format(pathLib.resolve(pathLib.dirname(configPath))) + else pathLib.format(process.cwd()) + + local lintConfig: internalTypes.LintConfig = lintCore.loadConfig(configPath, VERBOSE) + + local lintRules: { types.LintRule } = {} + local ruleConfigurations: internalTypes.RuleConfigurations = {} + local ruleIgnoreData: internalTypes.RuleIgnores = {} + + local globalIgnores = if lintConfig.ignores ~= nil then lintConfig.ignores else {} + local globalIgnoreData = parseIgnores.parseIgnoreContents(rootLocation, globalIgnores) + + if noDefaults then + if VERBOSE then + print("Default lint rules have been disabled.") + end + else + -- add path for each default rule to lintRulePaths + lintRules, ruleConfigurations, ruleIgnoreData = loadDefaultRules(lintConfig, rootLocation, VERBOSE) + end + + if rulePath then + if VERBOSE then + print(`Loading lint rule(s) from '{rulePath}'`) + end + + local customRules, customConfigs, customIgnores = loadLintRules(rulePath, lintConfig, rootLocation, VERBOSE) + tableext.extend(lintRules, customRules) + tableext.combine(ruleConfigurations, customConfigs) + tableext.combine(ruleIgnoreData, customIgnores) + end + + -- load local rules from config rules path + local configuredRulePaths: { pathLib.Pathlike } = if lintConfig.rulepaths then lintConfig.rulepaths else {} + for _, path in configuredRulePaths do + local resolvedPath = pathLib.resolve(rootLocation, path) + local loadedRules, loadedConfigs, loadedIgnoreData = + loadLintRules(pathLib.format(resolvedPath), lintConfig, rootLocation, VERBOSE) + tableext.extend(lintRules, loadedRules) + tableext.combine(ruleConfigurations, loadedConfigs) + tableext.combine(ruleIgnoreData, loadedIgnoreData) + end + + local configContextData = { + globals = if lintConfig.globals ~= nil then lintConfig.globals else {}, + ruleConfigs = ruleConfigurations, + ruleIgnores = ruleIgnoreData, + } + + return lintRules, configContextData :: internalTypes.RuleConfigData, globalIgnoreData +end + +return table.freeze(lintCore) diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau new file mode 100644 index 000000000..ebf6e7ed7 --- /dev/null +++ b/lute/cli/commands/lint/lsp/init.luau @@ -0,0 +1,85 @@ +local lintTypes = require("@lint") +local lspTypes = require("@self/types") +local pathLib = require("@std/path") +local syntax = require("@std/syntax") +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 + +local function getRange(location: syntax.Span): lspTypes.Range + return table.freeze({ + start = table.freeze({ + line = location.beginLine - 1, + character = location.beginColumn - 1, + }), + ["end"] = table.freeze({ + line = location.endLine - 1, + character = location.endColumn - 1, + }), + }) +end + +function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic + return table.freeze({ + range = getRange(lint.location), + severity = severity(lint.severity), + code = lint.lintname, + codeDescription = lint.target, + 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, + suggestedfix = if lint.suggestedfix + then table.freeze({ + fix = lint.suggestedfix.fix, + range = getRange(lint.suggestedfix.location or lint.location), + }) + 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..623605dee --- /dev/null +++ b/lute/cli/commands/lint/lsp/types.luau @@ -0,0 +1,29 @@ +export type Diagnostic = { + read range: Range, -- zero-based + read severity: number, -- 1: Error, 2: Warning, 3: Information, 4: Hint + read code: string, -- lint name + read codeDescription: string?, -- optional property to describe the violation + read source: "lute lint", + read message: string, + read tags: { number }?, -- 1: Unnecessary, 2: Deprecated + read suggestedfix: { + read fix: string, + read range: Range, + }?, +} + +export type Range = { + read start: { read line: number, read character: number }, + read ["end"]: { read line: number, read character: number }, +} + +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..b0bbcfe82 --- /dev/null +++ b/lute/cli/commands/lint/printer.luau @@ -0,0 +1,107 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local types = require("@lint") + +local printerLib = {} + +local TAB_SIZE = 4 + +local function formatLint(lint: types.LintViolation, sourceLines: { string }): string + local lines = {} + + table.insert(lines, `{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 + + table.insert(lines, filepathLine) + table.insert(lines, `{blankGutter}│`) + table.insert(lines, sourceLine) + table.insert( + lines, + `{blankGutter}│ {string.rep(" ", location.beginColumn - 1 + (prelintTabs * (TAB_SIZE - 1)))}{string.rep( + "^", + location.endColumn - location.beginColumn + (lintTabs * (TAB_SIZE - 1)) + )}` + ) + table.insert(lines, `{blankGutter}│`) + table.insert(lines, "") + 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 + + table.insert(lines, filepathLine) + table.insert(lines, `{blankGutter}│`) + for _, line in renderedSourceLines do + table.insert(lines, line) + end + table.insert(lines, `{blankGutter}│ ╰{string.rep("─", maxSourceLineLength - gutterSize - 8)}^`) + table.insert(lines, `{blankGutter}│\n`) + end + + if lint.suggestedfix then + table.insert(lines, `Suggested fix: {lint.suggestedfix.fix}\n`) + end + + if lint.target then + table.insert(lines, `For additional info: {lint.target}\n`) + end + + return table.concat(lines, "\n") +end + +function printerLib.formatLintsWithSource(lints: { types.LintViolation }, sourceContent: string): string + local sourceLines = sourceContent:split("\n") + local parts = {} + + for _, lint in lints do + table.insert(parts, formatLint(lint, sourceLines)) + end + + return table.concat(parts, "\n") +end + +function printerLib.formatLintsWithPath(lints: { types.LintViolation }, sourcePath: path.Path): string + return printerLib.formatLintsWithSource(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..e1f692f5f --- /dev/null +++ b/lute/cli/commands/lint/rules/README.md @@ -0,0 +1,8 @@ +To add a new default lint rule to `lute lint`: +1. Create a module or luau file defining the rule in this folder. +Each violation should report its target as `"https://lute.luau.org/cli/lint/.html"`. +1. Add the name of the rule to the `DEFAULT_RULES` constant in `lint/lintCore.luau` +1. Add tests for the rule in `tests/cli/lint.test.luau` +1. Add documentation for it in `docs/cli/lint`. The doc's title should be the same as your rule's name. + +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..d4a8c3395 --- /dev/null +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -0,0 +1,120 @@ +local lintTypes = require("@lint") +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 target = "https://lute.luau.org/cli/lint/almost_swapped.html" + +local compFuncs = {} + +function compFuncs.exprLocalsSame(a: syntax.CstExprLocal, b: syntax.CstExprLocal): boolean + return a["local"] == b["local"] +end + +function compFuncs.exprGlobalsSame(a: syntax.CstExprGlobal, b: syntax.CstExprGlobal): boolean + return a.name.text == b.name.text +end + +function compFuncs.exprIndexNamesSame(a: syntax.CstExprIndexName, b: syntax.CstExprIndexName): 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.CstExprIndexExpr, b: syntax.CstExprIndexExpr): boolean + if a.index.tag == "string" and b.index.tag == "string" then + if a.index.value.text ~= b.index.value.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.CstExpr, b: syntax.CstExpr): boolean + if a.tag ~= b.tag then + return false + end + + if a.tag == "local" then + return compFuncs.exprLocalsSame(a, b :: syntax.CstExprLocal) + elseif a.tag == "global" then + return compFuncs.exprGlobalsSame(a, b :: syntax.CstExprGlobal) + elseif a.tag == "indexname" then + return compFuncs.exprIndexNamesSame(a, b :: syntax.CstExprIndexName) + elseif a.tag == "index" then + return compFuncs.exprIndexExprsSame(a, b :: syntax.CstExprIndexExpr) + else + return false + end +end + +-- Report instances of attempted swaps like: +-- a = b; b = a +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + local violations = {} + + local nodes = query.findAllFromRoot(cst, 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], currStat.values[1] + local nextVar, nextVal = nextStat.variables[1], nextStat.values[1] + + 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, + }, + target = target, + }) + 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..5a2ee4c2d --- /dev/null +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -0,0 +1,80 @@ +local lintTypes = require("@lint") +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 = "Comparing a table reference to a table literal will always evaluate to `false`." +local target = "https://lute.luau.org/cli/lint/constant_table_comparison.html" + +local function isTableLiteral(expr: syntax.CstExpr): 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.CstExpr): 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( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + return query + .findAllFromRoot(cst, 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.CstExprBinary + ): 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, + target = target, + } + 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..024f6aba7 --- /dev/null +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -0,0 +1,67 @@ +local lintTypes = require("@lint") +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 = "Dividing by zero will always result in NaN, consider using `math.nan` instead." +local target = "https://lute.luau.org/cli/lint/divide_by_zero.html" + +local function isZeroLiteral(expr: syntax.CstExpr): boolean + return expr.kind == "expr" and expr.tag == "number" and expr.value == 0 +end + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + return query + .findAllFromRoot(cst, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" + end) + :filter(function(bin) + return isZeroLiteral(bin.rhsOperand) + end) + :mapToArray( + function( + n: syntax.CstExprBinary + ): lintTypes.LintViolation? -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation + local suggestedfix = nil + if n.operator.text == "/" or n.operator.text == "//" then + if isZeroLiteral(n.lhsOperand) then + suggestedfix = table.freeze({ fix = "math.nan" }) + elseif n.lhsOperand.tag == "unary" and n.lhsOperand.operator.text == "-" then + if isZeroLiteral(n.lhsOperand.operand) then + suggestedfix = table.freeze({ fix = "math.nan" }) + else + suggestedfix = table.freeze({ fix = "-math.huge" }) + end + else + suggestedfix = table.freeze({ fix = "math.huge" }) + end + elseif n.operator.text == "%" then + suggestedfix = table.freeze({ fix = "math.nan" }) + end + + return { + lintname = name, + location = n.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + suggestedfix = suggestedfix, + target = target, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau new file mode 100644 index 000000000..efa78b7f9 --- /dev/null +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -0,0 +1,72 @@ +local lintTypes = require("@lint") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "duplicate_keys" +local message = "Duplicate keys in table literals will result in only the last one being included." +local target = "https://lute.luau.org/cli/lint/duplicate_keys.html" + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + local function makeViolation(location: syntax.Span): lintTypes.LintViolation + return { + lintname = name, + location = location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + + return query + .findAllFromRoot(cst, utils.isExprTable) + :flatMap(function(tableExpr: syntax.CstExprTable): { lintTypes.LintViolation } + local violations = {} + + local seenKeys: { [string | number]: true } = {} + local numArrayElems = 0 + + for _, entry in tableExpr.entries do + if entry.kind == "list" then + numArrayElems += 1 + if seenKeys[numArrayElems] then + table.insert(violations, makeViolation(entry.location)) + end + elseif entry.kind == "record" then + if seenKeys[entry.key.text] then + table.insert(violations, makeViolation(entry.location)) + end + seenKeys[entry.key.text] = true + elseif entry.kind == "general" then + if entry.key.tag == "string" then + if seenKeys[entry.key.value.text] then + table.insert(violations, makeViolation(entry.location)) + end + seenKeys[entry.key.value.text] = true + elseif entry.key.tag == "number" then + if seenKeys[entry.key.value] then + table.insert(violations, makeViolation(entry.location)) + elseif numArrayElems > 0 and entry.key.value <= numArrayElems then + table.insert(violations, makeViolation(entry.location)) + end + seenKeys[entry.key.value] = true + end + end + end + + return violations + end).nodes +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/empty_if_block.luau b/lute/cli/commands/lint/rules/empty_if_block.luau new file mode 100644 index 000000000..4cc1c3bfe --- /dev/null +++ b/lute/cli/commands/lint/rules/empty_if_block.luau @@ -0,0 +1,134 @@ +local lintTypes = require("@lint") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "empty_if_block" +local message = "This `if` block is empty. Consider removing it or adding to it." +local target = "https://lute.luau.org/cli/lint/empty_if_block.html" + +local function containsComment(trivia: { syntax.Trivia }): boolean + for _, triv in trivia do + if triv.tag ~= "whitespace" then + return true + end + end + return false +end + +local function isBlockEmpty( + block: syntax.CstStatBlock, + commentsCount: boolean, + leadingTrivia: { syntax.Trivia }, + trailingTrivia: { syntax.Trivia } +): boolean + -- If the block has statements, it's not empty + if #block.statements > 0 then + return false + end + + -- If comments_count is false, we consider blocks with only comments as empty + if not commentsCount then + return true + end + + -- if comments_count is true, the block is not empty when it contains comments. + if containsComment(leadingTrivia) or containsComment(trailingTrivia) then + return false + end + + return true +end + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + context: lintTypes.RuleContext +): { lintTypes.LintViolation } + -- Get the comments_count option from the rule configuration + local commentsCount = false + if context.options and context.options.comments_count then + commentsCount = context.options.comments_count :: boolean + end + + -- Find all if statements + return query.findAllFromRoot(cst, utils.isStatIf):map(function(ifStat): lintTypes.LintViolation? + -- Check if the then block is empty + if + isBlockEmpty( + ifStat.thenBlock, + commentsCount, + ifStat.thenKeyword.trailingTrivia, + if #ifStat.elseifs > 0 -- trailingToken + then ifStat.elseifs[1].elseIfKeyword.leadingTrivia + elseif ifStat.elseKeyword then ifStat.elseKeyword.leadingTrivia + else ifStat.endKeyword.leadingTrivia + ) + then + return { + lintname = name, + location = ifStat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + + -- Check elseif blocks + for i, elseIfStat in ifStat.elseifs do + if + isBlockEmpty( + elseIfStat.thenBlock, + commentsCount, + elseIfStat.thenKeyword.trailingTrivia, + if i < #ifStat.elseifs -- trailingToken + then ifStat.elseifs[i + 1].elseIfKeyword.leadingTrivia + elseif ifStat.elseKeyword then ifStat.elseKeyword.leadingTrivia + else ifStat.endKeyword.leadingTrivia + ) + then + return { + lintname = name, + location = ifStat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + end + + -- Check else block if it exists + if ifStat.elseBlock then + assert(ifStat.elseKeyword, "elseblock should have an elsekeyword") + if + isBlockEmpty( + ifStat.elseBlock, + commentsCount, + ifStat.elseKeyword.trailingTrivia, + ifStat.endKeyword.leadingTrivia + ) + then + return { + lintname = name, + location = ifStat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + end + + return nil + end).nodes +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/global_function_in_scope.luau b/lute/cli/commands/lint/rules/global_function_in_scope.luau new file mode 100644 index 000000000..00df61dad --- /dev/null +++ b/lute/cli/commands/lint/rules/global_function_in_scope.luau @@ -0,0 +1,55 @@ +local lintTypes = require("@lint") +local path = require("@std/path") +local syntax = require("@std/syntax") +local visitorLib = require("@std/syntax/visitor") + +local name = "global_function_in_scope" +local message = + "Non-local function declaration found inside a nested scope. Use 'local function' instead, or move this to the top level." +local target = "https://lute.luau.org/cli/lint/global_function_in_scope.html" + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + local violations: { lintTypes.LintViolation } = {} + local depth = 0 + + local visitor: visitorLib.Visitor = visitorLib.create() :: any + + function visitor.visitStatBlock(_: syntax.CstStatBlock) + depth += 1 + return true + end + + function visitor.visitStatBlockEnd(_: syntax.CstStatBlock) + depth -= 1 + end + + function visitor.visitStatFunction(stat: syntax.CstStatFunction) + -- depth > 1 means we're inside a nested block (not the top-level block) + if depth > 1 and stat.name.tag ~= "indexname" then + table.insert(violations, { + lintname = name, + location = stat.location, + message = message, + severity = "warning" :: "warning", -- LUAUFIX cast needed because severity isn't inferred as a singleton + sourcepath = sourcepath, + target = target, + }) + end + return true + end + + visitorLib.visit(cst, visitor) + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/no_any.luau b/lute/cli/commands/lint/rules/no_any.luau new file mode 100644 index 000000000..0c5bebc68 --- /dev/null +++ b/lute/cli/commands/lint/rules/no_any.luau @@ -0,0 +1,40 @@ +local lintTypes = require("@lint") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "no_any" +local message = + "Consider using `unknown` instead of `any` as a type-safe alternative that requires narrowing before use." +local target = "https://lute.luau.org/cli/lint/no_any.html" + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + return query + .findAllFromRoot(cst, utils.isTypeReference) + :filter(function(typeRef) + return typeRef.name.text == "any" and typeRef.prefix == nil + end) + :mapToArray(function(n: syntax.CstTypeReference): lintTypes.LintViolation? + return { + lintname = name, + location = n.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + suggestedfix = table.freeze({ fix = "unknown" }), + target = target, + } + 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..509da98a5 --- /dev/null +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -0,0 +1,89 @@ +local lintTypes = require("@lint") +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 target = "https://lute.luau.org/cli/lint/parenthesized_conditions.html" + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + local violations: { lintTypes.LintViolation } = {} + + query + .findAllFromRoot(cst, function(node: syntax.CstNode): (syntax.CstExprIfElse | syntax.CstStatIf)? + 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" :: "info", -- LUAUFIX cast needed because severity isn't inferred as a singleton + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printNode(ifStat.condition.expression), + }, + target = target, + }) + 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" :: "info", -- LUAUFIX cast needed because severity isn't inferred as a singleton + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printNode(elseIfStat.condition.expression), + }, + target = target, + }) + end + end + end) + + query + .findAllFromRoot(cst, function(node: syntax.CstNode): (syntax.CstStatWhile | syntax.CstStatRepeat)? + 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" :: "info", -- LUAUFIX cast needed because severity isn't inferred as a singleton + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printNode((n.condition :: syntax.CstExprGroup).expression), + }, + target = target, + }) + end) + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/reassigned_parameter.luau b/lute/cli/commands/lint/rules/reassigned_parameter.luau new file mode 100755 index 000000000..e0a18e2fb --- /dev/null +++ b/lute/cli/commands/lint/rules/reassigned_parameter.luau @@ -0,0 +1,93 @@ +local lintTypes = require("@lint") +local path = require("@std/path") +local syntax = require("@std/syntax") +local visitorLib = require("@std/syntax/visitor") + +local name = "reassigned_parameter" +local message = "Did you mean to reassign to a function parameter?" +local target = "https://lute.luau.org/cli/lint/reassigned_parameter.html" + +type SavedScope = { [syntax.CstLocal]: true } + +local function checkAssignment(scopes: { SavedScope }, node: syntax.CstExpr): boolean + if node.tag ~= "local" then + return false + end + + for _, scope in scopes do + if scope[node["local"]] then + return true + end + end + + return false +end + +local function createViolation(sourcepath: path.Path?, location: syntax.Span): lintTypes.LintViolation + local violation: lintTypes.LintViolation = { + lintname = name, + location = location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + + return violation +end + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + local violations = {} + + local visitor = visitorLib.create() + + local scopes: { SavedScope } = {} + + visitor.visitExprFunction = function(node: syntax.CstExprFunction): boolean + local scope: SavedScope = {} + for _, parameter in node.parameters do + scope[parameter] = true + end + + table.insert(scopes, scope) + + return true + end + + visitor.visitStatAssign = function(stat: syntax.CstStatAssign): boolean + for _, variable in stat.variables do + if checkAssignment(scopes, variable) then + table.insert(violations, createViolation(sourcepath, stat.location)) + end + end + + return true + end + + visitor.visitStatCompoundAssign = function(stat: syntax.CstStatCompoundAssign): boolean + if checkAssignment(scopes, stat.variable) then + table.insert(violations, createViolation(sourcepath, stat.location)) + end + + return true + end + + visitor.visitExprFunctionEnd = function(): () + table.remove(scopes) + end + + visitorLib.visitBlock(cst, visitor) + + return violations +end + +local rule: lintTypes.LintRule = table.freeze({ + name = name, + lint = lint, +}) + +return rule diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau new file mode 100644 index 000000000..15666133b --- /dev/null +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -0,0 +1,449 @@ +local lintTypes = require("@lint") +local path = require("@std/path") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") +local tableext = require("@std/tableext") +local visitorLib = require("@std/syntax/visitor") + +local name = "unused_variable" +local message = "This variable is unused. Prefix its name with '_' to indicate that it is intentionally unused." +local target = "https://lute.luau.org/cli/lint/unused_variable.html" + +type leaf = { tag: "leaf", vals: { [number]: true } } + +type branch = { tag: "branch", entries: { [string]: leaf } } + +-- We assume that we only need 2 levels of depth, since there currently no APIs nested more than 2 levels. +-- If we need more levels in the future, we can generalize, but this greatly simplifies the logic. +type smallTrie = { tag: "root", entries: { [string]: branch | leaf } } + +local function getWriteOnlyArgs(func: syntax.CstExpr, writeOnlyAPIs: smallTrie): { [number]: true }? + if func.tag == "global" then + local entry = writeOnlyAPIs.entries[func.name.text] + + if not entry or entry.tag ~= "leaf" then + return nil + end + + return entry.vals + elseif func.tag == "indexname" and func.expression.tag == "global" then + local branch = writeOnlyAPIs.entries[(func.expression :: syntax.CstExprGlobal).name.text] + + if not branch or branch.tag ~= "branch" then + return nil + end + + local entry = branch.entries[func.index.text] + + return if entry then entry.vals else nil + elseif func.tag == "index" and func.expression.tag == "global" and func.index.tag == "string" then + func = func :: syntax.CstExprIndexName | syntax.CstExprIndexExpr + local branch = writeOnlyAPIs.entries[(func.expression :: syntax.CstExprGlobal).name.text] + + if not branch or branch.tag ~= "branch" then + return nil + end + + local entry = branch.entries[(func.index :: syntax.CstExprConstantString).value.text] + + return if entry then entry.vals else nil + else + return nil + end +end + +local function unusedLocals(block: syntax.CstStatBlock, writeOnlyAPIs: smallTrie): { syntax.CstLocal } + local visitor: visitorLib.Visitor & { + unusedLocals: { syntax.CstLocal }, + scopes: { { [syntax.CstLocal]: boolean } }, + ignoreReferences: { [syntax.CstLocal]: true }, + requiredModules: { [string]: syntax.CstLocal }, + insideMethodContext: boolean, + inLValueContext: boolean, + } = + visitorLib.create() :: any + visitor.unusedLocals = {} + visitor.scopes = {} + visitor.ignoreReferences = {} + visitor.requiredModules = {} + -- Used to avoid lookups to `self` + visitor.insideMethodContext = false + visitor.inLValueContext = false + + local function popAndSaveUnusedLocals(_: syntax.CstNode) + -- Pop scope and save unused locals + local scope = table.remove(visitor.scopes) + if not scope then + error("Expected a scope to pop") + end + + for l, visited in scope do + if not visited and l.name.text:sub(1, 1) ~= "_" then + table.insert(visitor.unusedLocals, l) + end + end + end + + local function markVisited(l: syntax.CstLocal) + -- Mark local as visited in closest scope + for i = #visitor.scopes, 1, -1 do + if visitor.scopes[i][l] ~= nil then + visitor.scopes[i][l] = true + return + end + end + + error("Local variable not found in any scope") + end + + function visitor.visitStatBlock(_: syntax.CstStatBlock) + -- Push a fresh scope + table.insert(visitor.scopes, {} :: { [syntax.CstLocal]: boolean }) + return true + end + + visitor.visitStatBlockEnd = popAndSaveUnusedLocals + + function visitor.visitStatLocalDeclaration(stat: syntax.CstStatLocal) + -- For each (var, value) pair, ignore references to the var in the value + for i, localVar in stat.variables do + if stat.values[i] then + if utils.isRequireCall(stat.values[i]) then + visitor.requiredModules[localVar.name.text] = localVar + visitorLib.visitExpression(stat.values[i], visitor) + else + visitor.ignoreReferences[localVar] = true + if localVar.annotation then + visitorLib.visitType(localVar.annotation, visitor) + end + visitorLib.visitExpression(stat.values[i], visitor) + visitor.ignoreReferences[localVar] = nil + end + end + end + + -- Mark each var as unused in the current scope + for _, localVar in stat.variables do + visitor.scopes[#visitor.scopes][localVar] = false + end + + return false + end + + function visitor.visitStatFor(stat: syntax.CstStatFor) + -- Push a scope for the loop variable + table.insert(visitor.scopes, { [stat.variable] = false }) + return true + end + + visitor.visitStatForEnd = popAndSaveUnusedLocals + + function visitor.visitStatForIn(stat: syntax.CstStatForIn) + -- Push a scope for the loop variables + local newScope: { [syntax.CstLocal]: boolean } = {} + for _, var in stat.variables do + newScope[var] = false + end + table.insert(visitor.scopes, newScope) + return true + end + + visitor.visitStatForInEnd = popAndSaveUnusedLocals + + function visitor.visitStatAssign(stat: syntax.CstStatAssign) + local i = 1 + local callFound = false + local prevInLValueContext = visitor.inLValueContext + while i <= math.min(#stat.variables, #stat.values) do + local var = stat.variables[i] + local val = stat.values[i] + -- An unused local doesn't become used just because it's used to assign to itself, eg: + -- local x + -- x = x + 1 + -- However, calls, can assign to multiple variables, so we can't assume variables and values correspond 1-1 once we see a call + if var.tag == "local" and not callFound then + visitor.ignoreReferences[var["local"]] = true + + visitorLib.visitExpression(val, visitor) + + visitor.ignoreReferences[var["local"]] = nil + else + visitor.inLValueContext = true + visitorLib.visitExpression(var, visitor) + visitor.inLValueContext = prevInLValueContext + + visitorLib.visitExpression(val, visitor) + end + + if val.tag == "call" then + callFound = true + end + + i = i + 1 + end + + -- If we see a call, we no longer know if variables correspond 1-1 with values, since functions can return multiple values + -- In this case, we stop caring about ignoring references, and only check as many values as there are variables + + return false + end + + function visitor.visitStatCompoundAssign(stat: syntax.CstStatCompoundAssign) + -- An unused local doesn't become used just because it's used to assign to itself, eg: + -- local x + -- x += x + 1 + if stat.variable.tag == "local" then + visitor.ignoreReferences[stat.variable["local"]] = true + visitorLib.visitExpression(stat.value, visitor) + visitor.ignoreReferences[stat.variable["local"]] = nil + else + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = true + visitorLib.visitExpression(stat.variable, visitor) + visitor.inLValueContext = prevInLValueContext + + visitorLib.visitExpression(stat.value, visitor) + end + + return false + end + + function visitor.visitStatFunction(stat: syntax.CstStatFunction) + -- This could be reassigning to a local, in which case we need to apply the same logic as visitStatAssign + if stat.name.tag == "local" then + -- If this shadows an unused local in the current scope, we need to mark it as unused + if visitor.scopes[#visitor.scopes][stat.name["local"]] == false then + table.insert(visitor.unusedLocals, stat.name["local"]) + end + + visitor.ignoreReferences[stat.name["local"]] = true + visitorLib.visitExpression(stat.func, visitor) + visitor.ignoreReferences[stat.name["local"]] = nil + elseif stat.name.tag == "indexname" and stat.name.accessor.text == ":" then + local prevMethodContext = visitor.insideMethodContext + visitor.insideMethodContext = true + + visitorLib.visitExpression(stat.func, visitor) + + visitor.insideMethodContext = prevMethodContext + elseif stat.name.tag == "index" then + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = true + visitorLib.visitExpression(stat.name, visitor) + visitor.inLValueContext = prevInLValueContext + + visitorLib.visitExpression(stat.func, visitor) + else + visitorLib.visitExpression(stat.func, visitor) + end + + return false + end + + function visitor.visitStatLocalFunction(stat: syntax.CstStatLocalFunction) + -- Recursive functions don't count as references to themselves + visitor.ignoreReferences[stat.name] = true + + visitorLib.visitExpression(stat.func, visitor) + + visitor.ignoreReferences[stat.name] = nil + + visitor.scopes[#visitor.scopes][stat.name] = false + + return false + end + + function visitor.visitStatRepeat(stat: syntax.CstStatRepeat) + -- Push a fresh scope + table.insert(visitor.scopes, {} :: { [syntax.CstLocal]: boolean }) + + -- Manually visit the block's states so we can keep the scope around + for _, bodyStat in stat.body.statements do + visitorLib.visitStatement(bodyStat, visitor) + end + + -- Visit the condition with the same scope as the body + visitorLib.visitExpression(stat.condition, visitor) + + popAndSaveUnusedLocals(stat) + + return false + end + + function visitor.visitExprLocal(node: syntax.CstExprLocal) + if node.token.text == "self" and visitor.insideMethodContext then + return false + end + + local cstlocal = node["local"] + + -- We only ignore writes to locals in the same scope + -- We don't want to warn writes to upvalues as unused + if visitor.inLValueContext and visitor.scopes[#visitor.scopes][cstlocal] ~= nil then + return false + end + + if visitor.ignoreReferences[cstlocal] then + return false + end + + markVisited(cstlocal) + + return false + end + + function visitor.visitExprFunction(node: syntax.CstExprFunction) + -- Push a scope for the function parameters + local newScope: { [syntax.CstLocal]: boolean } = {} + + for _, param in node.parameters do + newScope[param] = false + end + + table.insert(visitor.scopes, newScope) + + return true + end + + visitor.visitExprFunctionEnd = popAndSaveUnusedLocals + + function visitor.visitTypeReference(node: syntax.CstTypeReference) + if not node.prefix then + return true + end + + local requiredModule = visitor.requiredModules[node.prefix.text] + + if not requiredModule then + return true + end + + markVisited(requiredModule) + + return true + end + + function visitor.visitExprIndexExpr(node: syntax.CstExprIndexExpr) + visitorLib.visitExpression(node.expression, visitor) + + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = false + visitorLib.visitExpression(node.index, visitor) + visitor.inLValueContext = prevInLValueContext + + return false + end + + function visitor.visitExprCall(node: syntax.CstExprCall) + local writeOnlyIndices = getWriteOnlyArgs(node.func, writeOnlyAPIs) + + if not writeOnlyIndices then + return true + end + + visitorLib.visitExpression(node.func, visitor) + + for i, arg in node.arguments do + if writeOnlyIndices[i] then + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = true + visitorLib.visitExpression(arg, visitor) + visitor.inLValueContext = prevInLValueContext + else + visitorLib.visitExpression(arg, visitor) + end + end + + return false + end + + visitorLib.visitBlock(block, visitor) + + return visitor.unusedLocals +end + +local function isPositiveInt(v: unknown): boolean + return type(v) == "number" and v > 0 and math.floor(v) == v +end + +local function lint( + cst: syntax.CstStatBlock, + sourcepath: path.Path?, + context: lintTypes.RuleContext +): { lintTypes.LintViolation } + -- Validate context.options, which should be of form { ["api"] = { number } }, + -- where the number keys represent argument indices which only write to their passed values. + for api, args in context.options do + local errorMessage = + `Expected options for API '{api}' to be an array of argument indices which only write to their passed values` + if type(args) ~= "table" then + error(errorMessage) + end + + for k, v in args do + if not isPositiveInt(k) or not isPositiveInt(v) then + error(errorMessage) + end + end + end + + local writeOnlyAPIs: smallTrie = { + tag = "root", + entries = { + table = { tag = "branch", entries = { insert = { tag = "leaf", vals = { [1] = true } } } }, + }, + } + + for k, v in context.options do + local parts = k:split(".") + if #parts < 1 or #parts > 2 then + error(`Invalid API '{k}' in options. Only APIs up to 2 levels deep are supported.`) + end + + if #parts == 1 then + local entry = writeOnlyAPIs.entries[parts[1]] + + if entry then + error( + `API '{parts[1]}' is already defined as {if entry.tag == "branch" then "part of " else ""}a write-only API, so '{k}' cannot be added as a write-only API.` + ) + end + + writeOnlyAPIs.entries[parts[1]] = { tag = "leaf", vals = tableext.toSet(v :: { number }) } -- We validate v above, so the cast is ok + else + if not writeOnlyAPIs.entries[parts[1]] then + writeOnlyAPIs.entries[parts[1]] = { + tag = "branch", + entries = { [parts[2]] = { tag = "leaf", vals = tableext.toSet(v :: { number }) } }, + } + else + local branch = writeOnlyAPIs.entries[parts[1]] + if branch.tag ~= "branch" then + error( + `API '{parts[1]}' is already defined as a write-only API, so '{k}' cannot be added as a write-only API.` + ) + end + branch.entries[parts[2]] = { tag = "leaf", vals = tableext.toSet(v :: { number }) } -- We validate v above, so the cast is ok + end + end + end + + return tableext.map(unusedLocals(cst, writeOnlyAPIs), function(l: syntax.CstLocal): lintTypes.LintViolation + return { + lintname = name, + location = l.location, -- LUAUFIX: Bidirectional inference should let us know that l : syntax.CstLocal + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + tags = { "unnecessary" }, + } + end) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/worker.luau b/lute/cli/commands/lint/worker.luau new file mode 100644 index 000000000..89b7e0c17 --- /dev/null +++ b/lute/cli/commands/lint/worker.luau @@ -0,0 +1,25 @@ +local internalTypes = require("./internaltypes") +local lintCore = require("./lintCore") +local pathLib = require("@std/path") +local types = require("@lint") + +-- populate in initialize +local lintRules: { types.LintRule } = {} +local configData: internalTypes.RuleConfigData = { + ruleConfigs = {}, + ruleIgnores = {}, + globals = {}, +} + +local worker = {} + +-- load config, load rules +function worker.initialize(rulesPath: string?, noDefaults: boolean, configPath: string?, verbose: boolean) + lintRules, configData = lintCore.initialize(configPath, noDefaults, rulesPath, verbose) +end + +function worker.lintFile(file: string, autofix: boolean, verbose: boolean): { types.LintViolation } + return lintCore.lintFile(pathLib.resolve(file), lintRules, autofix, configData, verbose) +end + +return table.freeze(worker) diff --git a/lute/cli/commands/new/init.luau b/lute/cli/commands/new/init.luau new file mode 100644 index 000000000..0f3aba0a1 --- /dev/null +++ b/lute/cli/commands/new/init.luau @@ -0,0 +1,88 @@ +local cli = require("@batteries/cli") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local typedefs = require("./lib/typedefs") + +local USAGE = "Usage: lute new " + +local function printHelp() + print(USAGE) + print([[ +Create a new Lute project with a standard directory layout. + +ARGUMENTS: + Name of the project to create + +OPTIONS: + -h, --help Show this help message + +EXAMPLES: + lute new myproject +]]) +end + +local function main(...: string) + local args = cli.parser() + args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) + args:add("name", "positional", { help = "Name of the project to create" }) + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local projectName = args:get("name") + if projectName == nil or projectName == "" then + print("Error: Missing required argument .\n\n" .. USAGE .. "\nUse --help for more information.") + process.exit(1) + end + + -- LUAUFIX - refinements aren't correctly being applied here. + -- At this point, we should know that ~ (projectName == nil or projectName == "") is true (which should imply that projectName ~= nil) + assert(projectName) + local projectPath = path.join(process.cwd(), projectName) + + if fs.exists(projectPath) then + print(`Error: Directory '{projectName}' already exists.`) + process.exit(1) + end + + if not fs.exists(typedefs.TYPEDEFS_PATH) then + print("Running lute setup first...") + local result = process.run({ path.format(process.execPath()), "setup" }, { stdio = "inherit" }) + if not result.ok then + print("Error: lute setup failed.") + process.exit(1) + end + end + + fs.createDirectory(projectPath) + + local mainScript = path.join(projectPath, "main.luau") + local testScript = path.join(projectPath, "main.test.luau") + + fs.writeStringToFile( + mainScript, + [[print("Hello, world!") +]] + ) + + fs.writeStringToFile( + testScript, + [[local _test = require("@std/test") +]] + ) + + fs.writeStringToFile(path.join(projectPath, ".config.luau"), typedefs.generateConfigDotLuau("strict")) + + print(`Created project '{projectName}'`) + print("") + print("To get started:") + print(`\tcd {projectName}`) + print("\tlute run main.luau") + print("\tlute test") +end + +main(...) diff --git a/lute/cli/commands/pkg/init.luau b/lute/cli/commands/pkg/init.luau new file mode 100644 index 000000000..da044ca70 --- /dev/null +++ b/lute/cli/commands/pkg/init.luau @@ -0,0 +1,43 @@ +local process = require("@std/process") + +local install = require("@self/loom-core/src/commands/install") +local authenticate = require("@self/loom-core/src/commands/authenticate") + +local USAGE = "Usage: lute pkg " + +local function printHelp() + print(USAGE) + print([[ + +Loom package manager for Luau projects. + +COMMANDS: + install Install dependencies from loom.config.luau + auth --token [--domain ] Save authentication credentials + +Use --help with any command for more information. +]]) +end + +local function main(...: string) + local args = { ... } + + if #args == 0 or args[1] == "--help" or args[1] == "-h" then + printHelp() + return + end + + local command = args[1] + + if command == "install" then + install(table.unpack(args, 2)) + elseif command == "auth" then + authenticate(table.unpack(args, 2)) + else + print(`Unknown command: {command}\n`) + printHelp() + process.exit(1) + end +end + +main(...) diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/LICENSE.md b/lute/cli/commands/pkg/loom-core/extern/unzip/LICENSE.md new file mode 100644 index 000000000..21d615a34 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/LICENSE.md @@ -0,0 +1,7 @@ +Copyright © 2024 0x5eal + +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. \ No newline at end of file diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau new file mode 100644 index 000000000..3354c1709 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau @@ -0,0 +1,58 @@ +local CRC32_TABLE: { number } = table.create(256) :: any + +-- TODO: Maybe compute this AoT as an optimization +-- Initialize the lookup table and lock it in place +for i = 0, 255 do + local crc = i + for _ = 1, 8 do + if bit32.band(crc, 1) == 1 then + crc = bit32.bxor(bit32.rshift(crc, 1), 0xEDB88320) + else + crc = bit32.rshift(crc, 1) + end + end + CRC32_TABLE[i] = crc +end + +table.freeze(CRC32_TABLE) + +local function crc32(buf: buffer): number + local crc = 0xFFFFFFFF + local len = buffer.len(buf) + local i = 0 + + -- OPT: Unroll to process 8 bytes at a time for better performance + while i + 7 < len do + local b0 = buffer.readu8(buf, i) + local b1 = buffer.readu8(buf, i + 1) + local b2 = buffer.readu8(buf, i + 2) + local b3 = buffer.readu8(buf, i + 3) + local b4 = buffer.readu8(buf, i + 4) + local b5 = buffer.readu8(buf, i + 5) + local b6 = buffer.readu8(buf, i + 6) + local b7 = buffer.readu8(buf, i + 7) + + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b0), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b1), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b2), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b3), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b4), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b5), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b6), 0xFF)]) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[bit32.band(bit32.bxor(crc, b7), 0xFF)]) + + i += 8 + end + + -- Process remaining bytes + while i < len do + local byte = buffer.readu8(buf, i) + local index = bit32.band(bit32.bxor(crc, byte), 0xFF) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[index]) + i += 1 + end + + return bit32.bxor(crc, 0xFFFFFFFF) +end + +return crc32 diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/inflate.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/inflate.luau new file mode 100644 index 000000000..4df0b4fd5 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/inflate.luau @@ -0,0 +1,641 @@ +-- Tree class for storing Huffman trees used in DEFLATE decompression +local Tree = {} + +export type Tree = typeof(setmetatable({} :: TreeInner, { __index = Tree })) +type TreeInner = { + table: { number }, -- Length of 16, stores code length counts + trans: { number }, -- Length of 288, stores code to symbol translations + lookup: { number }?, -- Precomputed Huffman lookup table for fast path +} + +--- Creates a new Tree instance with initialized tables +function Tree.new(): Tree + return setmetatable( + { + table = table.create(16, 0), + trans = table.create(288, 0), + lookup = nil, + } :: TreeInner, + { __index = Tree } + ) +end + +-- Data class for managing compression state and buffers +local Data = {} +export type Data = typeof(setmetatable({} :: DataInner, { __index = Data })) +-- stylua: ignore +export type DataInner = { + source: buffer, -- Input buffer containing compressed data + sourceLen: number, -- Cached length of source buffer + sourceIndex: number, -- Current position in source buffer + tag: number, -- Bit buffer for reading compressed data + bitcount: number, -- Number of valid bits in tag + + dest: buffer, -- Output buffer for decompressed data + destLen: number, -- Current length of decompressed data + + ltree: Tree, -- Length/literal tree for current block + dtree: Tree, -- Distance tree for current block +} + +--- Creates a new Data instance with initialized compression state +function Data.new(source: buffer, dest: buffer): Data + return setmetatable( + { + source = source, + sourceLen = buffer.len(source), + sourceIndex = 0, + tag = 0, + bitcount = 0, + dest = dest, + destLen = 0, + ltree = Tree.new(), + dtree = Tree.new(), + } :: DataInner, + { __index = Data } + ) +end + +-- Static Huffman trees used for fixed block types +local staticLengthTree = Tree.new() +local staticDistTree = Tree.new() + +-- Tables for storing extra bits and base values for length/distance codes +local lengthBits = table.create(30, 0) +local lengthBase = table.create(30, 0) +local distBits = table.create(30, 0) +local distBase = table.create(30, 0) + +-- Special ordering of code length codes used in dynamic Huffman trees +-- stylua: ignore +local clcIndex = { + 16, 17, 18, 0, 8, 7, 9, 6, + 10, 5, 11, 4, 12, 3, 13, 2, + 14, 1, 15 +} + +-- Tree used for decoding code lengths in dynamic blocks +local codeTree = Tree.new() +local lengths = table.create(288 + 32, 0) + +-- Pre-calculate bitmasks for bit extraction, this only runs on the first +-- require before this module is cached +local bitMasks = table.create(17, 0) +for i = 0, 16 do + bitMasks[i] = bit32.rshift(0xffff, 16 - i) +end + +-- Maximum bit length for fixed Huffman codes. We selected 9 as a good mean +-- value instead of 10 or 12 as they seemed to be slightly slower, potentially +-- due to table memory overhead. In my testing using the `pandoc_soft_links` +-- file, I found these throughputs: +-- +-- - 10-bit (1024 entries): 15.0 MB / s +-- - 12-bit (4096 entries): 11.5 MB / s +-- - 9-bit (512 entries): 17.2 MB / s + +local LOOKUP_BITS = 9 +local LOOKUP_SIZE = bit32.lshift(1, LOOKUP_BITS) -- OPT: lshift instead of exponentiation + +--- Builds the extra bits and base tables for length and distance codes +local function buildBitsBase(bits: { number }, base: { number }, delta: number, first: number) + local sum = first + + -- Initialize the bits table with appropriate bit lengths + for i = 0, delta - 1 do + bits[i] = 0 + end + for i = 0, 29 - delta do + bits[i + delta] = math.floor(i / delta) + end + + -- Build the base value table using bit lengths + for i = 0, 29 do + base[i] = sum + sum += bit32.lshift(1, bits[i]) + end +end + +--- Constructs the fixed Huffman trees used in DEFLATE format +local function buildFixedTrees(lengthTree: Tree, distTree: Tree) + -- Build the fixed length tree according to DEFLATE specification + for i = 0, 6 do + lengthTree.table[i] = 0 + end + lengthTree.table[7] = 24 + lengthTree.table[8] = 152 + lengthTree.table[9] = 112 + + -- Populate the translation table for length codes + for i = 0, 23 do + lengthTree.trans[i] = 256 + i + end + for i = 0, 143 do + lengthTree.trans[24 + i] = i + end + for i = 0, 7 do + lengthTree.trans[24 + 144 + i] = 280 + i + end + for i = 0, 111 do + lengthTree.trans[24 + 144 + 8 + i] = 144 + i + end + + -- Build the fixed distance tree (simpler than length tree) + for i = 0, 4 do + distTree.table[i] = 0 + end + distTree.table[5] = 32 + + for i = 0, 31 do + distTree.trans[i] = i + end +end + +--- Temporary array for building trees +local offs = table.create(16, 0) + +--- Builds a fast lookup table for the first `LOOKUP_BITS` bits of Huffman codes +local function buildLookupTable(t: Tree) + local lookup = table.create(LOOKUP_SIZE, 0) + + for i = 0, LOOKUP_SIZE - 1 do + -- Traverse the Huffman tree to find what this bit pattern decodes to + local sum, cur, len = 0, 0, 0 + local bits = i + + repeat + cur = 2 * cur + bit32.band(bits, 1) + bits = bit32.rshift(bits, 1) + len += 1 + + -- Huffman codes cannot be longer than 15 bits + if len > 15 then + break + end + + sum += t.table[len] + cur -= t.table[len] + until cur < 0 + + if len <= LOOKUP_BITS then + -- Only store in lookup table if it is within the bits range + -- High 16 bits = symbol; low 16 bits = length + local symbol = t.trans[sum + cur] + lookup[i] = bit32.lshift(symbol, 16) + len + else + -- NOTE: a length of zero signifies no fast path + lookup[i] = 0 + end + end + + t.lookup = lookup +end + +--- Builds a Huffman tree from a list of code lengths +local function buildTree(t: Tree, lengths: { number }, off: number, num: number) + -- Initialize the code length count table + for i = 0, 15 do + t.table[i] = 0 + end + + -- Count the frequency of each code length + for i = 0, num - 1 do + t.table[lengths[off + i]] += 1 + end + + t.table[0] = 0 + + -- Calculate offsets for distribution sort + local sum = 0 + for i = 0, 15 do + offs[i] = sum + sum += t.table[i] + end + + -- Create the translation table mapping codes to symbols + for i = 0, num - 1 do + local len = lengths[off + i] + if len > 0 then + t.trans[offs[len]] = i + offs[len] += 1 + end + end + + -- Build fast lookup table for first LOOKUP_BITS bits + buildLookupTable(t) +end + +--- Ensures the bitbuffer has enough bits available +local function ensureBits(d: Data, minBits: number) + if d.bitcount >= minBits then + return + end + + -- Refill as much as possible at once + while d.bitcount < 24 and d.sourceIndex < d.sourceLen do + d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount)) + d.sourceIndex += 1 + d.bitcount += 8 + end +end + +--- Reads a single bit from the input stream +local function getBit(d: Data): number + if d.bitcount <= 0 then + d.tag = buffer.readu8(d.source, d.sourceIndex) + d.sourceIndex += 1 + d.bitcount = 8 + end + + local bit = bit32.band(d.tag, 1) + d.tag = bit32.rshift(d.tag, 1) + d.bitcount -= 1 + + return bit +end + +--- Reads multiple bits from the input stream with a base value +local function readBits(d: Data, num: number?, base: number): number + if not num then + return base + end + + ensureBits(d, num) + + local val = bit32.band(d.tag, bitMasks[num]) + d.tag = bit32.rshift(d.tag, num) + d.bitcount -= num + + return val + base +end + +--- Decodes a symbol using a Huffman tree with fast lookup table +local function decodeSymbol(d: Data, t: Tree): number + -- Longest possible code in a Huffman table is 15 bits, ensure + -- there are enough bits for that + ensureBits(d, 15) + + -- Fast path: lookup first `LOOKUP_BITS` + local lookup = t.lookup :: { number } + local lookupIndex = bit32.band(d.tag, LOOKUP_SIZE - 1) + local entry = lookup[lookupIndex] + local len = bit32.band(entry, 0xFFFF) + + if len > 0 then + -- If the code fits within the `LOOKUP_BITS` size, nothing more + -- to be done + local symbol = bit32.rshift(entry, 16) + d.tag = bit32.rshift(d.tag, len) + d.bitcount -= len + return symbol + end + + -- Slow path: code is longer than `LOOKUP_BITS`, we need to traverse + -- the full tree on our own + local sum, cur = 0, 0 + local tag = d.tag + len = 0 + + repeat + cur = 2 * cur + bit32.band(tag, 1) + tag = bit32.rshift(tag, 1) + len += 1 + sum += t.table[len] + cur -= t.table[len] + until cur < 0 + + d.tag = tag + d.bitcount -= len + + return t.trans[sum + cur] +end + +--- Decodes the dynamic Huffman trees for a block +local function decodeTrees(d: Data, lengthTree: Tree, distTree: Tree) + local hlit = readBits(d, 5, 257) -- Number of literal/length codes + local hdist = readBits(d, 5, 1) -- Number of distance codes + local hclen = readBits(d, 4, 4) -- Number of code length codes + + -- Initialize code lengths array + for i = 0, 18 do + lengths[i] = 0 + end + + -- Read code lengths for the code length alphabet + for i = 0, hclen - 1 do + lengths[clcIndex[i + 1]] = readBits(d, 3, 0) + end + + -- Build the code lengths tree + buildTree(codeTree, lengths, 0, 19) + + -- Decode length/distance tree code lengths + local num = 0 + while num < hlit + hdist do + local sym = decodeSymbol(d, codeTree) + + if sym == 16 then + -- Copy previous code length 3-6 times + local prev = lengths[num - 1] + for _ = 1, readBits(d, 2, 3) do + lengths[num] = prev + num += 1 + end + elseif sym == 17 then + -- Repeat zero 3-10 times + for _ = 1, readBits(d, 3, 3) do + lengths[num] = 0 + num += 1 + end + elseif sym == 18 then + -- Repeat zero 11-138 times + for _ = 1, readBits(d, 7, 11) do + lengths[num] = 0 + num += 1 + end + else + -- Regular code length 0-15 + lengths[num] = sym + num += 1 + end + end + + -- Build the literal/length and distance trees + buildTree(lengthTree, lengths, 0, hlit) + buildTree(distTree, lengths, hlit, hdist) +end + +--- Inflates a block of data using Huffman trees +local function inflateBlockData(d: Data, lengthTree: Tree, distTree: Tree) + local dest = d.dest + local destLen = d.destLen + local destSize = buffer.len(dest) + + local lastGrowCheck = destLen + + while true do + -- OPT: inlined `decodeSymbol` for length tree + ensureBits(d, 15) + local entry = (lengthTree.lookup :: { number })[bit32.band(d.tag, LOOKUP_SIZE - 1)] + local len = bit32.band(entry, 0xFFFF) + local sym + + if len > 0 then + sym = bit32.rshift(entry, 16) + d.tag = bit32.rshift(d.tag, len) + d.bitcount -= len + else + -- Slow path + local sum, cur = 0, 0 + local tag = d.tag + len = 0 + + repeat + cur = 2 * cur + bit32.band(tag, 1) + tag = bit32.rshift(tag, 1) + len += 1 + sum += lengthTree.table[len] + cur -= lengthTree.table[len] + until cur < 0 + + d.tag = tag + d.bitcount -= len + sym = lengthTree.trans[sum + cur] + end + + -- OPT: literals are the most common symbols + if sym < 256 then + -- Check growth periodically + if destLen - lastGrowCheck >= 64 then + if destLen + 64 > destSize then + local newSize = destSize * 2 + local newDest = buffer.create(newSize) + + buffer.copy(newDest, 0, dest, 0, destLen) + + dest = newDest + d.dest = newDest + destSize = newSize + end + + lastGrowCheck = destLen + end + + buffer.writeu8(dest, destLen, sym) + destLen += 1 + elseif sym == 256 then + -- End of block + d.destLen = destLen + return + else + -- Length/distance pair for copying + sym -= 257 + + local length = readBits(d, lengthBits[sym], lengthBase[sym]) + + -- OPT: inlined `decodeSymbol` for distance tree + ensureBits(d, 15) + entry = (distTree.lookup :: { number })[bit32.band(d.tag, LOOKUP_SIZE - 1)] + len = bit32.band(entry, 0xFFFF) + + local dist + + if len > 0 then + dist = bit32.rshift(entry, 16) + d.tag = bit32.rshift(d.tag, len) + d.bitcount -= len + else + -- Slow path + local sum, cur = 0, 0 + local tag = d.tag + len = 0 + + repeat + cur = 2 * cur + bit32.band(tag, 1) + tag = bit32.rshift(tag, 1) + len += 1 + sum += distTree.table[len] + cur -= distTree.table[len] + until cur < 0 + + d.tag = tag + d.bitcount -= len + dist = distTree.trans[sum + cur] + end + + dist = readBits(d, distBits[dist], distBase[dist]) + local offs = destLen - dist + + -- Check if we need to grow the buffer + if destLen + length > destSize then + local newSize = math.max(destSize * 2, destLen + length) + local newDest = buffer.create(newSize) + + buffer.copy(newDest, 0, dest, 0, destLen) + + dest = newDest + d.dest = newDest + destSize = newSize + end + + -- Validate offset is within bounds + if offs < 0 then + error(`Invalid distance: trying to copy from offset {offs} (destLen={destLen}, dist={dist})`) + end + + -- OPT: Non-overlapping copies are common + if dist >= length then + -- We can directly memcpy + buffer.copy(dest, destLen, dest, offs, length) + destLen += length + elseif length <= 8 then + -- OPT: small overlapping copies, we can unroll upto 8 bytes manually, + -- prepare for extremely ugly code ahead + if length <= 4 then + buffer.writeu8(dest, destLen, buffer.readu8(dest, offs)) + buffer.writeu8(dest, destLen + 1, buffer.readu8(dest, offs + 1)) + + if length > 2 then + buffer.writeu8(dest, destLen + 2, buffer.readu8(dest, offs + 2)) + if length > 3 then + buffer.writeu8(dest, destLen + 3, buffer.readu8(dest, offs + 3)) + end + end + else + buffer.writeu8(dest, destLen, buffer.readu8(dest, offs)) + buffer.writeu8(dest, destLen + 1, buffer.readu8(dest, offs + 1)) + buffer.writeu8(dest, destLen + 2, buffer.readu8(dest, offs + 2)) + buffer.writeu8(dest, destLen + 3, buffer.readu8(dest, offs + 3)) + + if length > 4 then + buffer.writeu8(dest, destLen + 4, buffer.readu8(dest, offs + 4)) + if length > 5 then + buffer.writeu8(dest, destLen + 5, buffer.readu8(dest, offs + 5)) + if length > 6 then + buffer.writeu8(dest, destLen + 6, buffer.readu8(dest, offs + 6)) + if length > 7 then + buffer.writeu8(dest, destLen + 7, buffer.readu8(dest, offs + 7)) + end + end + end + end + end + destLen += length + elseif dist == 1 then + -- OPT: single byte repition is common + local byte = buffer.readu8(dest, offs) + for i = 0, length - 1 do + buffer.writeu8(dest, destLen + i, byte) + end + destLen += length + else + -- OPT: try chunked overlapping copies + local chunkSize = dist + local chunks = length // chunkSize + local remainder = length % chunkSize + + for _ = 1, chunks do + buffer.copy(dest, destLen, dest, offs, chunkSize) + destLen += chunkSize + offs += chunkSize + end + + if remainder > 0 then + for i = 0, remainder - 1 do + buffer.writeu8(dest, destLen + i, buffer.readu8(dest, offs + i)) + end + destLen += remainder + end + end + + lastGrowCheck = destLen + end + end +end + +--- Processes an uncompressed block +local function inflateUncompressedBlock(d: Data) + -- Align to byte boundary + local bytesToMove = d.bitcount // 8 + d.sourceIndex -= bytesToMove + d.bitcount = 0 + d.tag = 0 + + -- Read block length and its complement + local length = buffer.readu8(d.source, d.sourceIndex + 1) + length = 256 * length + buffer.readu8(d.source, d.sourceIndex) + + local invlength = buffer.readu8(d.source, d.sourceIndex + 3) + invlength = 256 * invlength + buffer.readu8(d.source, d.sourceIndex + 2) + + -- Verify block length using ones complement + if length ~= bit32.bxor(invlength, 0xffff) then + error("Invalid block length") + end + + d.sourceIndex += 4 + + -- Check if we need to grow the buffer + local destSize = buffer.len(d.dest) + if d.destLen + length > destSize then + local newSize = math.max(destSize * 2, d.destLen + length) + local newDest = buffer.create(newSize) + + buffer.copy(newDest, 0, d.dest, 0, d.destLen) + d.dest = newDest + end + + -- Use bulk copy for uncompressed data + buffer.copy(d.dest, d.destLen, d.source, d.sourceIndex, length) + d.destLen += length + d.sourceIndex += length + + d.bitcount = 0 +end + +--- Main decompression function that processes DEFLATE compressed data +local function uncompress(source: buffer, uncompressedSize: number?): buffer + local dest = buffer.create( + -- If the uncompressed size is known, we use that, otherwise we use a default + -- size that is a 7 times more than the compressed size; this factor works + -- well for most cases other than those with a very high compression ratio + if uncompressedSize and uncompressedSize > 0 then uncompressedSize else buffer.len(source) * 7 + ) + local d = Data.new(source, dest) + + repeat + local bfinal = getBit(d) -- Last block flag + local btype = readBits(d, 2, 0) -- Block type (0=uncompressed, 1=fixed, 2=dynamic) + + if btype == 0 then + inflateUncompressedBlock(d) + elseif btype == 1 then + inflateBlockData(d, staticLengthTree, staticDistTree) + elseif btype == 2 then + decodeTrees(d, d.ltree, d.dtree) + inflateBlockData(d, d.ltree, d.dtree) + else + error("Invalid block type") + end + until bfinal == 1 + + -- Trim output buffer to actual size if needed + if d.destLen < buffer.len(d.dest) then + local result = buffer.create(d.destLen) + buffer.copy(result, 0, d.dest, 0, d.destLen) + return result + end + + return d.dest +end + +-- Initialize static trees and lookup tables for DEFLATE format +buildFixedTrees(staticLengthTree, staticDistTree) +buildLookupTable(staticLengthTree) +buildLookupTable(staticDistTree) +buildBitsBase(lengthBits, lengthBase, 4, 3) +buildBitsBase(distBits, distBase, 2, 1) +lengthBits[28] = 0 +lengthBase[28] = 258 + +return uncompress diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau new file mode 100644 index 000000000..1d48cea03 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau @@ -0,0 +1,1176 @@ +local inflate = require("@self/inflate") +local validateCrc = require("@self/utils/validate_crc") +local crc32 = require("@self/crc") +local path = require("@self/utils/path") + +-- The maximum supported PKZIP format specification version that we support +local MAX_SUPPORTED_PKZIP_VERSION = 63 + +-- Little endian constant signatures used in the ZIP file format +local SIGNATURES = table.freeze({ + -- Marks the beginning of each file in the ZIP + LOCAL_FILE = 0x04034b50, + -- Marks the start of a data descriptor + DATA_DESCRIPTOR = 0x08074b50, + -- Marks entries in the central directory + CENTRAL_DIR = 0x02014b50, + -- Marks the end of the central directory + END_OF_CENTRAL_DIR = 0x06054b50, +}) + +-- Set of placeholder entry properties for incompatible entries +local EMPTY_PROPERTIES: ZipEntryProperties = table.freeze({ + versionMadeBy = 0, + compressedSize = 0, + size = 0, + attributes = 0, + timestamp = 0, + crc = 0, +} :: any) -- FIXME(Luau): bidirectional typing breaks with table.freeze + +-- Lookup table for the OS that created the ZIP +local MADE_BY_OS_LOOKUP: { [number]: MadeByOS } = { + [0x0] = "FAT", + [0x1] = "AMIGA", + [0x2] = "VMS", + [0x3] = "UNIX", + [0x4] = "VM/CMS", + [0x5] = "Atari ST", + [0x6] = "OS/2", + [0x7] = "MAC", + [0x8] = "Z-System", + [0x9] = "CP/M", + [0xa] = "NTFS", + [0xb] = "MVS", + [0xc] = "VSE", + [0xd] = "Acorn RISCOS", + [0xe] = "VFAT", + [0xf] = "Alternate MVS", + [0x10] = "BeOS", + [0x11] = "TANDEM", + [0x12] = "OS/400", + [0x13] = "OS/X", +} + +--[=[ + @class ZipEntry + + A single entry (a file or a directory) in a ZIP file, and its properties. +]=] +local ZipEntry = {} + +--[=[ + @interface ZipEntry + @within ZipEntry + + @field name string -- File path within ZIP, '/' suffix indicates directory + @field versionMadeBy { software: string, os: MadeByOS } -- Version of software and OS that created the ZIP + @field compressedSize number -- Compressed size in bytes + @field size number -- Uncompressed size in bytes + @field offset number -- Absolute position of local header in ZIP + @field timestamp number -- MS-DOS format timestamp + @field method CompressionMethod -- Method used to compress the file + @field crc number -- CRC32 checksum of the uncompressed data + @field isDirectory boolean -- Whether the entry is a directory or not + @field isText boolean -- Whether the entry is plain ASCII text or binary + @field attributes number -- File attributes + @field parent ZipEntry? -- Parent directory entry, `nil` if entry is root + @field children { ZipEntry } -- Children of the entry, if it was a directory, empty array for files +]=] +export type ZipEntry = typeof(setmetatable({} :: ZipEntryInner, { __index = ZipEntry })) +type ZipEntryInner = { + name: string, + + versionMadeBy: { + software: string, + os: MadeByOS, + }, + + compressedSize: number, + size: number, + offset: number, + timestamp: number, + method: CompressionMethod, + crc: number, + isDirectory: boolean, + isText: boolean, + attributes: number, + parent: ZipEntry?, + children: { ZipEntry }, +} + +-- stylua: ignore +--[=[ + @within ZipEntry + @type MadeByOS "FAT" | "AMIGA" | "VMS" | "UNIX" | "VM/CMS" | "Atari ST" | "OS/2" | "MAC" | "Z-System" | "CP/M" | "NTFS" | "MVS" | "VSE" | "Acorn RISCOS" | "VFAT" | "Alternate MVS" | "BeOS" | "TANDEM" | "OS/400" | "OS/X" | "Unknown" + + The OS that created the ZIP. +]=] +export type MadeByOS = + | "FAT" -- 0x0; MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) + | "AMIGA" -- 0x1; Amiga + | "VMS" -- 0x2; OpenVMS + | "UNIX" -- 0x3; Unix + | "VM/CMS" -- 0x4; VM/CMS + | "Atari ST" -- 0x5; Atari ST + | "OS/2" -- 0x6; OS/2 HPFS + | "MAC" -- 0x7; Macintosh + | "Z-System" -- 0x8; Z-System + | "CP/M" -- 0x9; Original CP/M + | "NTFS" -- 0xa; Windows NTFS + | "MVS" -- 0xb; OS/390 & VM/ESA + | "VSE" -- 0xc; VSE + | "Acorn RISCOS" -- 0xd; Acorn RISCOS + | "VFAT" -- 0xe; VFAT + | "Alternate MVS" -- 0xf; Alternate MVS + | "BeOS" -- 0x10; BeOS + | "TANDEM" -- 0x11; Tandem + | "OS/400" -- 0x12; OS/400 + | "OS/X" -- 0x13; Darwin + | "Unknown" -- 0x14 - 0xff; Unused + +-- stylua: ignore +--[=[ + @within ZipEntry + @type CompressionMethod "STORE" | "SHRUNK" | "REDUCE_F1" | "REDUCE_F2" | "REDUCE_F3" | "REDUCE_F4" | "IMPLODE" | "TOKENIZE" | "DEFLATE" | "DEFLATE64" | "IMPLODE_DCL" | "BZIP2" | "LZMA" | "CMPSC" | "TERSE" | "LZ77" | "ZSTD" | "MP3" | "XZ" | "JPEG" | "WAVPACK" | "PPMD" | "AE_X" + + The method used to compress the file. See PKWARE APPNOTE §4.4.5 for details. + + | Method | Code | Description | + |---------------|------|---------------------------| + | `STORE` | 0 | No compression | + | `SHRUNK` | 1 | Shrunk | + | `REDUCE_F1` | 2 | Reduced factor 1 | + | `REDUCE_F2` | 3 | Reduced factor 2 | + | `REDUCE_F3` | 4 | Reduced factor 3 | + | `REDUCE_F4` | 5 | Reduced factor 4 | + | `IMPLODE` | 6 | Imploded | + | `TOKENIZE` | 7 | Reserved for Tokenizing | + | `DEFLATE` | 8 | Deflated (supported) | + | `DEFLATE64` | 9 | Enhanced Deflating | + | `IMPLODE_DCL` | 10 | PKWARE DCL Imploding | + | `BZIP2` | 12 | BZIP2 | + | `LZMA` | 14 | LZMA | + | `CMPSC` | 16 | IBM z/OS CMPSC | + | `TERSE` | 18 | IBM TERSE (new) | + | `LZ77` | 19 | IBM LZ77 z Architecture | + | `ZSTD` | 93 | Zstandard | + | `MP3` | 94 | MP3 Compression | + | `XZ` | 95 | XZ Compression | + | `JPEG` | 96 | JPEG variant | + | `WAVPACK` | 97 | WavPack | + | `PPMD` | 98 | PPMd version I, Rev 1 | + | `AE_X` | 99 | AE-x encryption marker | + + Only `STORE` and `DEFLATE` are supported for extraction by this library. + Custom decompression routines can be registered via [ZipReader.new] to support + additional methods. +]=] +export type CompressionMethod = + | "STORE" -- 0x00: No compression + | "SHRUNK" -- 0x01: Shrunk + | "REDUCE_F1" -- 0x02: Reduced factor 1 + | "REDUCE_F2" -- 0x03: Reduced factor 2 + | "REDUCE_F3" -- 0x04: Reduced factor 3 + | "REDUCE_F4" -- 0x05: Reduced factor 4 + | "IMPLODE" -- 0x06: Imploded + | "TOKENIZE" -- 0x07: Reserved for Tokenizing + | "DEFLATE" -- 0x08: Deflated (supported) + | "DEFLATE64" -- 0x09: Enhanced Deflating + | "IMPLODE_DCL" -- 0x0A: PKWARE DCL Imploding + | "BZIP2" -- 0x0C: BZIP2 + | "LZMA" -- 0x0E: LZMA + | "CMPSC" -- 0x10: IBM z/OS CMPSC + | "TERSE" -- 0x12: IBM TERSE (new) + | "LZ77" -- 0x13: IBM LZ77 z Architecture + | "ZSTD" -- 0x5D: Zstandard + | "MP3" -- 0x5E: MP3 Compression + | "XZ" -- 0x5F: XZ Compression + | "JPEG" -- 0x60: JPEG variant + | "WAVPACK" -- 0x61: WavPack + | "PPMD" -- 0x62: PPMd version I, Rev 1 + | "AE_X" -- 0x63: AE-x encryption marker + +--[=[ + @interface ZipEntryProperties + @within ZipEntry + @private + + A set of properties that describe a ZIP entry. Used internally for construction of + [ZipEntry] objects. + + @field versionMadeBy number -- Version of software and OS that created the ZIP + @field compressedSize number -- Compressed size in bytes + @field size number -- Uncompressed size in bytes + @field attributes number -- File attributes + @field timestamp number -- MS-DOS format timestamp + @field method CompressionMethod? -- Method used + @field crc number -- CRC32 checksum of the uncompressed data +]=] +type ZipEntryProperties = { + versionMadeBy: number, + compressedSize: number, + size: number, + attributes: number, + timestamp: number, + method: CompressionMethod?, + crc: number, +} + +--[=[ + @within ZipEntry + @function new + @private + + @param offset number -- Offset of the entry in the ZIP file + @param name string -- File path within ZIP, '/' suffix indicates directory + @param properties ZipEntryProperties -- Properties of the entry + @return ZipEntry -- The constructed entry +]=] +function ZipEntry.new(offset: number, name: string, properties: ZipEntryProperties): ZipEntry + local versionMadeByOS = bit32.rshift(properties.versionMadeBy, 8) + local versionMadeByVersion = bit32.band(properties.versionMadeBy, 0x00ff) + + return setmetatable( + { + name = name, + versionMadeBy = { + software = string.format("%d.%d", versionMadeByVersion / 10, versionMadeByVersion % 10), + os = MADE_BY_OS_LOOKUP[versionMadeByOS] :: MadeByOS, + }, + compressedSize = properties.compressedSize, + size = properties.size, + offset = offset, + timestamp = properties.timestamp, + method = properties.method, + crc = properties.crc, + isDirectory = string.sub(name, -1) == "/", + attributes = properties.attributes, + parent = nil, + children = {}, + } :: ZipEntryInner, + { __index = ZipEntry } + ) +end + +--[=[ + @within ZipEntry + @method isSymlink + + Returns whether the entry is a symlink. + + @return boolean +]=] +function ZipEntry.isSymlink(self: ZipEntry): boolean + return bit32.band(self.attributes, 0xA0000000) == 0xA0000000 +end + +--[=[ + @within ZipEntry + @method getPath + + Resolves the path of the entry based on its relationship with other entries. It is recommended to use this + method instead of accessing the `name` property directly, although they should be equivalent. + + > [!WARNING] + > Never use this method when extracting files from the ZIP, since it can contain absolute paths + > (say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`), + > causing unintended overwrites of files. + + @return string -- The path of the entry +]=] +function ZipEntry.getPath(self: ZipEntry): string + if self.name == "/" then + return "/" + end + + -- Get just the entry name without the path + local name = string.match(self.name, "([^/]+)/?$") or self.name + + if not self.parent or self.parent.name == "/" then + return self.name + end + + -- Combine parent path with entry name + local path = string.gsub(self.parent:getPath() .. name, "//+", "/") + return path +end + +--[=[ + @within ZipEntry + @method getSafePath + + Resolves the path of the entry based on its relationship with other entries and returns it + only if it is safe to use for extraction, otherwise returns `nil`. + + @return string? -- Optional path of the entry if it was safe +]=] +function ZipEntry.getSafePath(self: ZipEntry): string? + local pathStr = self:getPath() + + if path.isSafe(pathStr) then + return pathStr + end + + return nil +end + +--[=[ + @within ZipEntry + @method sanitizePath + + Sanitizes the path of the entry, potentially losing information, but ensuring the path is + safe to use for extraction. + + @return string -- The sanitized path of the entry +]=] +function ZipEntry.sanitizePath(self: ZipEntry): string + local pathStr = self:getPath() + return path.sanitize(pathStr) +end + +--[=[ + @within ZipEntry + @method compressionEfficiency + + Calculates the compression efficiency of the entry, or `nil` if the entry is a directory. + + Uses the formula: `round((1 - compressedSize / size) * 100)` and outputs a percentage. + + @return number? -- Optional compression efficiency of the entry +]=] +function ZipEntry.compressionEfficiency(self: ZipEntry): number? + if self.size == 0 or self.compressedSize == 0 then + return nil + end + + local ratio = 1 - self.compressedSize / self.size + return math.round(ratio * 100) +end + +--[=[ + @within ZipEntry + @method isFile + + Returns whether the entry is a file, i.e., not a directory or symlink. + + @return boolean -- Whether the entry is a file +]=] +function ZipEntry.isFile(self: ZipEntry): boolean + return not (self.isDirectory and self:isSymlink()) +end + +--[=[ + @within ZipEntry + @interface UnixMode + + A object representation of the UNIX mode. + + @field perms string -- The permission octal + @field typeFlags string -- The type flags octal +]=] +export type UnixMode = { perms: string, typeFlags: string } + +--[=[ + @within ZipEntry + @method unixMode + + Parses the entry's attributes to extract a UNIX mode, represented as a [UnixMode]. + + @return UnixMode? -- The UNIX mode of the entry, or `nil` if the entry is not a UNIX file +]=] +function ZipEntry.unixMode(self: ZipEntry): UnixMode? + if self.versionMadeBy.os ~= "UNIX" then + return nil + end + + local mode = bit32.rshift(self.attributes, 16) + local typeFlags = bit32.band(self.attributes, 0x1FF) + local perms = bit32.band(mode, 0x01FF) + + return { + perms = string.format("0o%o", perms), + typeFlags = string.format("0o%o", typeFlags), + } +end + +--[=[ + @class ZipReader + + The main class which represents a decoded state of a ZIP file, holding references + to its entries. This is the primary point of interaction with the ZIP file's contents. +]=] +local ZipReader = {} + +--[=[ + @interface ZipReader + @within ZipReader + + @field data buffer -- The buffer containing the raw bytes of the ZIP + @field comment string -- Comment associated with the ZIP + @field entries { ZipEntry } -- The decoded entries present + @field directories { [string]: ZipEntry } -- The directories and their respective entries + @field root ZipEntry -- The entry of the root directory +]=] +export type ZipReader = typeof(setmetatable({} :: ZipReaderInner, { __index = ZipReader })) +type ZipReaderInner = { + data: buffer, + comment: string, + entries: { ZipEntry }, + directories: { [string]: ZipEntry }, + root: ZipEntry, + decompressionRoutines: DecompressionRoutinesWithDefaults, +} + +--[=[ + @interface CrcValidationOptions + @within ZipReader + + Options passed to a decompression routine regarding CRC checksum + validation. + + @field expected number -- The expected hash provided in the ZIP + @field skip boolean -- Whether to skip validation altogether +]=] +export type CrcValidationOptions = validateCrc.CrcValidationOptions + +--[=[ + @interface DecompressionRoutine + @within ZipReader + + Represents a compression method's decompression implementation. + + A default implementation is provided for all of [CompressionMethod]'s + variants but this may be used to override or provide implementations for + unsupported compression methods. + + Accepted as an argument for [ZipReader.new]. +]=] +export type DecompressionRoutine = { + name: CompressionMethod, + decompress: (buffer, number, CrcValidationOptions) -> buffer, +} + +--[=[ + @interface DecompressionRoutines + @within ZipReader + @private + + Implementations for decompression methods, keyed by the method's ID. + + See [CompressionMethod] to find the ID for a compression method. +]=] +type DecompressionRoutines = { [number]: DecompressionRoutine } +type DecompressionRoutinesWithDefaults = typeof(setmetatable( + {} :: DecompressionRoutines, + { __index = {} :: DecompressionRoutines } +)) + +-- Default implementations for supported decompression routines +local DEFAULT_DECOMPRESSION_ROUTINES: DecompressionRoutines = { + -- `STORE` decompression method - No compression + [0x00] = { + name = "STORE", + decompress = function(buf, _, validation): buffer + validateCrc(buf, validation) + return buf + end, + }, + + -- `DEFLATE` decompression method - Compressed raw deflate chunks + [0x08] = { + name = "DEFLATE", + decompress = function(buf, uncompressedSize, validation): buffer + local decompressed = inflate(buf, uncompressedSize) + validateCrc(decompressed, validation) + return decompressed + end, + }, +} + +--[=[ + @within ZipReader + @function new + + Creates a new ZipReader instance from the raw bytes of a ZIP file. + + **Errors if the ZIP file is invalid.** + + @param data buffer -- The buffer containing the raw bytes of the ZIP + @param methods DecompressionRoutines? -- Custom implementations for decompression methods + @return ZipReader -- The new ZipReader instance +]=] +function ZipReader.new(data: buffer, methods: DecompressionRoutines?): ZipReader + local root = ZipEntry.new(0, "/", EMPTY_PROPERTIES) + root.isDirectory = true + + local decompressionRoutines = setmetatable((methods or {}) :: DecompressionRoutines, { + __index = DEFAULT_DECOMPRESSION_ROUTINES, + }) :: DecompressionRoutinesWithDefaults + + local this = setmetatable( + { + data = data, + entries = {}, + directories = {}, + root = root, + decompressionRoutines = decompressionRoutines, + } :: ZipReaderInner, + { __index = ZipReader } + ) + + this:parseCentralDirectory() + this:buildDirectoryTree() + return this +end + +--[=[ + @within ZipReader + @method findEocdPosition + @private + + Finds the position of the End of Central Directory (EoCD) signature in the ZIP file. This + implementation is inspired by that of [async_zip], a Rust library for parsing ZIP files + asynchronously. + + This method involves buffered reading in reverse and reverse linear searching along those buffers + for the EoCD signature. As a result of the buffered approach, we reduce individual reads when compared + to reading every single byte sequentially, by a factor of the buffer size (4 KB by default). The buffer + size of 4 KB was arrived at because it aligns with many systems' page sizes, and also provides a + good balance between read efficiency (not too small), memory usage (not too large) and CPU cache + performance. + + From my primitive benchmarks, this method is ~1.5x faster than the sequential approach. + + **Errors if the ZIP file is invalid.** + + [async_zip]: https://github.com/Majored/rs-async-zip/blob/527bda9/src/base/read/io/locator.rs#L37-L45 + + @error "Could not find End of Central Directory signature" + + @return number -- The offset to the End of Central Directory (including the signature) +]=] +function ZipReader.findEocdPosition(self: ZipReader): number + local BUFFER_SIZE = 4096 + local SIGNATURE_LENGTH = 4 + local bufSize = buffer.len(self.data) + + -- Start from the minimum possible position of EoCD (22 bytes from end) + local position = math.max(0, bufSize - (22 + 65536) --[[ max comment size: 64 KB ]]) + local searchBuf = buffer.create(BUFFER_SIZE) + + while position < bufSize do + local readSize = math.min(BUFFER_SIZE, bufSize - position) + buffer.copy(searchBuf, 0, self.data, position, readSize) + + -- Search backwards through buffer for signature + for i = readSize - 1, SIGNATURE_LENGTH - 1, -1 do + if buffer.readu32(searchBuf, i - SIGNATURE_LENGTH + 1) == SIGNATURES.END_OF_CENTRAL_DIR then + return position + i - SIGNATURE_LENGTH + 1 + end + end + + -- Move position backward with overlap for cross-boundary signatures + position += BUFFER_SIZE - SIGNATURE_LENGTH + end + + error("Could not find End of Central Directory signature") +end + +--[=[ + @within ZipReader + @interface EocdRecord + @private + + A parsed End of Central Directory record. + + @field diskNumber number -- The disk number + @field diskWithCD number -- The disk number of the disk with the Central Directory + @field cdEntries number -- The number of entries in the Central Directory + @field totalCDEntries number -- The total number of entries in the Central Directory + @field cdSize number -- The size of the Central Directory + @field cdOffset number -- The offset of the Central Directory + @field comment string -- The comment associated with the ZIP +]=] +export type EocdRecord = { + diskNumber: number, + diskWithCD: number, + cdEntries: number, + totalCDEntries: number, + cdSize: number, + cdOffset: number, + comment: string, +} + +--[=[ + @within ZipReader + @method parseEocdRecord + @private + + Parses the End of Central Directory record at the given position, usually located + using the [ZipReader:findEocdPosition]. + + **Errors if the ZIP file is invalid.** + + @error "Invalid Central Directory offset or size" + + @param pos number -- The offset to the End of Central Directory record + @return EocdRecord -- Structural representation of the parsed record +]=] +function ZipReader.parseEocdRecord(self: ZipReader, pos: number): EocdRecord + -- End of Central Directory format: + -- Offset Bytes Description + -- 0 4 End of central directory signature + -- 4 2 Number of this disk + -- 6 2 Disk where central directory starts + -- 8 2 Number of central directory records on this disk + -- 10 2 Total number of central directory records + -- 12 4 Size of central directory (bytes) + -- 16 4 Offset of start of central directory + -- 20 2 Comment length (n) + -- 22 n Comment + + local cdEntries = buffer.readu16(self.data, pos + 10) + local cdSize = buffer.readu32(self.data, pos + 12) + local cdOffset = buffer.readu32(self.data, pos + 16) + + -- Validate CD boundaries and entry count + local bufSize = buffer.len(self.data) + if cdOffset >= bufSize or cdOffset + cdSize > bufSize then + error("Invalid Central Directory offset or size") + end + + -- Validate CD size range; min = 46 bytes per entry, max = 0xFFFF * 3 + 46 bytes per entry + if cdSize < cdEntries * 46 or cdEntries * (0xFFFF * 3 + 46) < cdSize then + error("Invalid Central Directory size for claimed number of entries") + end + + local commentLength = buffer.readu16(self.data, pos + 20) + return { + diskNumber = buffer.readu16(self.data, pos + 4), + diskWithCD = buffer.readu16(self.data, pos + 6), + cdEntries = cdEntries, + totalCDEntries = buffer.readu16(self.data, pos + 8), + cdSize = cdSize, + cdOffset = cdOffset, + comment = buffer.readstring(self.data, pos + 22, commentLength), + } +end + +--[=[ + @within ZipReader + @method parseCentralDirectory + @private + + Parses the central directory of the ZIP file and populates the `entries` and `directories` + fields. Used internally during initialization of the [ZipReader]. + + **Errors if the ZIP file is invalid.** + + @error "Invalid Central Directory entry signature" + @error "Found different entries than specified in Central Directory" +]=] +function ZipReader.parseCentralDirectory(self: ZipReader): () + local eocdPos = self:findEocdPosition() + local record = self:parseEocdRecord(eocdPos) + + -- Track actual entries found + local entriesFound = 0 + local pos = record.cdOffset + while pos < record.cdOffset + record.cdSize do + if buffer.readu32(self.data, pos) ~= SIGNATURES.CENTRAL_DIR then + error("Invalid Central Directory entry signature") + end + + -- Central Directory Entry format: + -- Offset Bytes Description + -- 0 4 Central directory entry signature + -- 4 2 Version made by + -- 8 2 General purpose bitflags + -- 10 2 Compression method (8 = DEFLATE) + -- 12 4 Last mod time/date + -- 16 4 CRC-32 + -- 20 4 Compressed size + -- 24 4 Uncompressed size + -- 28 2 File name length (n) + -- 30 2 Extra field length (m) + -- 32 2 Comment length (k) + -- 36 2 Internal file attributes + -- 38 4 External file attributes + -- 42 4 Local header offset + -- 46 n File name + -- 46+n m Extra field + -- 46+n+m k Comment + + local versionMadeBy = buffer.readu16(self.data, pos + 4) + local _bitflags = buffer.readu16(self.data, pos + 8) + local timestamp = buffer.readu32(self.data, pos + 12) + local compressionMethod = buffer.readu16(self.data, pos + 10) + local crc = buffer.readu32(self.data, pos + 16) + local compressedSize = buffer.readu32(self.data, pos + 20) + local size = buffer.readu32(self.data, pos + 24) + local nameLength = buffer.readu16(self.data, pos + 28) + local extraLength = buffer.readu16(self.data, pos + 30) + local commentLength = buffer.readu16(self.data, pos + 32) + local internalAttrs = buffer.readu16(self.data, pos + 36) + local externalAttrs = buffer.readu32(self.data, pos + 38) + local offset = buffer.readu32(self.data, pos + 42) + local name = buffer.readstring(self.data, pos + 46, nameLength) + + local entrySize = 46 + nameLength + extraLength + commentLength + if pos + entrySize > record.cdOffset + record.cdSize then + error("Invalid Central Directory entry size") + end + + table.insert( + self.entries, + ZipEntry.new(offset, name, { + versionMadeBy = versionMadeBy, + compressedSize = compressedSize, + size = size, + crc = crc, + method = self.decompressionRoutines[compressionMethod].name :: CompressionMethod, + timestamp = timestamp, + attributes = externalAttrs, + isText = bit32.btest(internalAttrs, 0x0001), + }) + ) + + pos = pos + 46 + nameLength + extraLength + commentLength + entriesFound += 1 + end + + if entriesFound ~= record.cdEntries then + error("Found different entries than specified in Central Directory") + end + + self.comment = record.comment +end + +--[=[ + @within ZipReader + @method buildDirectoryTree + @private + + Builds the directory tree from the entries. Used internally during initialization of the + [ZipReader]. +]=] +function ZipReader.buildDirectoryTree(self: ZipReader): () + -- Sort entries to process directories first; I could either handle + -- directories and files in separate passes over the entries, or sort + -- the entries so I handled the directories first -- I decided to do + -- the latter + table.sort(self.entries, function(a: ZipEntry, b: ZipEntry) + if a.isDirectory ~= b.isDirectory then + return a.isDirectory + end + return a.name < b.name + end) + + for _, entry in self.entries do + local parts = {} + -- Split entry path into individual components + -- e.g. "folder/subfolder/file.txt" -> {"folder", "subfolder", "file.txt"} + for part in string.gmatch(entry.name, "([^/]+)/?") do + table.insert(parts, part) + end + + -- Start from root directory + local current = self.root + local path = "" + + -- Process each path component + for i, part in parts do + path ..= part + + if i < #parts or entry.isDirectory then + -- Create missing directory entries for intermediate paths + if not self.directories[path] then + if entry.isDirectory and i == #parts then + -- Existing directory entry, reuse it + self.directories[path] = entry + else + -- Create new directory entry for intermediate paths or undefined + -- parent directories in the ZIP + local dir = ZipEntry.new(0, path .. "/", { + versionMadeBy = 0, + compressedSize = 0, + size = 0, + crc = 0, + compressionMethod = "STORED", + timestamp = entry.timestamp, + attributes = entry.attributes, + }) + + dir.versionMadeBy = entry.versionMadeBy + dir.isDirectory = true + dir.parent = current + self.directories[path] = dir + end + + -- Track directory in both lookup table and parent's children + table.insert(current.children, self.directories[path]) + end + + -- Move deeper into the tree + current = self.directories[path] + continue + end + + -- Link file entry to its parent directory + entry.parent = current + table.insert(current.children, entry) + end + end +end + +--[=[ + @within ZipReader + @method findEntry + + Finds a [ZipEntry] by its path in the ZIP archive. + + @param path string -- Path to the entry to find + @return ZipEntry? -- The found entry, or `nil` if not found +]=] +function ZipReader.findEntry(self: ZipReader, path: string): ZipEntry? + if path == "/" then + -- If the root directory's entry was requested we do not + -- need to do any additional work + return self.root + end + + -- Normalize path by removing leading and trailing slashes + -- This ensures consistent lookup regardless of input format + -- e.g., "/folder/file.txt/" -> "folder/file.txt" + path = string.gsub(path, "^/", ""):gsub("/$", "") + + -- First check regular files and explicit directories + for _, entry in self.entries do + -- Compare normalized paths + if string.gsub(entry.name, "/$", "") == path then + return entry + end + end + + -- If not found, check virtual directory entries + -- These are directories that were created implicitly + return self.directories[path] +end + +--[=[ + @interface ExtractionOptions + @within ZipReader + @ignore + + Options accepted by the [ZipReader:extract] method. + + @field followSymlinks boolean? -- Whether to follow symlinks + @field decompress boolean -- Whether to decompress the entry or only return the raw data + @field type ("binary" | "text")? -- The type of data to return, automatically inferred based on the type of contents if not specified + @field skipCrcValidation boolean? -- Whether to skip CRC validation + @field skipSizeValidation boolean? -- Whether to skip size validation +]=] +type ExtractionOptions = { + followSymlinks: boolean?, + decompress: boolean?, + type: ("binary" | "text")?, + skipCrcValidation: boolean?, + skipSizeValidation: boolean?, +} + +--[=[ + @within ZipReader + @method extract + + Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for + extracting directories. + + @error "Cannot extract directory" -- If the entry is a directory, use [ZipReader:extractDirectory] instead + @error "Invalid local file header" -- Invalid ZIP file, local header signature did not match + @error "Unsupported PKZip spec version: {versionNeeded}" -- The ZIP file was created with an unsupported version of the ZIP specification + @error "Symlink path not found" -- If `followSymlinks` of options is `true` and the symlink path was not found + @error "Unsupported compression, ID: {compressionMethod}" -- The entry was compressed using an unsupported compression method + + @param entry ZipEntry -- The entry to extract + @param options ExtractionOptions? -- Options for the extraction + @return buffer | string -- The extracted data +]=] +function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: ExtractionOptions?): buffer | string + -- Local File Header format: + -- Offset Bytes Description + -- 0 4 Local file header signature + -- 4 2 Version needed to extract + -- 6 2 General purpose bitflags + -- 8 2 Compression method (8 = DEFLATE) + -- 14 4 CRC32 checksum + -- 18 4 Compressed size + -- 22 4 Uncompressed size + -- 26 2 File name length (n) + -- 28 2 Extra field length (m) + -- 30 n File name + -- 30+n m Extra field + -- 30+n+m - File data + + if entry.isDirectory then + error("Cannot extract directory") + end + + local defaultOptions: ExtractionOptions = { + followSymlinks = false, + decompress = true, + type = if entry.isText then "text" else "binary", + skipValidation = false, + } + + -- TODO: Use a `Partial` type function for this in the future! + local optionsOrDefault: { + followSymlinks: boolean, + decompress: boolean, + type: "binary" | "text", + skipCrcValidation: boolean, + skipSizeValidation: boolean, + } = if options + then setmetatable(options, { __index = defaultOptions }) :: any + else defaultOptions :: any + + local pos = entry.offset + if buffer.readu32(self.data, pos) ~= SIGNATURES.LOCAL_FILE then + error("Invalid local file header") + end + + -- Validate that the version needed to extract is supported + local versionNeeded = buffer.readu16(self.data, pos + 4) + assert(MAX_SUPPORTED_PKZIP_VERSION >= versionNeeded, `Unsupported PKZip spec version: {versionNeeded}`) + + local bitflags = buffer.readu16(self.data, pos + 6) + local crcChecksum = buffer.readu32(self.data, pos + 14) + local compressedSize = buffer.readu32(self.data, pos + 18) + local uncompressedSize = buffer.readu32(self.data, pos + 22) + local nameLength = buffer.readu16(self.data, pos + 26) + local extraLength = buffer.readu16(self.data, pos + 28) + + pos = pos + 30 + nameLength + extraLength + + if bit32.btest(bitflags, 0x08) then + -- The bit at offset 3 was set, meaning we did not have the file sizes + -- and CRC checksum at the time of the creation of the ZIP. Instead, they + -- were appended after the compressed data chunks in a data descriptor + + -- Data Descriptor format: + -- Offset Bytes Description + -- 0 0 or 4 0x08074b50 (optional signature) + -- 0 or 4 4 CRC32 checksum + -- 4 or 8 4 Compressed size + -- 8 or 12 4 Uncompressed size + + -- Start at the compressed data + local descriptorPos = pos + while true do + -- Try reading a u32 starting from current offset + local leading = buffer.readu32(self.data, descriptorPos) + + if leading == SIGNATURES.DATA_DESCRIPTOR then + -- If we find a data descriptor signature, that must mean + -- the current offset points to the start of the descriptor + break + end + + if leading == entry.crc then + -- If we find our file's CRC checksum, that means the data + -- descriptor signature was omitted, so our chunk starts 4 + -- bytes before + descriptorPos -= 4 + break + end + + -- Skip to the next byte + descriptorPos += 1 + end + + crcChecksum = buffer.readu32(self.data, descriptorPos + 4) + compressedSize = buffer.readu32(self.data, descriptorPos + 8) + uncompressedSize = buffer.readu32(self.data, descriptorPos + 12) + end + + local content = buffer.create(compressedSize) + buffer.copy(content, 0, self.data, pos, compressedSize) + + if optionsOrDefault.decompress then + local compressionMethod = buffer.readu16(self.data, entry.offset + 8) + local algo = self.decompressionRoutines[compressionMethod] + if algo == nil then + error(`Unsupported compression, ID: {compressionMethod}`) + end + + if optionsOrDefault.followSymlinks then + local linkPath = buffer.tostring(algo.decompress(content, 0, { + expected = 0x00000000, + skip = true, + })) + + -- Check if the path was a relative path + if path.isRelative(linkPath) then + if string.sub(linkPath, -1) ~= "/" then + linkPath ..= "/" + end + + linkPath = path.canonicalize(`{(entry.parent or self.root).name}{linkPath}`) + end + + optionsOrDefault.followSymlinks = false + optionsOrDefault.type = "binary" + optionsOrDefault.skipCrcValidation = true + optionsOrDefault.skipSizeValidation = true + content = self:extract( + self:findEntry(linkPath) or error("Symlink path not found"), + optionsOrDefault :: ExtractionOptions + ) :: buffer + end + + content = algo.decompress(content, uncompressedSize, { + expected = crcChecksum, + skip = optionsOrDefault.skipCrcValidation, + }) + + -- Unless skipping validation is requested, we make sure the uncompressed size matches + assert( + optionsOrDefault.skipSizeValidation or uncompressedSize == buffer.len(content), + "Validation failed; uncompressed size does not match" + ) + end + + return if optionsOrDefault.type == "text" then buffer.tostring(content) else content +end + +--[=[ + @within ZipReader + @method extractDirectory + + Extracts all the files in a specified directory, skipping any directory entries. + + **Errors if [ZipReader:extract] errors on an entry in the directory.** + + @param path string -- The path to the directory to extract + @param options ExtractionOptions? -- Options for the extraction + @return { [string]: buffer } | { [string]: string } -- A map of extracted file paths and their contents +]=] +function ZipReader.extractDirectory( + self: ZipReader, + path: string, + options: ExtractionOptions? +): { [string]: buffer } | { [string]: string } + local files: { [string]: buffer } | { [string]: string } = {} + -- Normalize path by removing leading slash for consistent prefix matching + path = string.gsub(path, "^/", "") + + -- Iterate through all entries to find files within target directory + for _, entry in self.entries do + -- Check if entry is a file (not directory) and its path starts with target directory + if not entry.isDirectory and string.sub(entry.name, 1, #path) == path then + -- Store extracted content mapped to full path + files[entry.name] = self:extract(entry, options) + end + end + + -- Return a map of file to contents + return files +end + +--[=[ + @within ZipReader + @method listDirectory + + Lists the entries within a specified directory path. + + @error "Not a directory" -- If the path does not exist or is not a directory + + @param path string -- The path to the directory to list + @return { ZipEntry } -- The list of entries in the directory +]=] +function ZipReader.listDirectory(self: ZipReader, path: string): { ZipEntry } + -- Locate the entry with the path + local entry = self:findEntry(path) + if not entry or not entry.isDirectory then + -- If an entry was not found, we error + error("Not a directory") + end + + -- Return the children of our discovered entry + return entry.children +end + +--[=[ + @within ZipReader + @method walk + + Recursively walks through the ZIP file, calling the provided callback for each entry + with the current entry and its depth. + + @param callback (entry: ZipEntry, depth: number) -> () -- The function to call for each entry +]=] +function ZipReader.walk(self: ZipReader, callback: (entry: ZipEntry, depth: number) -> ()): () + -- Wrapper function which recursively calls callback for every child + -- in an entry + local function walkEntry(entry: ZipEntry, depth: number) + callback(entry, depth) + + for _, child in entry.children do + -- ooo spooky recursion... blame this if shit go wrong + walkEntry(child, depth + 1) + end + end + + walkEntry(self.root, 0) +end + +--[=[ + @interface ZipStatistics + @within ZipReader + + @field fileCount number -- The number of files in the ZIP + @field dirCount number -- The number of directories in the ZIP + @field totalSize number -- The total size of all files in the ZIP +]=] +export type ZipStatistics = { fileCount: number, dirCount: number, totalSize: number } + +--[=[ + @within ZipReader + @method getStats + + Retrieves statistics about the ZIP file. + + @return ZipStatistics -- The statistics about the ZIP file +]=] +function ZipReader.getStats(self: ZipReader): ZipStatistics + local stats: ZipStatistics = { + fileCount = 0, + dirCount = 0, + totalSize = 0, + } + + -- Iterate through the entries, updating stats + for _, entry in self.entries do + if entry.isDirectory then + stats.dirCount += 1 + continue + end + + stats.fileCount += 1 + stats.totalSize += entry.size + end + + return stats +end + +return { + ZipReader = ZipReader, + + -- Creates a `ZipReader` from a `buffer` of ZIP data. + load = function(data: buffer) + return ZipReader.new(data) + end, + + -- Optimized Cyclic Redundancy Check (CRC32) computation implementation + crc32 = crc32, +} diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau new file mode 100644 index 000000000..552fb1f50 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau @@ -0,0 +1,124 @@ +--- Canonicalize a path by removing redundant components +local function canonicalize(path: string): string + -- We convert any `\\` separators to `/` separators to ensure consistency + local components = string.split(path:gsub("\\", "/"), "/") + local result = {} + for _, component in components do + if component == "." then + -- Skip current directory + continue + end + + if component == ".." then + -- Traverse one upwards + table.remove(result, #result) + continue + end + + -- Otherwise, add the component to the result + table.insert(result, component) + end + + return table.concat(result, "/") +end + +--- Check if a path is absolute +local function isAbsolute(path: string): boolean + return ( + string.match(path, "^/") + or string.match(path, "^[a-zA-Z]:[\\/]") + or string.match(path, "^//") + or string.match(path, "^\\\\") + ) ~= nil +end + +--- Check if a path is relative +local function isRelative(path: string): boolean + return not isAbsolute(path) +end + +local function replaceBackslashes(input: string, replacement: "/"): string + -- Check if the string starts with double backslashes + if input:sub(1, 2) == "\\\\" then + -- Replace all single backslashes except the first two + return "\\\\" .. input:sub(3):gsub("\\", replacement) + else + -- Replace all single backslashes + return (input:gsub("\\", replacement)) + end +end + +--- Check if a path is safe to use, i.e., it does not: +--- - Contain null bytes +--- - Resolve to a directory outside of the current directory +--- - Have absolute components +local function isSafe(path: string): boolean + if string.find(path, "\0") or isAbsolute(path) then + -- Null bytes or absolute path, path is unsafe + return false + end + + local components = string.split(replaceBackslashes(path, "/"), "/") + local depth = 0 + for _, component in components do + if string.match(component, "^[a-zA-Z]:$") or string.find(component, "^\\\\") or component == "" then + -- Was a prefix component, or the root directory, path is unsafe + return false + end + + if component == ".." then + -- Traverse one upwards + depth -= 1 + if depth < 0 then + -- Traversed too far, path is unsafe + return false + end + + continue + end + + if component == "." then + -- Skip current directory + continue + end + + -- Otherwise, increment depth + depth += 1 + end + + return depth >= 0 +end + +--- Sanitize a path by ignoring special components +--- - Absolute paths become relative +--- - Special components (like upwards traversing) are removed +--- - Truncates path to the first null byte +local function sanitize(path: string): string + local truncatedPath = if string.find(path, "\0") then string.split(path, "\0")[1] else path + local components = string.split(replaceBackslashes(truncatedPath, "/"), "/") + local result = {} + for _, component in components do + if + not ( + component == ".." + or component == "." + or component == string.match(component, "^[a-zA-Z]:$") + or string.find(component, "^\\\\") + or component == "" + ) + then + -- If the path is not a special component, add it to the result + table.insert(result, component) + end + end + + return table.concat(result, "/") +end + +return { + canonicalize = canonicalize, + isAbsolute = isAbsolute, + isRelative = isRelative, + isSafe = isSafe, + sanitize = sanitize, +} diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/utils/validate_crc.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/validate_crc.luau new file mode 100644 index 000000000..c3dfbbb57 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/validate_crc.luau @@ -0,0 +1,22 @@ +local crc32 = require("../crc") + +export type CrcValidationOptions = { + skip: boolean, + expected: number, +} + +local function validateCrc(decompressed: buffer, validation: CrcValidationOptions) + -- Unless skipping validation is requested, we verify the checksum + if not validation.skip then + local computed = crc32(decompressed) + assert( + validation.expected == computed, + `Validation failed; CRC checksum does not match: {string.format("%x", computed)} ~= {string.format( + "%x", + validation.expected + )} (expected ~= got)` + ) + end +end + +return validateCrc diff --git a/lute/cli/commands/pkg/loom-core/src/authentication.luau b/lute/cli/commands/pkg/loom-core/src/authentication.luau new file mode 100644 index 000000000..9480b0db2 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/authentication.luau @@ -0,0 +1,46 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local process = require("@std/process") +local pp = require("@batteries/pp") + +export type AuthenticationEntry = { + read token: string, +} + +export type AuthenticationStore = { + [string]: AuthenticationEntry, +} + +local function getAuthenticationFilePath(): string + local homedir = path.join(process.homedir(), ".loom", "auth.luau") + return path.format(homedir) +end + +local function loadAuthenticationStore(): AuthenticationStore + local authPath = getAuthenticationFilePath() + local ok, result = pcall(luau.loadModule, authPath, nil) + if not ok or typeof(result) ~= "table" then + return {} + end + return result +end + +local function saveAuthenticationStore(store: AuthenticationStore) + local authPath = getAuthenticationFilePath() + local authDir = path.format(path.parse(path.dirname(authPath))) + fs.createDirectory(authDir, { makeParents = true }) + fs.writeStringToFile(authPath, `return {pp(store)}\n`) +end + +local function saveAuthenticationToken(domain: string, token: string) + local store = loadAuthenticationStore() + store[domain] = table.freeze({ token = token }) + saveAuthenticationStore(store) + print(`Credentials saved for {domain}`) +end + +return { + loadAuthenticationStore = loadAuthenticationStore, + saveAuthenticationToken = saveAuthenticationToken, +} diff --git a/lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau b/lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau new file mode 100644 index 000000000..e7b18bd2b --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau @@ -0,0 +1,19 @@ +local cli = require("@batteries/cli") +local authStore = require("../authentication") + +local function authenticate(...: string) + local args = cli.parser() + args:add("token", "option", { help = "Authentication token", required = true }) + args:add("domain", "option", { help = "Domain to authenticate with", default = "github.com" }) + args:parse({ ... }) + + local token = args:get("token") + if not token then + error("Usage: `lute pkg auth --token [--domain ]`") + end + + local domain = args:get("domain") or "github.com" + authStore.saveAuthenticationToken(domain, token) +end + +return authenticate diff --git a/lute/cli/commands/pkg/loom-core/src/commands/install.luau b/lute/cli/commands/pkg/loom-core/src/commands/install.luau new file mode 100644 index 000000000..b41a45f00 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/commands/install.luau @@ -0,0 +1,73 @@ +local cli = require("@batteries/cli") +local fs = require("@std/fs") +local lockfile = require("../lockfile") +local manifest = require("../manifest") +local resolution = require("../resolution") +local path = require("@std/path") +local process = require("@std/process") + +local function printDiagnostics(lock: lockfile.Lockfile, devAliases: { [string]: boolean }) + local depCount = 0 + local devCount = 0 + for alias in lock.dependencies do + if devAliases[alias] then + devCount += 1 + else + depCount += 1 + end + end + + if devCount > 0 then + print(`Resolved {depCount + devCount} package(s) ({devCount} dev).`) + else + print(`Resolved {depCount} package(s).`) + end + + for alias, key in lock.dependencies do + local transitive = 0 + local pkg = lock.packages[key] + if pkg then + for _ in pkg.dependencies do + transitive += 1 + end + end + local suffix = if devAliases[alias] then " [dev]" else "" + print(` {key} ({pkg.sourceKind}, {transitive} dep(s)){suffix}`) + end +end + +local function install(...: string) + local args = cli.parser() + args:add("exclude-dev", "flag", { help = "Skip dev dependencies" }) + args:add("locked", "flag", { help = "Error if lockfile doesn't match manifest (CI mode)" }) + args:parse({ ... }) + + local excludeDev = args:get("exclude-dev") ~= nil + local locked = args:get("locked") ~= nil + local rootDirectory = path.format(process.cwd()) + local manifestPath = `{rootDirectory}/loom.config.luau` + + local devAliases: { [string]: boolean } = {} + + if fs.exists(manifestPath) then + local rootManifest = manifest.parseFile(manifestPath) + for _, depSpec in rootManifest.package.dependencies do + if depSpec.sourceKind == "registry" then + error("Registry dependencies are not yet supported by the install command") + end + end + if not excludeDev then + for alias, depSpec in rootManifest.package.devDependencies do + if depSpec.sourceKind == "registry" then + error("Registry dependencies are not yet supported by the install command") + end + devAliases[alias] = true + end + end + end + + local lock = resolution.resolve(rootDirectory, nil, { excludeDev = excludeDev, locked = locked }) + printDiagnostics(lock, devAliases) +end + +return install diff --git a/lute/cli/commands/pkg/loom-core/src/github.luau b/lute/cli/commands/pkg/loom-core/src/github.luau new file mode 100644 index 000000000..3d14f2a3a --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/github.luau @@ -0,0 +1,123 @@ +local authenticationStore = require("./authentication") +local fs = require("@std/fs") +local net = require("@std/net") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local unzip = require("../extern/unzip") + +local function sanitizePath(path: string): string + -- Input validation + if not path or typeof(path) ~= "string" then + error("Path must be a string") + end + + if #path == 0 then + error("Path cannot be empty") + end + + local maxPathLength = 255 + if #path > maxPathLength then + error("Path too long (max " .. maxPathLength .. " characters)") + end + + -- Remove/reject dangerous characters + if path:match("[\0\r\n]") then + error("Path contains invalid control characters") + end + + -- Normalize path separators and remove redundant slashes + local normalized = path:gsub("\\", "/"):gsub("//+", "/") + + -- Remove leading slashes but preserve trailing slash for directories + normalized = normalized:gsub("^/+", "") + + -- Security check - prevent directory traversal via ".." path components + if normalized == ".." or normalized:match("^%.%./") or normalized:match("/%.%./") or normalized:match("/%.%.$") then + error("Path contains '..' (directory traversal attempt)") + end + + return normalized +end + +local function getGithubAuthenticationToken(): string? + local githubToken = process.env.GITHUB_TOKEN + if githubToken ~= nil then + return githubToken + end + + local store = authenticationStore.loadAuthenticationStore() + local entry = store["github.com"] + if entry ~= nil then + return entry.token + end + + return nil +end + +local function installZipArchive(content: string, installPath: string) + local reader = unzip.load(buffer.fromstring(content)) + + reader:walk(function(entry, _depth) + if entry.isDirectory then + return + end + + local content = reader:extract(entry, { type = "text" }) :: string + + local sanitized = sanitizePath(entry.name) + local slash = string.find(sanitized, "/") + assert(slash, `Expected entry name to contain a slash: {sanitized}`) + + local outputPath = `{installPath}/{string.sub(sanitized, slash)}` + fs.createDirectory(path.dirname(outputPath), { makeParents = true }) + fs.writeStringToFile(outputPath, content) + end) +end + +local function downloadFromGitHub(url: string): string + local headers = { + ["User-Agent"] = "lute-pkg", + } + + local token = getGithubAuthenticationToken() + if token ~= nil and token ~= "" then + headers["Authorization"] = `Bearer {token}` + end + + local response = net.request(url, { method = "GET", headers = headers }) + if not response.ok then + error(`Failed to download {url}: HTTP {response.status}\n{string.sub(response.body, 1, 200)}`) + end + return response.body +end + +local function installPackage(source: string, rev: string, installPath: string): () + local owner, repo = string.match(source, "^https?://github%.com/([^/]+)/([^/]+)") + if not owner or not repo then + error(`Unsupported GitHub source URL: {source}`) + end + local url = `https://api.github.com/repos/{owner}/{repo}/zipball/{rev}` + local zipArchive = downloadFromGitHub(url) + + local safeName = rev:gsub("[^%w%-_.]", "-") + local tempDir = path.format(path.join(system.tmpdir(), `lute-pkg-install-{safeName}`)) + + if fs.exists(tempDir) then + fs.removeDirectory(tempDir, { recursive = true }) + end + + xpcall(installZipArchive, function(err) + if fs.exists(tempDir) then + fs.removeDirectory(tempDir, { recursive = true }) + end + + error(err, 0) + end, zipArchive, tempDir) + + fs.move(tempDir, installPath) +end + +return { + installPackage = installPackage, +} diff --git a/lute/cli/commands/pkg/loom-core/src/lockfile.luau b/lute/cli/commands/pkg/loom-core/src/lockfile.luau new file mode 100644 index 000000000..ff0740ce7 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/lockfile.luau @@ -0,0 +1,200 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local manifest = require("./manifest") +local semver = require("./semver") + +export type LockfileDependency = { + read name: string, + read rev: string?, + read source: string, + read sourceKind: manifest.SourceKind, + read installPath: string, + read dependencies: { [string]: string }, +} + +export type Lockfile = { + read version: number, + read packages: { [string]: LockfileDependency }, + read dependencies: { [string]: string }, + read overrides: { [string]: string }?, +} + +local function validatePackageEntry(key: string, entry: any) + assert(typeof(entry) == "table", `Expected table for dependency '{key}'`) + assert(typeof(entry.name) == "string", `Expected 'name' field in dependency '{key}'`) + assert( + entry.rev == nil or typeof(entry.rev) == "string", + `Expected 'rev' field to be a string or nil for dependency '{key}'` + ) + assert(typeof(entry.source) == "string", `Expected 'source' field in dependency '{key}'`) + assert( + manifest.isSourceKind(entry.sourceKind), + `Expected 'sourceKind' to be 'path', 'github', or 'registry' for dependency '{key}', got '{entry.sourceKind}'` + ) + assert(typeof(entry.installPath) == "string", `Expected 'installPath' field in dependency '{key}'`) + assert(typeof(entry.dependencies) == "table", `Expected 'dependencies' field in dependency '{key}'`) + for alias, depKey in entry.dependencies do + assert(typeof(alias) == "string", `Expected alias to be a string for dependency '{key}'`) + assert(typeof(depKey) == "string", `Expected dependency key to be a string for dependency '{key}'`) + end + table.freeze(entry.dependencies) +end + +local function parseLockfile(t: any): Lockfile + assert(t ~= nil and typeof(t) == "table", "Expected lockfile to be a table") + assert(t.version == 3, `Unsupported lockfile version: {t.version}`) + assert(typeof(t.packages) == "table", "Expected 'packages' field in lockfile") + assert(typeof(t.dependencies) == "table", "Expected 'dependencies' field in lockfile") + + for key, entry in t.packages do + assert(typeof(key) == "string", "Expected package key to be a string") + validatePackageEntry(key, entry) + end + table.freeze(t.packages) + + for alias, key in t.dependencies do + assert(typeof(alias) == "string", "Expected dependency alias to be a string") + assert(typeof(key) == "string", "Expected dependency key to be a string") + assert(t.packages[key] ~= nil, `Dependency '{alias}' references package '{key}' which is not in packages`) + end + table.freeze(t.dependencies) + + if t.overrides ~= nil then + assert(typeof(t.overrides) == "table", "Expected 'overrides' field to be a table") + for name, value in t.overrides do + assert(typeof(name) == "string", "Expected override key to be a string") + assert(typeof(value) == "string", "Expected override value to be a string") + end + table.freeze(t.overrides) + end + + return table.freeze(t) +end + +local function parseFile(lockfilePath: string): Lockfile + local t = luau.loadModule(lockfilePath) + return parseLockfile(t) +end + +local function tryParseFile(lockfilePath: string): Lockfile? + if not fs.exists(lockfilePath) then + return nil + end + local ok, result = pcall(parseFile, lockfilePath) + if not ok then + return nil + end + return result +end + +local function serializeOverrides(overrides: { [string]: manifest.DependencySpec }): { [string]: string } + local result: { [string]: string } = {} + for name, spec in overrides do + if spec.sourceKind == "registry" then + result[name] = spec.version + elseif spec.sourceKind == "path" then + result[name] = `path:{spec.source}` + elseif spec.sourceKind == "github" then + result[name] = `github:{spec.source}@{spec.rev}` + end + end + return result +end + +local function overridesMatch(locked: { [string]: string }?, current: { [string]: string }): (boolean, string?) + for name, value in current do + if locked == nil or locked[name] ~= value then + return false, `override changed: {name}` + end + end + + if locked then + for name in locked do + if current[name] == nil then + return false, `override removed: {name}` + end + end + end + + return true, nil +end + +local function getEffectiveRootSpec( + spec: manifest.DependencySpec, + overrides: { [string]: manifest.DependencySpec } +): manifest.DependencySpec + return overrides[spec.name] or spec +end + +local function satisfiesManifest( + lock: Lockfile, + rootManifest: manifest.ProjectManifest, + excludeDev: boolean +): (boolean, string?) + local expectedAliases: { [string]: manifest.DependencySpec } = {} + + for alias, spec in rootManifest.package.dependencies do + expectedAliases[alias] = spec + end + + if not excludeDev then + for alias, spec in rootManifest.package.devDependencies do + expectedAliases[alias] = spec + end + end + + for alias, spec in expectedAliases do + spec = getEffectiveRootSpec(spec, rootManifest.overrides) + + local key = lock.dependencies[alias] + if key == nil then + return false, `new dependency: {alias}` + end + + local pkg = lock.packages[key] + if pkg == nil then + return false, `broken lockfile entry for: {alias}` + end + + if pkg.sourceKind ~= spec.sourceKind then + return false, `source kind changed for: {alias}` + end + + if spec.sourceKind == "github" then + if pkg.source ~= spec.source or pkg.rev ~= spec.rev then + return false, `github source changed for: {alias}` + end + elseif spec.sourceKind == "path" then + if pkg.source ~= spec.source then + return false, `path changed for: {alias}` + end + elseif spec.sourceKind == "registry" then + local constraint = semver.parseConstraint(spec.version) + local lockedVersion = semver.tryParse(pkg.rev :: string) + if lockedVersion == nil or not semver.satisfies(lockedVersion, constraint) then + return false, `registry constraint no longer satisfied for: {alias}` + end + end + end + + for alias in lock.dependencies do + if expectedAliases[alias] == nil then + return false, `removed dependency: {alias}` + end + end + + local currentOverrides = serializeOverrides(rootManifest.overrides) + local overridesOk, overridesReason = overridesMatch(lock.overrides, currentOverrides) + if not overridesOk then + return false, overridesReason + end + + return true, nil +end + +return { + parseFile = parseFile, + tryParseFile = tryParseFile, + satisfiesManifest = satisfiesManifest, + serializeOverrides = serializeOverrides, +} diff --git a/lute/cli/commands/pkg/loom-core/src/manifest.luau b/lute/cli/commands/pkg/loom-core/src/manifest.luau new file mode 100644 index 000000000..24a8e6215 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/manifest.luau @@ -0,0 +1,146 @@ +local luau = require("@std/luau") + +export type SourceKind = "path" | "github" | "registry" + +export type PathDependencySpec = { + read sourceKind: "path", + read name: string, + read source: string, +} + +export type GitHubDependencySpec = { + read sourceKind: "github", + read name: string, + read source: string, + read rev: string, +} + +export type RegistryDependencySpec = { + read sourceKind: "registry", + read name: string, + read source: string, + read version: string, +} + +export type DependencySpec = PathDependencySpec | GitHubDependencySpec | RegistryDependencySpec + +export type PackageManifest = { + read name: string, + read version: string, + read dependencies: { [string]: DependencySpec }, + read devDependencies: { [string]: DependencySpec }, +} + +export type ProjectManifest = { + read package: PackageManifest, + read overrides: { [string]: DependencySpec }, +} + +local function isSourceKind(s: unknown): boolean + return s == "path" or s == "github" or s == "registry" +end + +local function parseSourceKind(s: unknown): SourceKind? + if not isSourceKind(s) then + return nil + end + + return s :: SourceKind +end + +local function parseDependencyMap(raw: any, sectionName: string): { [string]: DependencySpec } + local result: { [string]: DependencySpec } = {} + if raw == nil then + return table.freeze(result) + end + + for name, spec in raw do + assert( + spec.name == nil or typeof(spec.name) == "string", + `Optional 'name' field in dependency spec for '{name}' in {sectionName} expected to be a string` + ) + assert( + isSourceKind(spec.sourceKind), + `Expected 'sourceKind' to be 'path', 'github', or 'registry' in dependency spec for '{name}' in {sectionName}, got '{spec.sourceKind}'` + ) + assert( + spec.source ~= nil and typeof(spec.source) == "string", + `Expected 'source' field in dependency spec for '{name}' in {sectionName}` + ) + + if spec.sourceKind == "path" then + result[name] = table.freeze({ + name = spec.name or name, + sourceKind = "path" :: "path", + source = spec.source, + }) + elseif spec.sourceKind == "github" then + assert(typeof(spec.rev) == "string", `Expected 'rev' field for github dependency '{name}' in {sectionName}`) + result[name] = table.freeze({ + name = spec.name or name, + sourceKind = "github" :: "github", + source = spec.source, + rev = spec.rev, + }) + elseif spec.sourceKind == "registry" then + assert( + typeof(spec.version) == "string", + `Expected 'version' field for registry dependency '{name}' in {sectionName}` + ) + result[name] = table.freeze({ + name = spec.name or name, + sourceKind = "registry" :: "registry", + source = spec.source, + version = spec.version, + }) + end + end + + return table.freeze(result) +end + +-- TODO: We should do proper error handling because this is user input. +local function parseManifest(t: unknown): ProjectManifest + assert(t ~= nil and typeof(t) == "table", "Expected a table") + assert(t.package ~= nil and typeof(t.package) == "table", "Expected 'package' field in manifest") + + local pkg = t.package + + assert(pkg.name ~= nil and typeof(pkg.name) == "string", "Expected 'name' field in package manifest") + -- FIXME(Luau): Luau currently refines `pkg` to have `version` based on this check, but + -- also produces a type error for the access _in_ the condition without the cast here. + assert( + (pkg :: any).version ~= nil and typeof(pkg.version) == "string", + "Expected 'version' field in package manifest" + ) + + local dependencies = parseDependencyMap((pkg :: any).dependencies, "dependencies") + local devDependencies = parseDependencyMap((pkg :: any).dev_dependencies, "dev_dependencies") + + for alias in devDependencies do + assert(dependencies[alias] == nil, `Alias '{alias}' appears in both dependencies and dev_dependencies`) + end + + local overrides = parseDependencyMap((t :: any).overrides, "overrides") + + return table.freeze({ + package = table.freeze({ + name = pkg.name, + version = pkg.version, + dependencies = dependencies, + devDependencies = devDependencies, + }), + overrides = overrides, + }) +end + +local function parseFile(manifestPath: string): ProjectManifest + local manifestTable = luau.loadModule(manifestPath) + return parseManifest(manifestTable) +end + +return { + isSourceKind = isSourceKind, + parseSourceKind = parseSourceKind, + parseFile = parseFile, +} diff --git a/lute/cli/commands/pkg/loom-core/src/resolution.luau b/lute/cli/commands/pkg/loom-core/src/resolution.luau new file mode 100644 index 000000000..748e105d4 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/resolution.luau @@ -0,0 +1,613 @@ +local fs = require("@std/fs") +local github = require("./github") +local lockfile = require("./lockfile") +local path = require("@std/path") +local pp = require("@batteries/pp") +local manifest = require("./manifest") +local semver = require("./semver") +local store = require("./store") + +local function getManifestPath(directory: string): string + return `{directory}/loom.config.luau` +end + +local function getLockfilePath(directory: string): string + return `{directory}/loom.lock.luau` +end + +local function getKey(spec: manifest.DependencySpec): string + if spec.sourceKind == "github" then + return `{spec.name}@{spec.rev}` + end + + return spec.name +end + +local function hasConflict(resolved: lockfile.LockfileDependency, incoming: manifest.DependencySpec): boolean + if resolved.sourceKind ~= incoming.sourceKind then + return true + end + + if resolved.source ~= incoming.source then + return true + end + + if incoming.sourceKind == "github" then + return resolved.rev ~= incoming.rev + end + + return false +end + +local function normalizePathDependency( + spec: manifest.DependencySpec, + installPath: string, + rootDirectory: string +): manifest.DependencySpec + -- normalization is a no-op if it's not a path dependency + if spec.sourceKind ~= "path" then + return spec + end + + local sourcePath = path.format(path.resolve(installPath, spec.source)) + if not fs.exists(sourcePath) then + error(`Path dependency directory not found: {sourcePath} (for package '{spec.name}')`) + end + + return table.freeze({ + name = spec.name, + sourceKind = "path", + source = path.format(path.relative(rootDirectory, sourcePath)), + }) :: manifest.PathDependencySpec +end + +local function fetchSource(spec: manifest.DependencySpec, rootDirectory: string): string + if spec.sourceKind == "path" then + local sourcePath = path.format(path.resolve(rootDirectory, spec.source)) + if not fs.exists(sourcePath) then + error(`Path dependency directory not found: {sourcePath} (for package '{spec.name}')`) + end + return sourcePath + elseif spec.sourceKind == "github" then + store.ensureStoreDirectory() + + local key = getKey(spec) + local sourcePath = store.getPackagePath(key) + if not store.hasPackage(key) then + github.installPackage(spec.source, spec.rev, sourcePath) + end + + return sourcePath + else + error(`Unsupported source kind: {spec.sourceKind} for package {spec.name}`) + end +end + +export type ResolveOptions = { + read excludeDev: boolean?, + read locked: boolean?, +} + +type RootRegistryAlias = { + read alias: string, + read name: string, +} + +local function removeRequirementsForPackage(registryRequirements: { [string]: semver.Requirement }, packageName: string) + for reqKey, req in registryRequirements do + if req.name == packageName then + registryRequirements[reqKey] = nil + end + end +end + +local function collectSourcePackageDependencies( + key: string, + installPath: string, + rootDirectory: string, + transitiveDependencies: { [string]: string }, + queue: { manifest.DependencySpec }, + registryRequirements: { [string]: semver.Requirement }, + registrySources: { [string]: string }, + unresolvedRegistryDependencies: { [string]: { [string]: string } } +) + local nestedManifestPath = getManifestPath(installPath) + if not fs.exists(nestedManifestPath) then + return + end + + local dependencyManifest = manifest.parseFile(nestedManifestPath) + for dependencyAlias, dependencySpec in dependencyManifest.package.dependencies do + local normalizedSpec = normalizePathDependency(dependencySpec, installPath, rootDirectory) + + if normalizedSpec.sourceKind == "registry" then + registryRequirements[`{key}:{dependencyAlias}`] = { + name = normalizedSpec.name, + constraint = semver.parseConstraint(normalizedSpec.version), + requiredBy = key, + aliasedAs = if dependencyAlias ~= normalizedSpec.name then dependencyAlias else nil, + } + + registrySources[normalizedSpec.name] = normalizedSpec.source + + if not unresolvedRegistryDependencies[key] then + unresolvedRegistryDependencies[key] = {} + end + + unresolvedRegistryDependencies[key][dependencyAlias] = normalizedSpec.name + continue + end + + transitiveDependencies[dependencyAlias] = getKey(normalizedSpec) + table.insert(queue, normalizedSpec) + end +end + +local function buildEffectiveUniverse( + universe: semver.Universe, + overrides: { [string]: manifest.DependencySpec } +): (semver.Universe, { [string]: { [string]: string } }) + local registryOverrides: { [string]: semver.Constraint } = {} + local sourceOverrideNames: { [string]: boolean } = {} + for name, spec in overrides do + if spec.sourceKind == "registry" then + registryOverrides[name] = semver.parseConstraint(spec.version) + else + sourceOverrideNames[name] = true + end + end + + local effective: semver.Universe = {} + local sourceOverridedDeps: { [string]: { [string]: string } } = {} + + for pkgName, versions in universe do + if registryOverrides[pkgName] then + local filtered: { semver.PackageVersion } = {} + for _, v in versions do + if semver.satisfies(v.version, registryOverrides[pkgName]) then + table.insert(filtered, v) + end + end + assert( + #filtered > 0, + `Override constrains '{pkgName}' to {semver.formatConstraint(registryOverrides[pkgName])}, but no matching version exists in the registry` + ) + effective[pkgName] = filtered + elseif sourceOverrideNames[pkgName] then + continue + else + local rewritten: { semver.PackageVersion } = {} + for _, v in versions do + local newDeps: { [string]: semver.Requirement } = {} + for alias, dep in v.dependencies do + if registryOverrides[dep.name] then + newDeps[alias] = table.freeze({ + name = dep.name, + constraint = registryOverrides[dep.name], + requiredBy = dep.requiredBy, + aliasedAs = dep.aliasedAs, + }) + elseif sourceOverrideNames[dep.name] then + local vKey = `{v.name}@{v.versionString}` + if not sourceOverridedDeps[vKey] then + sourceOverridedDeps[vKey] = {} + end + sourceOverridedDeps[vKey][alias] = dep.name + else + newDeps[alias] = dep + end + end + table.insert( + rewritten, + table.freeze({ + name = v.name, + version = v.version, + versionString = v.versionString, + dependencies = table.freeze(newDeps), + }) + ) + end + effective[pkgName] = rewritten + end + end + + return effective, sourceOverridedDeps +end + +local function pathDepsAreStale( + lock: lockfile.Lockfile, + rootDirectory: string, + overrides: { [string]: manifest.DependencySpec } +): boolean + for _, pkg in lock.packages do + if pkg.sourceKind ~= "path" then + continue + end + + local packageInstallPath = path.format(path.join(rootDirectory, pkg.installPath)) + local depManifestPath = path.format(path.join(packageInstallPath, "loom.config.luau")) + if not fs.exists(depManifestPath) then + return true + end + + local ok, depManifest = pcall(manifest.parseFile, depManifestPath) + if not ok then + return true + end + + for alias in depManifest.package.dependencies do + if pkg.dependencies[alias] == nil then + return true + end + end + + for alias in pkg.dependencies do + if depManifest.package.dependencies[alias] == nil then + return true + end + end + + for alias, depSpec in depManifest.package.dependencies do + local ok, normalizedDepSpec = pcall(normalizePathDependency, depSpec, packageInstallPath, rootDirectory) + if not ok then + return true + end + depSpec = overrides[(normalizedDepSpec :: manifest.DependencySpec).name] or normalizedDepSpec + + local recordedKey = pkg.dependencies[alias] + if recordedKey == nil then + continue + end + local recordedPkg = lock.packages[recordedKey] + if recordedPkg == nil then + return true + end + if recordedPkg.sourceKind ~= depSpec.sourceKind then + return true + end + if depSpec.sourceKind == "github" then + if recordedPkg.source ~= depSpec.source or recordedPkg.rev ~= depSpec.rev then + return true + end + elseif depSpec.sourceKind == "path" then + if recordedPkg.source ~= depSpec.source then + return true + end + end + end + end + return false +end + +local function resolve(rootDirectory: string, universe: semver.Universe?, options: ResolveOptions?): lockfile.Lockfile + local excludeDev = if options then options.excludeDev == true else false + + local rootManifestPath = getManifestPath(rootDirectory) + if not fs.exists(rootManifestPath) then + error(`Manifest file not found: {rootManifestPath}`) + end + + local lockfilePath = getLockfilePath(rootDirectory) + local rootManifest = manifest.parseFile(rootManifestPath) + + -- Attempt lockfile reuse + local existingLock = lockfile.tryParseFile(lockfilePath) + + if options and options.locked and existingLock == nil then + error("--locked: no lockfile found. Run `lute pkg install` first.") + end + + if existingLock then + local satisfied, reason = lockfile.satisfiesManifest(existingLock, rootManifest, excludeDev) + + if options and options.locked then + if not satisfied then + error(`--locked: lockfile does not satisfy manifest ({reason}). Run \`lute pkg install\` to update.`) + end + print("Lockfile up to date.") + return existingLock + end + + if satisfied and not pathDepsAreStale(existingLock, rootDirectory, rootManifest.overrides) then + print("Lockfile up to date.") + return existingLock + end + end + + -- Extract locked registry versions for soft preference during re-resolution. + -- Skip packages that were previously overridden — removing an override should unlock the version. + local lockedRegistryVersions: { [string]: string }? = nil + if existingLock then + lockedRegistryVersions = {} + local previousOverrides = existingLock.overrides + for _, pkg in existingLock.packages do + if pkg.sourceKind == "registry" and pkg.rev then + if previousOverrides and previousOverrides[pkg.name] then + continue + end + (lockedRegistryVersions :: { [string]: string })[pkg.name] = pkg.rev + end + end + end + + local queue: { manifest.DependencySpec } = {} + local dependencies: { [string]: string } = {} + local packages: { [string]: lockfile.LockfileDependency } = {} + + -- All registry solver inputs: root-level dependencies keyed by alias, transitive by "{pkgKey}:{dependencyAlias}". + local registryRequirements: { [string]: semver.Requirement } = {} + local registrySources: { [string]: string } = {} + + -- Root aliases that need a resolved key written back into `dependencies` after solving. + local rootRegistryAliases: { RootRegistryAlias } = {} + -- Per-package registry aliases that need keys written into their dependency table after solving. + local unresolvedRegistryDependencies: { [string]: { [string]: string } } = {} + -- Open (not yet finalized) dependency maps for unversioned packages; registry entries are filled in during versioned resolution. + local openDependencies: { [string]: { [string]: string } } = {} + local queueIndex = 1 + + local function processSourceQueue() + while queueIndex <= #queue do + local spec = queue[queueIndex] + queueIndex += 1 + + local key = getKey(spec) + + if packages[key] then + if hasConflict(packages[key], spec) then + error( + `Source mismatch for package '{spec.name}':\n` + .. ` resolved: {packages[key].source} ({packages[key].sourceKind})\n` + .. ` incoming: {spec.source} ({spec.sourceKind})\n` + .. `Both resolve to '{key}' but from different sources.` + ) + else + -- Already resolved at this exact version — deduplicate + continue + end + end + + local transitiveDependencies: { [string]: string } = {} + openDependencies[key] = transitiveDependencies + + local installPath = fetchSource(spec, rootDirectory) + + collectSourcePackageDependencies( + key, + installPath, + rootDirectory, + transitiveDependencies, + queue, + registryRequirements, + registrySources, + unresolvedRegistryDependencies + ) + + packages[key] = table.freeze({ + name = spec.name, + rev = if spec.sourceKind == "github" then spec.rev else nil, + source = spec.source, + sourceKind = spec.sourceKind, + installPath = if spec.sourceKind == "path" + then path.format(path.relative(rootDirectory, installPath)) + else store.toStorePath(key), + dependencies = transitiveDependencies, + }) + end + end + + for dependencyAlias, dependencySpec in rootManifest.package.dependencies do + if dependencySpec.sourceKind == "registry" then + registryRequirements[dependencyAlias] = { + name = dependencySpec.name, + constraint = semver.parseConstraint(dependencySpec.version), + requiredBy = rootManifest.package.name, + aliasedAs = if dependencyAlias ~= dependencySpec.name then dependencyAlias else nil, + } + + registrySources[dependencySpec.name] = dependencySpec.source + table.insert(rootRegistryAliases, { alias = dependencyAlias, name = dependencySpec.name }) + continue + end + + dependencies[dependencyAlias] = getKey(dependencySpec) + table.insert(queue, dependencySpec) + end + + if not excludeDev then + for dependencyAlias, dependencySpec in rootManifest.package.devDependencies do + if dependencySpec.sourceKind == "registry" then + registryRequirements[dependencyAlias] = { + name = dependencySpec.name, + constraint = semver.parseConstraint(dependencySpec.version), + requiredBy = rootManifest.package.name, + aliasedAs = if dependencyAlias ~= dependencySpec.name then dependencyAlias else nil, + } + + registrySources[dependencySpec.name] = dependencySpec.source + table.insert(rootRegistryAliases, { alias = dependencyAlias, name = dependencySpec.name }) + continue + end + + dependencies[dependencyAlias] = getKey(dependencySpec) + table.insert(queue, dependencySpec) + end + end + + processSourceQueue() + + -- Apply overrides: version pins replace constraints; source overrides redirect to path/github. + local overrides = rootManifest.overrides + local overriddenPackages: { [string]: semver.Requirement } = {} + + for overrideName, overrideSpec in overrides do + removeRequirementsForPackage(registryRequirements, overrideName) + + if overrideSpec.sourceKind == "registry" then + local overrideReq = { + name = overrideName, + constraint = semver.parseConstraint(overrideSpec.version), + requiredBy = "override", + } + registryRequirements[`override:{overrideName}`] = overrideReq + overriddenPackages[overrideName] = overrideReq + + if not registrySources[overrideName] then + registrySources[overrideName] = overrideSpec.source + end + else + local overrideKey = getKey(overrideSpec) + + for pkgKey, unresolvedAliases in unresolvedRegistryDependencies do + for depAlias, registryName in unresolvedAliases do + if registryName == overrideName then + local packageDependencies = openDependencies[pkgKey] + if packageDependencies then + packageDependencies[depAlias] = overrideKey + end + unresolvedAliases[depAlias] = nil + end + end + end + + -- Alias was removed from registryRequirements by the override; write override key directly. + for i = #rootRegistryAliases, 1, -1 do + local rootAlias = rootRegistryAliases[i] + if rootAlias.name == overrideName then + dependencies[rootAlias.alias] = overrideKey + table.remove(rootRegistryAliases, i) + end + end + + if not packages[overrideKey] then + local transitiveDependencies: { [string]: string } = {} + openDependencies[overrideKey] = transitiveDependencies + + local installPath = fetchSource(overrideSpec, rootDirectory) + collectSourcePackageDependencies( + overrideKey, + installPath, + rootDirectory, + transitiveDependencies, + queue, + registryRequirements, + registrySources, + unresolvedRegistryDependencies + ) + + packages[overrideKey] = table.freeze({ + name = overrideSpec.name, + rev = if overrideSpec.sourceKind == "github" then overrideSpec.rev else nil, + source = overrideSpec.source, + sourceKind = overrideSpec.sourceKind, + installPath = if overrideSpec.sourceKind == "path" + then path.format(path.relative(rootDirectory, installPath)) + else store.toStorePath(overrideKey), + dependencies = transitiveDependencies, + }) + end + end + end + + processSourceQueue() + + -- Build a modified universe that respects overrides. + local effectiveUniverse = universe + local sourceOverridedDeps: { [string]: { [string]: string } } = {} + if universe ~= nil and next(overrides) ~= nil then + effectiveUniverse, sourceOverridedDeps = buildEffectiveUniverse(universe, overrides) + end + + -- Versioned dependency resolution: solve registry dependencies and stitch them into the package graph. + if next(registryRequirements) ~= nil then + assert(effectiveUniverse ~= nil, "Registry dependencies require a universe to be provided") + + local resolution = semver.solve(effectiveUniverse, registryRequirements, lockedRegistryVersions) + + -- Build a lookup from package name to resolved version(s) for slot-keyed results. + local versionsByPackageName = {} + for _, pkgVersion in resolution do + versionsByPackageName[pkgVersion.name] = pkgVersion + end + + for _, pkgVersion in resolution do + local name = pkgVersion.name + local key = `{name}@{pkgVersion.versionString}` + + local transitiveDependencies: { [string]: string } = {} + for alias, dependency in pkgVersion.dependencies do + local resolvedVersion = versionsByPackageName[dependency.name] + assert(resolvedVersion ~= nil, `Solver produced an incomplete resolution: missing '{dependency.name}'`) + + transitiveDependencies[alias] = `{dependency.name}@{resolvedVersion.versionString}` + end + + -- Add back dependencies that were stripped for source overrides. + if sourceOverridedDeps[key] then + for alias, overriddenName in sourceOverridedDeps[key] do + local overrideSpec = overrides[overriddenName] + if overrideSpec then + transitiveDependencies[alias] = getKey(overrideSpec) + end + end + end + + packages[key] = table.freeze({ + name = name, + rev = pkgVersion.versionString, + source = registrySources[name] or name, + sourceKind = "registry" :: "registry", + installPath = store.toStorePath(key), + dependencies = table.freeze(transitiveDependencies), + }) + end + + -- Fill in pending registry dependency keys directly into the unversioned packages' dependency tables. + for pkgKey, unresolvedAliases in unresolvedRegistryDependencies do + local packageDependencies = openDependencies[pkgKey] + for dependencyAlias, registryName in unresolvedAliases do + local pkgVersion = versionsByPackageName[registryName] + assert(pkgVersion ~= nil, `Solver did not resolve transitive registry dependency '{registryName}'`) + packageDependencies[dependencyAlias] = `{registryName}@{pkgVersion.versionString}` + end + end + + -- Write root-level registry dependency aliases now that versions are resolved. + for _, rootAlias in rootRegistryAliases do + local req = registryRequirements[rootAlias.alias] or overriddenPackages[rootAlias.name] + if req == nil then + continue + end + local pkgVersion = versionsByPackageName[req.name] + assert(pkgVersion ~= nil, `Solver did not resolve root dependency '{req.name}'`) + dependencies[rootAlias.alias] = `{req.name}@{pkgVersion.versionString}` + end + end + + for _, packageDependencies in openDependencies do + table.freeze(packageDependencies) + end + + table.freeze(packages) + table.freeze(dependencies) + + local serializedOverrides = lockfile.serializeOverrides(overrides) + local frozenOverrides: { [string]: string }? = if next(serializedOverrides) + then table.freeze(serializedOverrides) + else nil + + local lock: lockfile.Lockfile = table.freeze({ + version = 3, + packages = packages, + dependencies = dependencies, + overrides = frozenOverrides, + }) + + fs.writeStringToFile(lockfilePath, `return {pp(lock)}\n`) + + return lock +end + +return { + resolve = resolve, +} diff --git a/lute/cli/commands/pkg/loom-core/src/semver/init.luau b/lute/cli/commands/pkg/loom-core/src/semver/init.luau new file mode 100644 index 000000000..9f93aba9e --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/semver/init.luau @@ -0,0 +1,27 @@ +--!strict + +local parser = require("@self/parser") +local solver = require("@self/solver") + +export type PrereleaseId = parser.PrereleaseId +export type Version = parser.Version +export type Comparator = parser.Comparator +export type Constraint = parser.Constraint + +export type PackageVersion = solver.PackageVersion +export type Requirement = solver.Requirement +export type Universe = solver.Universe +export type Resolution = solver.Resolution + +return { + parse = parser.parse, + tryParse = parser.tryParse, + parseConstraint = parser.parseConstraint, + satisfies = parser.satisfies, + compare = parser.compare, + sort = parser.sort, + format = parser.format, + formatConstraint = parser.formatConstraint, + + solve = solver.solve, +} diff --git a/lute/cli/commands/pkg/loom-core/src/semver/parser.luau b/lute/cli/commands/pkg/loom-core/src/semver/parser.luau new file mode 100644 index 000000000..43e061231 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/semver/parser.luau @@ -0,0 +1,540 @@ +--!strict + +export type PrereleaseId = string | number + +export type Version = { + read major: number, + read minor: number, + read patch: number, + read prerelease: { PrereleaseId }, + read build: { string }, +} + +export type Comparator = { + read op: "=" | ">" | ">=" | "<" | "<=", + read version: Version, +} + +-- A constraint is a list of Comparators joined by AND. +-- An empty list means "any version" (wildcard). +export type Constraint = { Comparator } + +-- Matches only [0-9A-Za-z-] +local function isValidIdentChar(c: string): boolean + return c:match("^[0-9A-Za-z%-]$") ~= nil +end + +local function isAllDigits(s: string): boolean + return s:match("^[0-9]+$") ~= nil +end + +local function hasLeadingZero(s: string): boolean + return #s > 1 and s:sub(1, 1) == "0" +end + +local function parseIdents(raw: string, label: string): { string } + -- Detect empty identifiers from leading/trailing dots or consecutive dots + if raw:sub(1, 1) == "." or raw:sub(-1) == "." or raw:find("%.%.") then + error(`invalid semver {label}: empty identifier in '{raw}'`) + end + + local idents: { string } = {} + for ident in raw:gmatch("[^%.]+") do + for i = 1, #ident do + if not isValidIdentChar(ident:sub(i, i)) then + error(`invalid semver {label}: identifier '{ident}' contains invalid character`) + end + end + table.insert(idents, ident) + end + + if #idents == 0 then + error(`invalid semver {label}: '{raw}' produced no identifiers`) + end + + return idents +end + +local function parsePrereleaseIdents(raw: string): { PrereleaseId } + local rawIdents = parseIdents(raw, "pre-release") + local result: { PrereleaseId } = {} + + for _, ident in rawIdents do + if not isAllDigits(ident) then + table.insert(result, ident) + continue + end + + if hasLeadingZero(ident) then + error(`invalid semver pre-release: numeric identifier '{ident}' has a leading zero`) + end + + table.insert(result, tonumber(ident) :: number) + end + + return result +end + +local function parseVersionString(s: string): Version + -- Split off build metadata first (after +) + local versionPartOptional, buildRaw = s:match("^([^+]+)%+(.+)$") + if versionPartOptional == nil then + buildRaw = nil + end + + local versionPart = versionPartOptional or s + + -- Split off pre-release (after -) + local corePartOptional, prereleaseRaw = versionPart:match("^([^%-]+)%-(.+)$") + if corePartOptional == nil then + prereleaseRaw = nil + end + + local corePart = corePartOptional or versionPart + + -- Parse M.m.p + local majorStr, minorStr, patchStr = corePart:match("^(%d+)%.(%d+)%.(%d+)$") + if majorStr == nil then + error(`invalid semver version: '{s}' (expected X.Y.Z)`) + end + + for _, part in { majorStr, minorStr, patchStr } do + assert(part) -- FIXME(Luau): type inference doesn't understand the loop invariant that these are all non-nil + + if hasLeadingZero(part) then + error(`invalid semver version: '{s}' (leading zero in numeric identifier)`) + end + end + + local prerelease: { PrereleaseId } = {} + if prereleaseRaw ~= nil then + -- Check that pre-release section is not just an empty trailing hyphen + if prereleaseRaw == "" then + error(`invalid semver version: '{s}' (empty pre-release)`) + end + + prerelease = parsePrereleaseIdents(prereleaseRaw) + end + + local build: { string } = {} + if buildRaw ~= nil then + if buildRaw == "" then + error(`invalid semver version: '{s}' (empty build metadata)`) + end + + build = parseIdents(buildRaw, "build metadata") + end + + return table.freeze({ + major = tonumber(majorStr) :: number, + minor = tonumber(minorStr) :: number, + patch = tonumber(patchStr) :: number, + prerelease = table.freeze(prerelease), + build = table.freeze(build), + }) +end + +local function parse(s: string): Version + return parseVersionString(s) +end + +local function tryParse(s: string): Version? + local ok, result = pcall(parseVersionString, s) + + if not ok then + return nil + end + + return result +end + +-- Compare two versions per the semver spec. +-- Returns -1 if a < b, 0 if a == b, 1 if a > b. +-- Build metadata is ignored. +local function compare(a: Version, b: Version): number + if a.major ~= b.major then + return if a.major < b.major then -1 else 1 + end + + if a.minor ~= b.minor then + return if a.minor < b.minor then -1 else 1 + end + + if a.patch ~= b.patch then + return if a.patch < b.patch then -1 else 1 + end + + -- Equal M.m.p — compare pre-release + local aHasPre = #a.prerelease > 0 + local bHasPre = #b.prerelease > 0 + + if not aHasPre and not bHasPre then + return 0 + end + + -- Pre-release < release + if aHasPre and not bHasPre then + return -1 + end + + if not aHasPre and bHasPre then + return 1 + end + + -- Both have pre-release — compare identifier by identifier + local aIds = a.prerelease + local bIds = b.prerelease + local minLen = math.min(#aIds, #bIds) + + for i = 1, minLen do + local ai = aIds[i] + local bi = bIds[i] + + if ai == bi then + continue + end + + if typeof(ai) == "number" and typeof(bi) == "number" then + return if ai < bi then -1 else 1 + end + + -- Numeric < non-numeric + if typeof(ai) == "number" and typeof(bi) ~= "number" then + return -1 + end + + if typeof(ai) ~= "number" and typeof(bi) == "number" then + return 1 + end + + assert(typeof(ai) == "string" and typeof(bi) == "string") + + -- Both strings: ASCII lexicographic + return if ai < bi then -1 else 1 + end + + -- All compared identifiers are equal — larger set has higher precedence + if #aIds ~= #bIds then + return if #aIds < #bIds then -1 else 1 + end + + return 0 +end + +local function sort(versions: { Version }, descending: boolean?): { Version } + local copy = table.clone(versions) + + table.sort(copy, function(a, b) + local c = compare(a, b) + return if descending then c > 0 else c < 0 + end) + + return copy +end + +-- Build metadata is intentionally omitted: it does not affect precedence +-- and carries no meaning for package resolution. +local function format(v: Version): string + local base = `{v.major}.{v.minor}.{v.patch}` + + if #v.prerelease == 0 then + return base + end + + local parts: { string } = {} + for _, id in v.prerelease do + table.insert(parts, tostring(id)) + end + + return `{base}-{table.concat(parts, ".")}` +end + +-- Parse a partial version string "X", "X.Y", or "X.Y.Z" into (major, minor?, patch?) +-- Used for caret/tilde with partial versions. +local function parsePartialVersion(s: string): (number, number?, number?) + local major, minor, patch = s:match("^(%d+)%.(%d+)%.(%d+)$") + if major then + return tonumber(major) :: number, tonumber(minor), tonumber(patch) + end + + local ma, mi = s:match("^(%d+)%.(%d+)$") + if ma then + return tonumber(ma) :: number, tonumber(mi), nil + end + + local ma2 = s:match("^(%d+)$") + if ma2 then + return tonumber(ma2) :: number, nil, nil + end + + error(`invalid partial version: '{s}'`) +end + +local function makeVersion(major: number, minor: number?, patch: number?): Version + return table.freeze({ + major = major, + minor = minor or 0, + patch = patch or 0, + prerelease = table.freeze({} :: { PrereleaseId }), + build = table.freeze({} :: { string }), + }) +end + +-- Build a Comparator table. +local function makeComparator(op: "=" | ">" | ">=" | "<" | "<=", v: Version): Comparator + return table.freeze({ op = op, version = v }) +end + +-- Expand partial-wildcard constraint: +-- X.* / X.x / X.X -> >=X.0.0 <(X+1).0.0 +-- X.Y.* / X.Y.x / X.Y.X -> >=X.Y.0 =", makeVersion(major, 0, 0)), + makeComparator("<", makeVersion(major + 1, 0, 0)), + }) + end + + -- X.Y.* or X.Y.x or X.Y.X + local majorStr2, minorStr = s:match("^(%d+)%.(%d+)%.[*xX]$") + if majorStr2 then + local major = tonumber(majorStr2) :: number + local minor = tonumber(minorStr) :: number + return table.freeze({ + makeComparator(">=", makeVersion(major, minor, 0)), + makeComparator("<", makeVersion(major, minor + 1, 0)), + }) + end + + return nil +end + +-- Expand caret constraint per npm semver semantics. +local function expandCaret(s: string): Constraint + -- Try a full version parse first to support pre-release lower bounds (e.g. ^1.4.2-beta.5). + local loFull = tryParse(s) + local major: number + local minor: number? + local patch: number? + local lo: Version + + if loFull ~= nil then + major = loFull.major + minor = loFull.minor + patch = loFull.patch + lo = loFull + else + major, minor, patch = parsePartialVersion(s) + lo = makeVersion(major, minor or 0, patch or 0) + end + + if major > 0 then + -- ^X.Y.Z[-pre] -> >=lo <(X+1).0.0 + return table.freeze({ makeComparator(">=", lo), makeComparator("<", makeVersion(major + 1, 0, 0)) }) + end + + if minor ~= nil and minor > 0 then + -- ^0.Y.Z[-pre] -> >=lo <0.(Y+1).0 + return table.freeze({ makeComparator(">=", lo), makeComparator("<", makeVersion(0, minor + 1, 0)) }) + end + + if patch ~= nil then + if loFull ~= nil and #loFull.prerelease > 0 then + -- ^0.0.Z-pre -> >=lo <0.0.(Z+1) + return table.freeze({ makeComparator(">=", lo), makeComparator("<", makeVersion(0, 0, patch + 1)) }) + end + -- ^0.0.Z -> =0.0.Z + return table.freeze({ makeComparator("=", lo) }) + end + + if minor ~= nil then + -- ^0.0 -> >=0.0.0 <0.1.0 + return table.freeze({ makeComparator(">=", lo), makeComparator("<", makeVersion(0, 1, 0)) }) + end + + -- ^0 -> >=0.0.0 <1.0.0 + return table.freeze({ makeComparator(">=", lo), makeComparator("<", makeVersion(1, 0, 0)) }) +end + +-- Expand tilde constraint: ~X.Y.Z[-pre] -> >=lo =", lo), makeComparator("<", hi) }) +end + +-- Parse a single comparator token like ">=1.2.3", "1.2.3", or "=1.2.3". +-- Note: Lua patterns do not support alternation (|), so operators are checked manually. +local function parseSingleComparator(token: string): Comparator + if token:sub(1, 2) == ">=" then + return makeComparator(">=", parse(token:sub(3))) + end + + if token:sub(1, 2) == "<=" then + return makeComparator("<=", parse(token:sub(3))) + end + + if token:sub(1, 1) == ">" then + return makeComparator(">", parse(token:sub(2))) + end + + if token:sub(1, 1) == "<" then + return makeComparator("<", parse(token:sub(2))) + end + + if token:sub(1, 1) == "=" then + return makeComparator("=", parse(token:sub(2))) + end + + return makeComparator("=", parse(token)) +end + +local function parseConstraint(s: string): Constraint + local trimmed = s:match("^%s*(.-)%s*$") :: string + + if trimmed == "" then + error(`invalid constraint: empty string`) + end + + if trimmed == "*" then + return table.freeze({} :: Constraint) + end + + if trimmed:sub(1, 1) == "^" then + return expandCaret(trimmed:sub(2)) + end + + if trimmed:sub(1, 1) == "~" then + return expandTilde(trimmed:sub(2)) + end + + local wildcard = expandWildcard(trimmed) + if wildcard ~= nil then + return wildcard + end + + -- Space-separated comparators (AND) + local comparators: Constraint = {} + for token in trimmed:gmatch("%S+") do + table.insert(comparators, parseSingleComparator(token)) + end + + if #comparators == 0 then + error(`invalid constraint: '{s}'`) + end + + return table.freeze(comparators) +end + +-- Check if a version satisfies a single comparator. +local function satisfiesComparator(v: Version, c: Comparator): boolean + local cmp = compare(v, c.version) + + if c.op == "=" then + return cmp == 0 + end + + if c.op == ">" then + return cmp > 0 + end + + if c.op == ">=" then + return cmp >= 0 + end + + if c.op == "<" then + return cmp < 0 + end + + if c.op == "<=" then + return cmp <= 0 + end + + return false +end + +local function prereleaseExcluded(v: Version, comparators: Constraint): boolean + if #v.prerelease == 0 then + return false + end + + -- Allowed only if a comparator explicitly names a pre-release on the same M.m.p tuple. + for _, c in comparators do + if + #c.version.prerelease > 0 + and c.version.major == v.major + and c.version.minor == v.minor + and c.version.patch == v.patch + then + return false + end + end + return true +end + +local function satisfies(v: Version, constraint: Constraint): boolean + -- Wildcard (*): no comparator names a pre-release, so pre-releases are excluded. + if #constraint == 0 then + return #v.prerelease == 0 + end + + -- Exclude pre-release versions unless the constraint explicitly opts in + if prereleaseExcluded(v, constraint) then + return false + end + + for _, c in constraint do + if not satisfiesComparator(v, c) then + return false + end + end + + return true +end + +local function formatConstraint(constraint: Constraint): string + if #constraint == 0 then + return "*" + end + + local parts: { string } = {} + for _, c in constraint do + -- Omit the "=" prefix for exact comparators; bare version is conventional. + local token = if c.op == "=" then format(c.version) else `{c.op}{format(c.version)}` + table.insert(parts, token) + end + + return table.concat(parts, " ") +end + +return { + parse = parse, + tryParse = tryParse, + parseConstraint = parseConstraint, + satisfies = satisfies, + compare = compare, + sort = sort, + format = format, + formatConstraint = formatConstraint, +} diff --git a/lute/cli/commands/pkg/loom-core/src/semver/solver.luau b/lute/cli/commands/pkg/loom-core/src/semver/solver.luau new file mode 100644 index 000000000..d42f86fed --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/semver/solver.luau @@ -0,0 +1,324 @@ +--!strict + +local tableext = require("@std/tableext") + +local parser = require("./parser") + +export type PackageVersion = { + read name: string, + read version: parser.Version, + read versionString: string, + read dependencies: { [string]: Requirement }, +} + +export type Requirement = { + read name: string, + read constraint: parser.Constraint, + read requiredBy: string?, + read aliasedAs: string?, +} + +-- All known versions per package name, sorted newest-first. +export type Universe = { [string]: { PackageVersion } } + +-- One selected PackageVersion per compatibility slot. +-- Slot keys: "name@major" (major>0), "name@0.minor" (0.Y), "name@0.0.patch" (0.0.Z) +export type Resolution = { [string]: PackageVersion } + +-- Constraints accumulated during the search, keyed by slot. +type ConstraintSet = { [string]: { [string]: Requirement } } + +local function computeSlot(name: string, version: parser.Version): string + if version.major > 0 then + return `{name}@{version.major}` + elseif version.minor > 0 then + return `{name}@0.{version.minor}` + else + return `{name}@0.0.{version.patch}` + end +end + +local function slotFromConstraint(name: string, constraint: parser.Constraint): string? + if #constraint == 0 then + return nil + end + + if #constraint == 1 and constraint[1].op == "=" then + return computeSlot(name, constraint[1].version) + end + + local lower: parser.Version? = nil + local upper: parser.Version? = nil + for _, comp in constraint do + if (comp.op == ">=" or comp.op == ">") and lower == nil then + lower = comp.version + elseif comp.op == "<" and upper == nil then + upper = comp.version + end + end + + -- FIXME(Luau): we had to split nil checks for better type inference, refinements _should_ still work if it was one check + if lower == nil then + return nil + end + if upper == nil then + return nil + end + + if lower.major > 0 and upper.major <= lower.major + 1 then + return `{name}@{lower.major}` + elseif lower.major == 0 and lower.minor > 0 and upper.major == 0 and upper.minor <= lower.minor + 1 then + return `{name}@0.{lower.minor}` + elseif lower.major == 0 and lower.minor == 0 and upper.major == 0 and upper.minor == 0 then + return `{name}@0.0.{lower.patch}` + end + + return nil +end + +local function nameFromSlot(slotKey: string): string + local name = slotKey:match("^(.+)@") + return name or slotKey +end + +local function resolveSlotKey(name: string, constraint: parser.Constraint, universe: Universe): string + local slot = slotFromConstraint(name, constraint) + if slot then + return slot + end + + local candidates = universe[name] + if candidates then + for _, c in candidates do + if parser.satisfies(c.version, constraint) then + return computeSlot(name, c.version) + end + end + end + + return `{name}@0` +end + +local function formatConflict(name: string, requirements: { Requirement }): string + local body: { string } = {} + for _, req in requirements do + local source = req.requiredBy or "root" + if req.aliasedAs then + table.insert(body, ` {parser.formatConstraint(req.constraint)} (required by {source} as {req.aliasedAs})`) + else + table.insert(body, ` {parser.formatConstraint(req.constraint)} (required by {source})`) + end + end + table.sort(body) + + return `version conflict for '{name}': no available version satisfies:\n` .. table.concat(body, "\n") +end + +local function constraintsFor(set: ConstraintSet, slotKey: string): { Requirement } + local list: { Requirement } = {} + for _, req in set[slotKey] or {} do + table.insert(list, req) + end + return list +end + +-- Pick the slot with the fewest satisfying candidates (MRV heuristic). +-- Tie-break alphabetically for determinism. +-- Returns nil when all constrained slots are already selected. +local function pickPending(set: ConstraintSet, selected: Resolution, universe: Universe): string? + local best: string? = nil + local bestCount: number = math.huge + + for slotKey in set do + if selected[slotKey] ~= nil then + continue + end + + local name = nameFromSlot(slotKey) + local candidates = universe[name] + if candidates == nil then + return slotKey + end + + local reqs = constraintsFor(set, slotKey) + local count = 0 + for _, candidate in candidates do + local satisfiesAll = tableext.all(reqs, function(req) + return parser.satisfies(candidate.version, req.constraint) + end) + if satisfiesAll then + count += 1 + end + end + + if count == 0 then + return slotKey + end + + -- fewest candiates with alphabetical tie-breaker + if best == nil or count < bestCount or (count == bestCount and slotKey < best) then + best = slotKey + bestCount = count + end + end + + return best +end + +-- Merge `deps` into the constraint set under sourceKey. +-- TODO check the constraints against already-selected versions (forward checking). +local function propagateDeps( + set: ConstraintSet, + sourceKey: string, + deps: { [string]: Requirement }, + universe: Universe +): ConstraintSet + local out: ConstraintSet = table.clone(set) + + for alias, dep in deps do + local slotKey = resolveSlotKey(dep.name, dep.constraint, universe) + + local existing = out[slotKey] + local merged: { [string]: Requirement } = if existing then table.clone(existing) else {} + merged[`{sourceKey}:{alias}`] = table.freeze({ + name = dep.name, + constraint = dep.constraint, + requiredBy = sourceKey, + }) + out[slotKey] = merged + end + + return out +end + +type DecisionFrame = { + read slotKey: string, + read candidates: { PackageVersion }, + candidateIdx: number, + read prevConstraints: ConstraintSet, + read prevSelected: Resolution, +} + +local function solve(universe: Universe, roots: { [string]: Requirement }, locked: { [string]: string }?): Resolution + -- Seed the constraint set from root requirements, keyed by slot. + local initial: ConstraintSet = {} + for alias, req in roots do + local slotKey = resolveSlotKey(req.name, req.constraint, universe) + initial[slotKey] = initial[slotKey] or {} + initial[slotKey][`root:{alias}`] = table.freeze({ + name = req.name, + constraint = req.constraint, + requiredBy = req.requiredBy, + aliasedAs = req.aliasedAs, + }) + end + + local stuck: { name: string, requirements: { Requirement }, depth: number }? = nil + + local stack: { DecisionFrame } = {} + local constraints = initial + local selected: Resolution = {} + + while true do + local slotKey = pickPending(constraints, selected, universe) + + if slotKey == nil then + return table.freeze(selected) + end + + local name = nameFromSlot(slotKey) + + if universe[name] == nil then + error(`unknown package '{name}': not found in universe`) + end + + local reqs = constraintsFor(constraints, slotKey) + + -- Filter candidates to those satisfying all current constraints. + local candidates: { PackageVersion } = {} + for _, candidate in universe[name] do + local satisfiesAll = tableext.all(reqs, function(r) + return parser.satisfies(candidate.version, r.constraint) + end) + if satisfiesAll then + table.insert(candidates, candidate) + end + end + + -- Prefer the previously-locked version if it satisfies current constraints. + if locked and #candidates > 1 then + local lockedVersion = locked[name] + if lockedVersion then + for i, candidate in candidates do + if candidate.versionString == lockedVersion and i > 1 then + table.remove(candidates, i) + table.insert(candidates, 1, candidate) + break + end + end + end + end + + -- Advance with the first candidate (universe is sorted newest-first). + if #candidates > 0 then + local candidate = candidates[1] + local sourceKey = `{candidate.name}@{candidate.versionString}` + local nextSelected = table.clone(selected) + nextSelected[slotKey] = candidate + local nextConstraints = propagateDeps(constraints, sourceKey, candidate.dependencies, universe) + + table.insert(stack, { + slotKey = slotKey, + candidates = candidates, + candidateIdx = 1, + prevConstraints = constraints, + prevSelected = selected, + }) + constraints = nextConstraints + selected = nextSelected + else + local depth = #stack + if stuck == nil or depth < stuck.depth then + stuck = { name = name, requirements = reqs, depth = depth } + end + + -- Backtrack: try next candidates at earlier decision levels. + local backtracked = false + while #stack > 0 do + local frame = stack[#stack] + + if frame.candidateIdx < #frame.candidates then + local idx = frame.candidateIdx + 1 + local candidate = frame.candidates[idx] + local sourceKey = `{candidate.name}@{candidate.versionString}` + local nextSelected = table.clone(frame.prevSelected) + nextSelected[frame.slotKey] = candidate + local nextConstraints = + propagateDeps(frame.prevConstraints, sourceKey, candidate.dependencies, universe) + + frame.candidateIdx = idx + constraints = nextConstraints + selected = nextSelected + backtracked = true + if stuck and stuck.depth > #stack then + stuck = nil + end + break + end + + table.remove(stack) + end + + if not backtracked then + if stuck then + error(formatConflict(stuck.name, stuck.requirements)) + end + error("dependency resolution failed") + end + end + end +end + +return { + solve = solve, +} diff --git a/lute/cli/commands/pkg/loom-core/src/store.luau b/lute/cli/commands/pkg/loom-core/src/store.luau new file mode 100644 index 000000000..106874ca7 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/store.luau @@ -0,0 +1,38 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") + +local STORE_PATH_PREFIX = "@pkg/" + +local function getStoreDirectory(): string + return path.format(path.join(process.homedir(), ".loom", "store")) +end + +local function getPackagePath(key: string): string + return path.format(path.join(getStoreDirectory(), key)) +end + +local function hasPackage(key: string): boolean + return fs.exists(getPackagePath(key)) +end + +local function ensureStoreDirectory(): () + local storeDir = getStoreDirectory() + + if not fs.exists(storeDir) then + fs.createDirectory(storeDir, { makeParents = true }) + end +end + +-- Returns the portable alias form stored in the lockfile, e.g. "@loom/pkg@rev". +local function toStorePath(key: string): string + return `{STORE_PATH_PREFIX}{key}` +end + +return { + getStoreDirectory = getStoreDirectory, + getPackagePath = getPackagePath, + hasPackage = hasPackage, + ensureStoreDirectory = ensureStoreDirectory, + toStorePath = toStorePath, +} diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 3b4a0056f..3550dba18 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -1,20 +1,72 @@ +local cli = require("@batteries/cli") 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 typedefs = require("./lib/typedefs") -local homedir = process.homedir() +local BASE_PATH = typedefs.TYPEDEFS_PATH +local BASE_PATH_PRETTY = typedefs.TYPEDEFS_RELATIVE_DIR +local TEMPLATE = typedefs.LUAURC_TEMPLATE + +local args = cli.parser() +args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) +args:add("with-luaurc", "flag", { help = "Also write a .luaurc file in the current directory" }) +args:parse({ ... }) + +if args:has("help") then + print("Usage: lute setup [options]") + print([[ +Install Lute type definitions to ~/.lute/typedefs/{version}. + +OPTIONS: + --with-luaurc Also write a .luaurc file in the current directory + -h, --help Show this help message +]]) + return +end --- 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 + +local typedefsLuauRcPath = path.join(BASE_PATH, ".luaurc") + +-- LUAUFIX. Without an annotation on typedefs.LUAURC_TEMPLATE, we arent able to figure out the type of TEMPLATE +-- If that table was declared in this file, this typechecks, but if it's in a different one, we'll get a type error here +fs.writeStringToFile(typedefsLuauRcPath, json.serialize(TEMPLATE, true)) + +print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) + +if not args:has("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.aliases :: { [string]: string } do + rcFile.aliases[key] = value + end +else + rcFile = TEMPLATE 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..b9c067b91 --- /dev/null +++ b/lute/cli/commands/test/filter.luau @@ -0,0 +1,59 @@ +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 = table.clone(suite) + filteredSuite.cases = caseEntry.suites[suiteName] + 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 = table.clone(suite) + filteredSuite.cases = cases + 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..984a78aa8 --- /dev/null +++ b/lute/cli/commands/test/finder.luau @@ -0,0 +1,51 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local ignore = require("../lib/ignore") + +local function istestfile(filename: string): boolean + return stringext.hasSuffix(filename, ".test.luau") or stringext.hasSuffix(filename, ".spec.luau") +end + +local function issnapfile(filename: string): boolean + return stringext.hasSuffix(filename, ".snap.luau") +end + +local function findfiles(paths: { string }, predicate: (string) -> boolean): { path.Path } + local files = {} + local resolver = ignore.new() + + local function walk(directory: path.Path) + for _, entry in fs.listDirectory(directory) do + local fullPath = path.join(directory, entry.name) + if entry.type == "dir" then + if not resolver:isIgnoredDirectory(fullPath) then + walk(fullPath) + end + elseif predicate(entry.name) and not resolver:isIgnoredFile(fullPath) then + table.insert(files, fullPath) + end + end + end + + for _, p in paths do + local rooted = path.resolve(p) + if fs.type(rooted) == "dir" then + walk(rooted) + elseif predicate(path.format(rooted)) then + table.insert(files, rooted) + end + end + + return files +end + +local function findtestfiles(paths: { string }): { path.Path } + return findfiles(paths, istestfile) +end + +local function findsnapfiles(paths: { string }): { path.Path } + return findfiles(paths, issnapfile) +end + +return table.freeze({ findtestfiles = findtestfiles, findsnapfiles = findsnapfiles }) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau new file mode 100644 index 000000000..fd91c8728 --- /dev/null +++ b/lute/cli/commands/test/init.luau @@ -0,0 +1,183 @@ +local cli = require("@batteries/cli") +local finder = require("@self/finder") +local luau = require("@std/luau") +local path = require("@std/path") +local ps = require("@std/process") +local system = require("@std/system") +local task = require("@std/task") +local vm = require("@lute/vm") +local testtypes = require("@std/test/types") +local reporter = require("@self/reporter") +local test = require("@std/test") +local snaprunner = require("@self/snap/runner") +local updater = require("@self/snap/updater") + +local function loadtests(testfiles: { path.Path }) + -- Load all test files first + for _, p in testfiles do + local success, err = pcall(luau.loadModule, p, nil) + + if not success then + print(`Error loading {path.format(p)}: {err}`) + ps.exit(1) + end + end +end + +local function listtests() + -- Gets all registered tests using the shared test instance + local registered = test._registered() + + 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( + testfiles: { path.Path }, + suiteName: string?, + caseName: string?, + runOptions: testtypes.TestRunnerOptions +) + local result = { failures = {}, total = 0, failed = 0, passed = 0 } + local workerFailures: { string } = {} + local nfiles = #testfiles + + if nfiles > 0 then + local nworkers = math.max(1, math.min(system.threadCount(), nfiles)) + + if runOptions.verbose then + print(`Running tests on {nworkers} worker{if nworkers == 1 then "" else "s"}...`) + end + + local cursor = 1 + local function takenext(): path.Path? + if cursor > nfiles then + return nil + end + local p = testfiles[cursor] + cursor += 1 + return p + end + + local done = 0 + for _ = 1, nworkers do + task.spawn(function() + local current: path.Path? = nil + xpcall(function() + local worker = vm.create("@self/worker") + while true do + current = takenext() + if not current then + break + end + local res = worker.runfile({ + file = path.format(current :: path.Path), -- LUAUFIX: remove typecast when refinements supports break statements + suite = suiteName, + case = caseName, + verbose = runOptions.verbose, + }) + if res.__tag == "err" then + table.insert(workerFailures, res.error) + else + local r = res.value + result.total += r.total + result.passed += r.passed + result.failed += r.failed + for _, f in r.failures do + table.insert(result.failures, f) + end + end + end + end, function(err: any) + local where = if current then path.format(current) else "" + table.insert(workerFailures, `worker errored while running {where}: {tostring(err)}`) + end) + done += 1 + end) + end + + while done < nworkers do + task.wait() + end + end + + reporter.color(result) + reporter.workerFailures(workerFailures) + if result.failed ~= 0 or #workerFailures > 0 then + ps.exit(1) + end + ps.exit(0) +end + +local function runsnapshots(searchpaths: { string }, update: boolean, interactive: boolean) + local files = finder.findsnapfiles(searchpaths) + + if #files == 0 then + print("No snapshot files (*.snap.luau) found.") + ps.exit(0) + return + end + + local result = snaprunner.runsnapshots(files) + reporter.snap(result) + updater.updatesnapshot(result, update, interactive) +end + +local function main(...: string) + local args = cli.parser() + + args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) + args:add("list", "flag", { help = "List all discovered test cases without running them" }) + args:add( + "verbose", + "flag", + { help = "Run tests verbosely (prints test case and suite names before executing)", aliases = { "v" } } + ) + args:add("suite", "option", { help = "Run only tests in the specified suite", aliases = { "s" } }) + args:add("case", "option", { help = "Run only test cases matching the specified name", aliases = { "c" } }) + args:add("interactive", "flag", { help = "Interactively accept/reject snapshot changes", aliases = { "i" } }) + args:add("update", "flag", { help = "Auto-accept all snapshot changes", aliases = { "u" } }) + + args:parse({ ... }) + + if args:has("help") then + print(args:help()) + return + end + + local forwarded = args:forwarded() + -- Check if this is a snapshot subcommand + if forwarded and #forwarded > 0 and forwarded[1] == "snapshots" then + if #forwarded == 1 then + runsnapshots({ "./tests" }, args:has("update"), args:has("interactive")) + else + local searchpaths = {} + for idx = 2, #forwarded do + table.insert(searchpaths, forwarded[idx]) + end + runsnapshots(searchpaths, args:has("update"), args:has("interactive")) + end + end + + local searchpath = if forwarded and #forwarded > 0 then forwarded else { "./" } + local testfiles = finder.findtestfiles(searchpath) + + if args:has("list") then + loadtests(testfiles) + listtests() + else + local runOpts: testtypes.TestRunnerOptions = { verbose = args:has("verbose") } + runtests(testfiles, args:get("suite"), args:get("case"), runOpts) + end +end + +main(...) diff --git a/lute/cli/commands/test/reporter.luau b/lute/cli/commands/test/reporter.luau new file mode 100644 index 000000000..351aa02b7 --- /dev/null +++ b/lute/cli/commands/test/reporter.luau @@ -0,0 +1,130 @@ +local rt = require("@batteries/richterm") +local path = require("@std/path") +local tys = require("@std/test/types") +local snapty = require("./snap/types") +local snapsty = require("./snap/styles") + +local function splitStacktrace(stacktrace: string): { string } + local lines = {} + for line in stacktrace:gmatch("[^\n]+") do + table.insert(lines, line) + end + return lines +end + +-- Styles +local pass = rt.combine(rt.green, rt.bold) +local fail = rt.combine(rt.red, rt.bold) +local testname = rt.combine(rt.bold, rt.white) +local errlabel = rt.yellow +local errmsg = rt.red +local stkframe = rt.red +local location = rt.cyan +local dim = rt.dim +local separator = dim + +local function printFailure(failed: tys.FailedTest) + print(fail(" FAIL ") .. testname(failed.test)) + if failed.failure.__tag == "assertion" then + print(location(`\t{failed.failure.filename}:{failed.failure.linenumber}`)) + print(errmsg(`\t{failed.failure.assertion}: {failed.failure.msg}`)) + elseif failed.failure.__tag == "lifecycle" then + print(location(`\t{failed.failure.filename}:{failed.failure.linenumber}`)) + print(errmsg(`\t{failed.failure.hook}: {failed.failure.msg}`)) + else + print(errlabel("\tRuntime error: ") .. errmsg(failed.failure.msg)) + print(stkframe("\tStacktrace:")) + for _, line in splitStacktrace(failed.failure.stacktrace) do + print(stkframe(`\t {line}`)) + end + end +end + +local function printSummary(result: tys.TestRunResult) + print(separator(string.rep("─", 50))) + print( + rt.bold("Results: ") + .. pass(`{result.passed} passed`) + .. dim(", ") + .. fail(`{result.failed} failed`) + .. dim(` of {result.total}`) + ) +end + +local function colorReporter(result: tys.TestRunResult) + if #result.failures > 0 then + print("") + print(fail("Failures:")) + print("") + for _, failed in result.failures do + printFailure(failed) + print("") + end + end + + printSummary(result) +end + +local function workerFailuresReporter(failures: { string }) + if #failures == 0 then + return + end + + print("") + print(fail("Worker failures:")) + print("") + for _, msg in failures do + print(errlabel(" WORKER ") .. msg) + end +end + +local function snapReporter(result: snapty.SnapResult) + local total = #result.passed + #result.failed + #result.new + + for _, p in result.passed do + print(snapsty.pass("\tPASS ") .. snapsty.filepath(path.format(p.filepath))) + end + + for _, n in result.new do + print(snapsty.newstyle("\tNEW ") .. snapsty.filepath(path.format(n.filepath))) + end + + if #result.failed > 0 then + print("") + print(snapsty.fail("Failures:")) + print("") + for _, f in result.failed do + print(snapsty.fail("\tFAIL ") .. snapsty.filepath(path.format(f.filepath))) + print("") + if f.stderr and f.stderr ~= "" then + print(snapsty.dim(`Failed to execute test process with err {f.stderr}`)) + print("") + end + + if f.exitCondition and f.exitCondition.expected ~= f.exitCondition.actual then + print( + snapsty.fail( + `Exit code mismatch: expected {f.exitCondition.expected}, got {f.exitCondition.actual}` + ) + ) + print("") + else + print(f.diff) + print("") + end + end + end + + print(separator(string.rep("─", 50))) + local parts: { string } = {} + table.insert(parts, snapsty.pass(`{#result.passed} passed`)) + if #result.new > 0 then + table.insert(parts, snapsty.newstyle(`{#result.new} new`)) + end + if #result.failed > 0 then + table.insert(parts, snapsty.fail(`{#result.failed} failed`)) + end + print(snapsty.bold("Snapshots: ") .. table.concat(parts, snapsty.dim(", ")) .. snapsty.dim(` of {total}`)) +end + +return table.freeze({ color = colorReporter, snap = snapReporter, workerFailures = workerFailuresReporter }) diff --git a/lute/cli/commands/test/snap/runner.luau b/lute/cli/commands/test/snap/runner.luau new file mode 100644 index 000000000..d6e9b4b42 --- /dev/null +++ b/lute/cli/commands/test/snap/runner.luau @@ -0,0 +1,202 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local process = require("@std/process") + +local strext = require("@std/stringext") + +local difftext = require("@batteries/difftext") + +local snapty = require("./types") + +-- Derive the .snap artifact path from a .snap.luau file path. +-- e.g. /foo/bar.snap.luau -> /foo/bar.snap +local function artifactFromPath(filepath: path.Path): string + local f = path.format(filepath) + assert(strext.hasSuffix(f, ".snap.luau"), `expected {f} to have suffix .snap.luau`) + return strext.removeSuffix(f, ".luau") -- strips ".luau" +end + +local function retrieveConfiguration(directory: string): snapty.SnapshotConfiguration? + local configFile = path.join(directory, "snap.config.luau") + if not fs.exists(configFile) then + return nil + end + + local ok, v = pcall(luau.loadModule, configFile, nil) + if not ok then + error("Failed to load snapshot testing configuration at path: " .. directory) + end + + return v +end + +-- Returns the argument and expected exit code if any +local function getArtifactTestConfiguration( + execpath: string, + filepath: string, + configuration: snapty.SnapshotConfiguration?, + artifactPath: string +): ({ string }, number?) + local args = { execpath } + local expectedExit = nil + local pathEnd = path.basename(path.parse(artifactPath)) + local testname = "" + if pathEnd ~= nil then + testname = strext.removeSuffix(pathEnd, ".snap") + end + -- configuration here is the discovered file with snapshot testing config + -- it also has + if not configuration then + table.insert(args, filepath) + return args, expectedExit + end + + -- Resolve args and expectedExitCode independently: override field takes priority, then base config + local base = configuration.default + local override = if configuration.overrides then configuration.overrides[testname] else nil + + local configArgs: { string }? = nil + if override and override.args then + configArgs = override.args + elseif base and base.args then + configArgs = base.args + end + + local configExit: number? = nil + if override and override.expectedExitCode then + configExit = override.expectedExitCode + elseif base and base.expectedExitCode then + configExit = base.expectedExitCode + end + + -- Config args go between execpath and filepath (e.g. lute lint --flags ) + if configArgs then + for _, arg in configArgs do + table.insert(args, arg) + end + end + if configExit then + expectedExit = configExit + end + + table.insert(args, filepath) + return args, expectedExit +end + +local function posixify(str) + local posixy, _ = string.gsub(str, [[\]], "/") + return posixy +end + +local function normalizeLineEndings(str: string): string + local noCrlf, _ = string.gsub(str, "\r+\n", "\n") + local noCr, _ = string.gsub(noCrlf, "\r+", "\n") + return noCr +end + +local function normalizeSnapshotOutput(output: string, normalizedCwd: string): string + local nobackslash = posixify(normalizeLineEndings(output)) + local replacedCwd, _ = string.gsub(nobackslash, normalizedCwd, "") + return replacedCwd +end + +local runner = {} + +function runner.runsnapshots(files: { path.Path }): snapty.SnapResult + local result: snapty.SnapResult = { + passed = {}, + failed = {}, + new = {}, + } + + local execpath = path.format(process.execPath()) + + -- Compute the normalized current working directory exactly once + --[[ + Paths can have '.' characters in them, but gsub takes patterns as arguments and a '.' will match any single character. + E.g. Interpreting "/foo/bar.baz" as a pattern, will match "/foo/bar[any char]baz". + We need to replace any explicit '.' in a string that will be used as a pattern, with the escaped '.' + Gsub takes two patterns as its second and third argument, so we'll need to replace all '.' in the string(represented by the pattern '%.') + with the '.' matching pattern '%.'(itself represented by escaping the %, '%%.'). + ]] + -- + local cwd = posixify(path.format(process.cwd())) + cwd, _ = string.gsub(cwd, "%.", "%%.") + + local configCache: { [string]: snapty.SnapshotConfiguration | "missing" } = {} + for _, filepath in files do + local configuration: snapty.SnapshotConfiguration? = nil + local directory = path.dirname(filepath) + local maybeConfig = configCache[directory] + if maybeConfig == nil then + local ok, confOrErr = pcall(retrieveConfiguration, directory) + if not ok then + -- Something went wrong loading this module, and pcall will have yielded an error for us + print(confOrErr) + break + end + + local conf = confOrErr + -- it doesn't exist + if not conf then + configCache[directory] = "missing" + else + configCache[directory] = conf + configuration = conf + end + elseif maybeConfig ~= "missing" then + configuration = maybeConfig + end + + local artifactpath = artifactFromPath(filepath) + local args, expectedExit = + getArtifactTestConfiguration(execpath, path.format(filepath), configuration, artifactpath) + + local proc = process.run(args) + local checkExitMismatch: snapty.ProcessExitCondition? = nil + if expectedExit then + checkExitMismatch = { expected = expectedExit, actual = proc.exitcode } + end + + local actual = normalizeSnapshotOutput(proc.stdout, cwd) + + if not fs.exists(artifactpath) then + -- No artifact file yet — this is a new snapshot + table.insert(result.new, { + filepath = filepath, + artifactpath = artifactpath, + actual = actual, + }) + continue + end + + local rawExpected = fs.readFileToString(artifactpath) + local expected = normalizeSnapshotOutput(rawExpected, cwd) + -- A test passes when no exit code checking is requested and b) snapshot contents match + -- It also passes when exit code checking is requested and b)snapshot contents match and c) exit codes match + local exitCodePasses = not checkExitMismatch or checkExitMismatch.expected == checkExitMismatch.actual + + if actual == expected and exitCodePasses then + table.insert(result.passed, { filepath = filepath, artifactpath = artifactpath }) + else + local diff = difftext.prettydiff(expected, actual, { + detailed = true, + includeLineNumbers = true, + }) + table.insert(result.failed, { + filepath = filepath, + artifactpath = artifactpath, + actual = actual, + expected = expected, + exitCondition = checkExitMismatch, + diff = diff, + stderr = if not proc.ok then proc.stderr else "", + }) + end + end + + return result +end + +return table.freeze(runner) diff --git a/lute/cli/commands/test/snap/styles.luau b/lute/cli/commands/test/snap/styles.luau new file mode 100644 index 000000000..70f32b8b3 --- /dev/null +++ b/lute/cli/commands/test/snap/styles.luau @@ -0,0 +1,10 @@ +local rt = require("@batteries/richterm") + +return table.freeze({ + bold = rt.bold, + filepath = rt.cyan, + dim = rt.dim, + pass = rt.combine(rt.green, rt.bold), + fail = rt.red, + newstyle = rt.combine(rt.yellow, rt.bold), +}) diff --git a/lute/cli/commands/test/snap/types.luau b/lute/cli/commands/test/snap/types.luau new file mode 100644 index 000000000..8761897ad --- /dev/null +++ b/lute/cli/commands/test/snap/types.luau @@ -0,0 +1,51 @@ +local path = require("@std/path") + +export type SnapPass = { + filepath: path.Path, + artifactpath: string, +} + +export type ProcessExitCondition = { + expected: number, + actual: number, +} + +export type SnapFail = { + filepath: path.Path, + artifactpath: string, + actual: string, + expected: string, + exitCondition: ProcessExitCondition?, + diff: string, + stderr: string?, +} + +export type SnapNew = { + filepath: path.Path, + artifactpath: string, + actual: string, +} + +export type SnapResult = { + passed: { SnapPass }, + failed: { SnapFail }, + new: { SnapNew }, +} + +--- Configuration for an individual test +export type TestConfiguration = { + --- Arguments to pass `lute` before the snapshot file + args: { string }?, + --- Expected process exit code - if nil, not checked + expectedExitCode: number?, +} + +--- SnapshotConfiguration format +export type SnapshotConfiguration = { + --- Default arguments for the directory + default: TestConfiguration?, + --- Per test case overrides - each override is just the name of the snapshot (no file extensions) + overrides: { [string]: TestConfiguration? }?, +} + +return {} diff --git a/lute/cli/commands/test/snap/updater.luau b/lute/cli/commands/test/snap/updater.luau new file mode 100644 index 000000000..b6eb1a308 --- /dev/null +++ b/lute/cli/commands/test/snap/updater.luau @@ -0,0 +1,139 @@ +local fs = require("@std/fs") +local snapty = require("./types") +local io = require("@std/io") +local path = require("@std/path") +local sty = require("./styles") +local ps = require("@std/process") +local strext = require("@std/stringext") + +-- Write content directly to a .snap artifact file. +-- Used both for creating new snapshots and updating existing ones. +local function writeSnapshot(snappath: string, content: string): boolean + local ok = pcall(fs.writeStringToFile, snappath, content) + return ok +end + +local function acceptAll(result: snapty.SnapResult) + -- Auto-accept all new snapshots and failures + local accepted = 0 + for _, n in result.new do + local ok = writeSnapshot(n.artifactpath, n.actual) + if ok then + accepted += 1 + else + print(`Failed to create {n.artifactpath}`) + end + end + for _, f in result.failed do + local ok = writeSnapshot(f.artifactpath, f.actual) + if ok then + accepted += 1 + else + print(`Failed to update {f.artifactpath}`) + end + end + print(`\nUpdated {accepted} snapshot(s).`) +end + +local function acceptInteractive(failures: { snapty.SnapFail }, news: { snapty.SnapNew }): number + local function prompt(acceptAll: boolean): string + if acceptAll then + return "a" + end + local prompt = sty.dim("[a]ccept [r]eject [A]ccept all > ") + local choice = io.input(prompt) + return strext.trim(choice) + end + + local function updateArtifact(choice, artifactpath, content): boolean + local created = false + if choice == "a" then + created = writeSnapshot(artifactpath, content) + if created then + print(sty.pass("\tCreated ") .. sty.filepath(artifactpath)) + else + print(sty.fail("\tFailed to write ") .. sty.filepath(artifactpath)) + end + else + print(sty.dim("\tSkipped")) + end + return created + end + + local accepted = 0 + + local acceptAll = false + -- Process new snapshots + + for i, n in news do + print(sty.bold(`\nNew Snapshot {i}/{#news}: `) .. sty.filepath(path.format(n.filepath))) + print(sty.dim("\tNo .snap file exists. Actual output:")) + print("") + print(n.actual) + print("") + + local ok = false + if acceptAll then + ok = updateArtifact("a", n.artifactpath, n.actual) + else + local choice = prompt(acceptAll) + if choice == "A" then + acceptAll = true + choice = "a" + end + ok = updateArtifact(choice, n.artifactpath, n.actual) + end + + accepted += if ok then 1 else 0 + end + + for i, f in failures do + print(sty.bold(`\nSnapshot {i}/{#failures}: `) .. sty.filepath(path.format(f.filepath))) + print("") + print(f.diff) + print("") + local ok = false + if acceptAll then + ok = updateArtifact("a", f.artifactpath, f.actual) + else + local choice = prompt(acceptAll) + if choice == "A" then + acceptAll = true + choice = "a" + end + ok = updateArtifact(choice, f.artifactpath, f.actual) + end + accepted += if ok then 1 else 0 + end + + return accepted +end + +local update = {} + +function update.updatesnapshot(result: snapty.SnapResult, update: boolean, interactive: boolean) + local hasNew = #result.new > 0 + local hasFailed = #result.failed > 0 + if hasNew or hasFailed then + -- Auto-accept both failed and new output + if update then + acceptAll(result) + elseif interactive then + local accepted = acceptInteractive(result.failed, result.new) + print(`\nUpdated {accepted} snapshot(s).`) + else + if hasNew then + print(`\n{#result.new} new snapshot(s) found.`) + end + if hasFailed then + print(`{#result.failed} snapshot(s) failed.`) + end + + print(`Run with --interactive to review or --update to accept all`) + ps.exit(1) + end + end + ps.exit(0) +end + +return table.freeze(update) diff --git a/lute/cli/commands/test/worker.luau b/lute/cli/commands/test/worker.luau new file mode 100644 index 000000000..348a856b3 --- /dev/null +++ b/lute/cli/commands/test/worker.luau @@ -0,0 +1,30 @@ +local luau = require("@std/luau") +local env = require("@std/test/env") +local runner = require("@std/test/runner") +local types = require("@std/test/types") +local filter = require("./filter") + +export type Result = { __tag: "ok", value: T } | { __tag: "err", error: E } + +local function reset() + table.clear(env.anonymous) + table.clear(env.suites) + table.clear(env.suiteindex) + table.clear(env.caseindex) +end + +local function runfile( + args: { file: string, suite: string?, case: string?, verbose: boolean? } +): Result + reset() + + local ok, err = pcall(luau.loadModule, args.file, nil) + if not ok then + return { __tag = "err", error = tostring(err) } + end + + local filtered = filter.filtertests(env, args.suite, args.case) + return { __tag = "ok", value = runner.run(filtered, { verbose = args.verbose == true }) } +end + +return table.freeze({ runfile = runfile }) diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau new file mode 100644 index 000000000..6b97d32ce --- /dev/null +++ b/lute/cli/commands/transform/init.luau @@ -0,0 +1,274 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local pathLib = require("@std/path") +local process = require("@std/process") +local printer = require("@std/syntax/printer") +local syntax = require("@std/syntax") + +local richterm = require("@batteries/richterm") +local io = require("@std/io") +local printDiffHunks = require("./transform/printDiffHunks") +local cli = require("@batteries/cli") +local files = require("./lib/files") +local types = require("@transform") + +local function exhaustiveMatch(value: never): never + error(`Unknown value in exhaustive match: {value}`) +end + +local function confirmChange(prompt: string): boolean + local userInput = io.input(prompt) + local answer = userInput:match("^%s*(.-)%s*$") or "" + return answer:lower() == "y" +end + +local function loadMigration(path: string): types.Migration + local success, loaded = pcall(luau.loadModule, path, nil) + 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, + interactive: 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 + local deletionConfirmed = true + if interactive then + -- prompt to confirm deletion + deletionConfirmed = confirmChange(richterm.bold(`\nDelete {pathStr}? (y/n)`)) + end + + if deletionConfirmed then + print(`Marking {pathStr} for deletion`) + table.insert(deletedPaths, pathStr) + end + end + elseif not dryRun then + if outputFilePath then + assert(outputFilePath ~= nil) + fs.writeStringToFile(outputFilePath, toWrite) + elseif toWrite ~= source then + local changesConfirmed = true + if interactive then + local fileDiff = printDiffHunks(source, toWrite) + + -- First: print out diff w/ option to accept / deny + print(`\n` .. fileDiff .. `\n`) + changesConfirmed = confirmChange(richterm.bold(`Accept changes to {pathStr}? (y/n)`)) + print(if changesConfirmed then `Changes confirmed` else `Changes denied` .. `\n`) + end + if changesConfirmed then + fs.writeStringToFile(pathStr, toWrite) + end + end + end + end + + if not dryRun then + for _, pathStr in deletedPaths do + print(`Deleting {pathStr}`) + fs.remove(pathStr) + end + end +end + +local USAGE = "Usage: lute transform [options] [files...] [-- [--option value...]]" + +local function printHelp() + print(USAGE) + print([[ +Apply the specified code transformation to the given files. + +OPTIONS: + -h, --help Show this help message + --dry-run Preview changes without writing files + -o, --output [FILE] Write output to a single file instead of in-place (only works if a single input file is specified) + +lute transform also supports forwarding options to the code transformation script by placing them after a '--' separator. For example: + + lute transform my-migration.luau files/ -- --option value + ]]) +end + +local function main(...: string) + local args = cli.parser() + args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) + args:add("dry-run", "flag", { help = "Preview changes without writing any files" }) + args:add( + "interactive", + "flag", + { help = "Interactive mode, where you can preview and confirm changes", aliases = { "i" } } + ) + args:add("output", "option", { help = "Write output to this file (single input only)", aliases = { "o" } }) + args:add("migration-path", "positional", { help = "Path to the migration script to run" }) + args:add( + "files", + "variadic", + { help = "Files or directories to apply the migration to (defaults to current directory if not specified)" } + ) + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local migrationPath = args:get("migration-path") + if not migrationPath then + print(`Error: No code transformation script specified.\n\n{USAGE}\nUse --help for more information.`) + process.exit(1) + error("Unreachable") -- LUAUFIX: Remove once we can refine migrationPath is string outside this block + end + + local dryRun = args:has("dry-run") + local interactive = args:has("interactive") + local outputFile = args:get("output") + + -- Split forwarded args into file paths and migration options. + -- Non-flag tokens are file paths; --name value pairs are migration options. + local filePaths: { string } = args:variadics() + local migrationOptions: { [string]: string } = {} + local forwarded = args:forwarded() or {} + local i = 1 + while i <= #forwarded do + local token = forwarded[i] + local name = string.sub(token, 3) + i += 1 + assert(i <= #forwarded, `Missing value for '--{name}'`) + migrationOptions[name] = forwarded[i] + i += 1 + end + + assert(outputFile == nil or #filePaths == 1, "When specifying an output file, only one input file is allowed") + + if dryRun then + print("Executing in dry run mode") + end + + if interactive then + print("Executing in interactive mode") + end + + if outputFile then + print(`Output path specified: '{outputFile}'`) + end + + print(`Loading migration '{migrationPath}'`) + local migration = loadMigration(migrationPath) + + print("Finding source files") + local sourceFiles = files.getSourceFiles(filePaths) + if #sourceFiles == 0 then + error("error: no source files provided") + end + + print("Processing migration options") + local processedOptions = processMigrationOptions(migration, migrationOptions) + + print("Applying migration") + applyMigration(migration, sourceFiles, processedOptions, dryRun, interactive, outputFile) + + print(`Processed {#sourceFiles} files!`) +end + +main(...) diff --git a/lute/cli/commands/transform/printDiffHunks.luau b/lute/cli/commands/transform/printDiffHunks.luau new file mode 100644 index 000000000..f9bba3dda --- /dev/null +++ b/lute/cli/commands/transform/printDiffHunks.luau @@ -0,0 +1,61 @@ +--[[ +Module to trim the diff of a file into hunks + +Better solution is prob to do this at the printdiff level, but I didn't feel like doing all that yet 🫠 +]] + +local prettydiff = require("@batteries/difftext").prettydiff + +local function getDiffType(line: string) + -- Looks for + or -, followed by spaces, numbers, spaces, and a | + return line:match("([%+%-])%s*%d+%s*|") +end + +local function isDiffLine(line: string) + return getDiffType(line) ~= nil +end + +local PATCH_CONTEXT_LIMIT = 3 +local function trimDiff(diff: string) + --[[ + iterate thru diff + keep track of diff hunks (where things actually changed) (we'll build a table of these and concat them at the end + ]] + local lines = string.split(diff, "\n") + + local inDiff = false + local diffBuffer = 0 + local activeHunk: { string } = {} + local hunks = {} + for _, line in lines do + if isDiffLine(line) then + table.insert(activeHunk, line) + inDiff = true + elseif inDiff and diffBuffer < PATCH_CONTEXT_LIMIT then + table.insert(activeHunk, line) + diffBuffer += 1 + else + if inDiff then + -- capture the hunk we're leaving + table.insert(hunks, table.concat(activeHunk, "\n")) + end + inDiff, diffBuffer, activeHunk = false, 0, {} + end + end + + if inDiff then + table.insert(hunks, table.concat(activeHunk, "\n")) + end + + return table.concat(hunks, "\n...\n") +end + +local function conciseDiff(a, b): string + local diff = prettydiff(a, b, { + includeLineNumbers = true, + }) + + return trimDiff(diff) +end + +return conciseDiff diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index d2bc33aae..89d66196b 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -1,7 +1,7 @@ #pragma once -struct lua_State; -struct Runtime; +#include -lua_State* setupCliState(Runtime& runtime); -int cliMain(int argc, char** argv); +class LuteReporter; + +int cliMain(int argc, char** 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/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..f6b149715 --- /dev/null +++ b/lute/cli/include/lute/profiler.h @@ -0,0 +1,16 @@ +#pragma once +#include "lute/reporter.h" + +#include + +struct lua_State; + +struct ProfileOptions +{ + std::string filename; + int frequency = 10000; +}; + +void profilerStart(lua_State* L, int frequency); +void profilerStop(); +void profilerDump(const char* path, LuteReporter& reporter); diff --git a/lute/cli/include/lute/requiresetup.h b/lute/cli/include/lute/requiresetup.h new file mode 100644 index 000000000..05dbd0074 --- /dev/null +++ b/lute/cli/include/lute/requiresetup.h @@ -0,0 +1,27 @@ +#pragma once + +#include "lute/userlandvfs.h" + +#include "Luau/DenseHash.h" + +#include +#include + +struct lua_State; +struct Runtime; + +lua_State* setupRunState(Runtime& runtime, std::function preSandboxInit = nullptr); + +lua_State* setupCliCommandState(Runtime& runtime, std::function preSandboxInit = nullptr); + +lua_State* setupPkgRunState( + Runtime& runtime, + std::vector directDependencies, + std::vector> allDependencies +); + +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luauConfigFiles, + 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..bbd668d62 --- /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& getLuauConfigFiles() const + { + return luauConfigFiles; + } + + 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 luauConfigFiles{""}; // 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 resolveModule(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/clicommands_stub.cpp b/lute/cli/src/clicommands_stub.cpp new file mode 100644 index 000000000..9422448c0 --- /dev/null +++ b/lute/cli/src/clicommands_stub.cpp @@ -0,0 +1,11 @@ +#include "lute/clicommands.h" + +CliModuleResult getCliModule(std::string_view) +{ + return {CliModuleType::NotFound}; +} + +std::optional getCliCommand(std::string_view) +{ + return std::nullopt; +} diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index f190f69ed..376f8eb75 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -1,137 +1,177 @@ -#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/fileutils.h" +#include "lute/luauflags.h" +#include "lute/modulepath.h" #include "lute/options.h" -#include "lute/ref.h" -#include "lute/require.h" +#include "lute/packagerun.h" +#include "lute/process.h" +#include "lute/profiler.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" -#ifdef _WIN32 -#include -#endif +#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 +#include +#include +#include #include -#include +#include #include -static int program_argc = 0; -static char** program_argv = nullptr; - -void* createCliRequireContext(lua_State* L) -{ - void* ctx = lua_newuserdatadtor( - L, - sizeof(RequireCtx), - [](void* ptr) - { - static_cast(ptr)->~RequireCtx(); - } - ); - - if (!ctx) - luaL_error(L, "unable to allocate RequireCtx"); - - ctx = new (ctx) RequireCtx{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; -} +#ifdef _WIN32 +#include +#endif -lua_State* setupCliState(Runtime& runtime) +static const char* HELP_STRING = R"(Usage: lute [options] [arguments...] + +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. + +Run Options (when using 'run' or no command): + lute [run] [args...] + Executes the script, passing [args...] to it. + +Check Options: + lute check [file2.luau...] + Performs a type check on the specified files. + +Compile Options: + lute compile [--output ] + Compiles entry point and auto-discovered dependencies into a standalone executable. + +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. + +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. + +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. + +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: + --list List all available scripts in the nearest .lute folder. + --profile Enable profiling for the script. + --profile-output Output file for the profile (default: _.json). + --frequency Profiler sampling frequency in Hz (default: 10000). + -h, --help Display this usage message. +)"; + +static const char* PKGRUN_HELP_STRING = R"(Usage: lute pkg run [args...] + +Run Options: + --list List all available scripts in the nearest .lute folder. + -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 runBytecode( + Runtime& runtime, + const std::string& bytecode, + const std::string& chunkname, + int program_argc, + char** program_argv, + LuteReporter& reporter, + std::optional profileOptions = std::nullopt +) { - return setupState( - runtime, - [](lua_State* L) + bool success = runtime.runBytecode( + bytecode, + chunkname, + program_argc, + program_argv, + [&](lua_State* L) { - if (Luau::CodeGen::isSupported()) - Luau::CodeGen::create(L); + Luau::CodeGen::CompilationOptions nativeOptions; + nativeOptions.flags = Luau::CodeGen::CodeGen_OnlyNativeModules; + Luau::CodeGen::compile(L, -1, nativeOptions); - luaopen_require(L, requireConfigInit, createCliRequireContext(L)); + if (profileOptions) + profilerStart(L, profileOptions->frequency); } ); -} - -bool setupArguments(lua_State* L, int argc, char** argv) -{ - if (!lua_checkstack(L, argc)) - return false; - - for (int i = 0; i < argc; ++i) - lua_pushstring(L, argv[i]); - - return true; -} - -static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL) -{ - // module needs to run in a new thread, isolated from the rest - lua_State* L = lua_newthread(GL); - - // new thread needs to have the globals sandboxed - luaL_sandboxthread(L); - - 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); - else - fprintf(stderr, "Failed to load bytecode"); - - lua_pop(GL, 1); - return false; - } - - if (getCodegenEnabled()) - { - Luau::CodeGen::CompilationOptions nativeOptions; - Luau::CodeGen::compile(L, -1, nativeOptions); - } - if (!setupArguments(L, program_argc, program_argv)) + if (profileOptions) { - fprintf(stderr, "Failed to pass arguments to Luau"); - lua_pop(GL, 1); - return false; + profilerStop(); + profilerDump(profileOptions->filename.c_str(), reporter); } - runtime.GL = GL; - runtime.runningThreads.push_back({true, getRefForThread(L), program_argc}); - - lua_pop(GL, 1); - - return runtime.runToCompletion(); + return success; } -static bool runFile(Runtime& runtime, const char* name, lua_State* GL) +static bool runFile( + Runtime& runtime, + const char* name, + int program_argc, + char** program_argv, + LuteReporter& reporter, + std::optional profileOptions = std::nullopt +) { 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,97 +179,141 @@ static bool runFile(Runtime& runtime, const char* name, lua_State* GL) std::string bytecode = Luau::compile(*source, copts()); - return runBytecode(runtime, bytecode, chunkname, GL); + return runBytecode(runtime, bytecode, chunkname, program_argc, program_argv, reporter, profileOptions); } -static void displayHelp(const char* argv0) +static int assertionHandler(const char* expr, const char* file, int line, const char* function) { - 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"); + fprintf(stderr, "%s(%d): ASSERTION FAILED: %s\n", file, line, expr); + return 1; } -static void displayRunHelp(const char* argv0) +// 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) { - printf("Usage: lute run [args...]\n"); - printf("\n"); - printf("Run Options:\n"); - printf(" -h, --help Display this usage message.\n"); -} + std::string normalized = normalizePath(std::move(filePath)); -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"); -} + std::string rootOfPath; + std::string restOfPath = normalized; + if (size_t firstSlash = normalized.find_first_of("\\/"); firstSlash != std::string::npos) + { + rootOfPath = normalized.substr(0, firstSlash); + restOfPath = normalized.substr(firstSlash + 1); + } -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"); -} + std::optional mp = ModulePath::create(std::move(rootOfPath), std::move(restOfPath), isFile, isDirectory); + if (!mp) + return {false, "Could not initialize ModulePath instance."}; -static int assertionHandler(const char* expr, const char* file, int line, const char* function) + ResolvedRealPath resolved = mp->getRealPath(); + + std::pair result; + switch (resolved.status) + { + 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; + } + + 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) { - printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr); - return 1; + 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) + { + const std::string commandPath = joinPaths(*currentPath, fallbackPath); + if (isFile(commandPath)) + return {true, commandPath}; + } + } + + return {false, res}; } -static bool checkValidPath(std::filesystem::path& filePath) +static std::optional getNearestLuteFolderPath() { - if (std::filesystem::exists(filePath)) + for (std::optional currentPath = getCurrentWorkingDirectory(); currentPath; currentPath = getParentPath(*currentPath)) { - return true; - } + std::string lutePath = joinPaths(*currentPath, ".lute"); - // if the file has an explicit extension, dont do a fallback - if (filePath.has_extension()) { - return false; + if (isDirectory(lutePath)) + return lutePath; } - std::filesystem::path fallbackPath = ".lute" / filePath; + return std::nullopt; +} + +static std::vector getLuteScripts(const std::string& luteFolderPath) +{ + std::set names; + std::string normalizedPrefix = normalizePath(luteFolderPath); + if (!normalizedPrefix.empty() && normalizedPrefix.back() != '/') + normalizedPrefix += '/'; - for (const auto& ext : {".luau", ".lua"}) + traverseDirectory(luteFolderPath, [&](const std::string& rawFilePath) { - fallbackPath.replace_extension(ext); + std::string filePath = normalizePath(rawFilePath); + std::string rel = filePath.substr(normalizedPrefix.size()); + size_t slashPos = rel.find('/'); - if (std::filesystem::exists(fallbackPath)) + // If there's a slash at the end, we'll check if the rest is init.luau or init.lua, and add it as a command if so. + if (slashPos != std::string::npos) { - filePath = std::move(fallbackPath); - return true; + std::string rest = rel.substr(slashPos + 1); + if (rest.find('/') == std::string::npos && (rest == "init.luau" || rest == "init.lua")) + names.insert(rel.substr(0, slashPos)); + + return; } - } - return false; + // Otherwise, we can just look for .luau or .lua files and add them as commands without the extension. + if (rel.size() > 5 && rel.substr(rel.size() - 5) == ".luau") + names.insert(rel.substr(0, rel.size() - 5)); + else if (rel.size() > 4 && rel.substr(rel.size() - 4) == ".lua") + names.insert(rel.substr(0, rel.size() - 4)); + }); + + return {names.begin(), names.end()}; } -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 ? "pkg run" : "run"; + std::string filePath; + int program_argc = 0; + bool enableProfiling = false; + ProfileOptions profileOpts; + bool hasProfileOutputArg = false; + bool hasProfileFreqArg = false; + char** program_argv = nullptr; for (int i = argOffset; i < argc; ++i) { @@ -237,45 +321,135 @@ 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 (strcmp(currentArg, "--list") == 0) + { + std::optional luteFolderPath = getNearestLuteFolderPath(); + std::string output = "Commands\n"; + if (luteFolderPath) + { + for (const std::string& name : getLuteScripts(*luteFolderPath)) + output += '\t' + name + '\n'; + } + reporter.reportOutput(output.c_str()); return 0; } + else if (strcmp(currentArg, "--profile") == 0) + { + enableProfiling = true; + } + else if (strcmp(currentArg, "--profile-output") == 0) + { + if (i + 1 >= argc) + { + reporter.reportError("Error: --profile-output requires a path argument."); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); + return 1; + } + profileOpts.filename = argv[++i]; + hasProfileOutputArg = true; + } + else if (strcmp(currentArg, "--frequency") == 0) + { + if (i + 1 >= argc) + { + reporter.reportError("Error: --frequency requires a value argument."); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); + return 1; + } + profileOpts.frequency = std::stoi(argv[++i]); + hasProfileFreqArg = true; + } 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); + std::optional profileOptions; + if (enableProfiling) + { + if (profileOpts.filename.empty()) + { + auto now = std::chrono::system_clock::now(); + std::time_t nowTime = std::chrono::system_clock::to_time_t(now); + std::tm* localTime = std::localtime(&nowTime); + + char dateTimeStr[20]; + std::strftime(dateTimeStr, sizeof(dateTimeStr), "%Y-%m-%d_%H-%M-%S", localTime); + + std::string baseName = Lute::getFilenameWithoutExtension(filePath); + profileOpts.filename = std::string(dateTimeStr) + "_" + baseName + ".json"; + } - if (!checkValidPath(filePath.value())) + profileOptions.emplace(profileOpts); + } + else if (hasProfileOutputArg || hasProfileFreqArg) { - std::cerr << "Error: File '" << filePath->string() << "' does not exist.\n"; + reporter.reportError("You passed --profile-output or --frequency without passing --profile."); return 1; } - bool success = runFile(runtime, filePath->string().c_str(), L); + Runtime runtime{reporter}; + + if (packageAwareness) + { + 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, validPath)); + } + + 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); + setupPkgRunState(runtime, std::move(directDependencies), std::move(allDependencies)); + } + else + { + setupRunState(runtime); + } + + bool success = runFile(runtime, validPath.c_str(), program_argc, program_argv, reporter, profileOptions); 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 +459,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 +476,236 @@ 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.getLuauConfigFiles()); + + + // 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) +void setupVersionLibrary(lua_State* L) { - Runtime runtime; - lua_State* L = setupCliState(runtime); + lua_checkstack(L, 2); + + lua_createtable(L, 0, 3); + + lua_pushstring(L, LUTE_VERSION); + lua_setfield(L, -2, "version"); + + lua_pushstring(L, LUTE_VERSION_SUFFIX); + lua_setfield(L, -2, "versionSuffix"); + + lua_pushstring(L, LUTE_VERSION_FULL); + lua_setfield(L, -2, "versionFull"); - std::string bytecode = Luau::compile(std::string(result.contents), copts()); - return runBytecode(runtime, bytecode, "@" + result.path, L) ? 0 : 1; + lua_setglobal(L, "version"); } -int cliMain(int argc, char** argv) +int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, LuteReporter& reporter) +{ + Runtime runtime{reporter}; + setupCliCommandState(runtime, setupVersionLibrary); + + return runtime.runSource(std::string(result.contents), copts(), "@" + result.path, program_argc, program_argv) ? 0 : 1; +} + +int cliMain(int argc, char** argv, LuteReporter& reporter) { Luau::assertHandler() = assertionHandler; + setLuauFlags(); - AppendedBytecodeResult embedded = checkForAppendedBytecode(argv[0]); - if (embedded.found) + if (const char* unbuffered = std::getenv("LUTE_UNBUFFERED"); unbuffered && std::string_view(unbuffered) == "1") { - Runtime runtime; - lua_State* GL = setupCliState(runtime); + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + } - program_argc = argc; - program_argv = argv; + std::string err; + std::optional exePath = Process::getExecPath(&err); + if (!exePath) + { + reporter.formatError("Unable to find the `lute` executable path: Process::getExecPath failed with error: %s", err.c_str()); + return 1; + } - bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL); + LuteExecutable exe{*exePath, reporter}; + if (auto payload = exe.extract()) + { + Runtime runtime{reporter}; - return success ? 0 : 1; + 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, argc, argv, reporter); + return success ? 0 : 1; + } } #ifdef _WIN32 @@ -410,38 +715,48 @@ int cliMain(int argc, char** argv) if (argc < 2) { // TODO: REPL? - displayHelp(argv[0]); + reporter.reportOutput(HELP_STRING); return 0; } const char* command = argv[1]; + const char* subcommand = argc >= 3 ? argv[2] : nullptr; int argOffset = 2; if (strcmp(command, "run") == 0) { - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ false, reporter); + } + else if (strcmp(command, "pkg") == 0 && subcommand && strcmp(subcommand, "run") == 0) + { + return handleRunCommand(argc, argv, argOffset + 1, /* 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/luauflags.cpp b/lute/cli/src/luauflags.cpp new file mode 100644 index 000000000..757e07c92 --- /dev/null +++ b/lute/cli/src/luauflags.cpp @@ -0,0 +1,39 @@ +#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(); + + setLuauFlag("LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier", false); + // 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..4dff4135d --- /dev/null +++ b/lute/cli/src/packagerun.cpp @@ -0,0 +1,261 @@ +#include "lute/packagerun.h" + +#include "lute/common.h" +#include "lute/userlandvfs.h" +#include "lute/uvutils.h" + +#include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" + +#include "uv.h" + +#include +#include +#include +#include +#include +#include + +static constexpr std::string_view kStorePathPrefix = "@pkg/"; + +// Expands "@pkg/" to "/.loom/store/". +// Returns nullopt if the home directory cannot be determined. +static std::optional expandStorePath(std::string_view path) +{ + if (path.substr(0, kStorePathPrefix.size()) != kStorePathPrefix) + return std::string(path); + + auto result = uvutils::getStringFromUv(uv_os_homedir); + std::string* homeDir = result.get_if(); + if (!homeDir) + return std::nullopt; + + return *homeDir + "/.loom/store/" + std::string(path.substr(kStorePathPrefix.size())); +} + +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; +} + +// 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 +) +{ + LUTE_ASSERT(isFile(lockfilePath)); + + std::optional contents = readFile(lockfilePath); + if (!contents) + return {}; + + std::optional configOpt = Luau::extractConfig(*contents, {}); + if (!configOpt) + return {}; + + Luau::ConfigTable config = std::move(*configOpt); + + if (!config.contains("packages") || !config.contains("dependencies")) + return {}; + + Luau::ConfigTable* packagesTable = config["packages"].get_if(); + Luau::ConfigTable* depsTable = config["dependencies"].get_if(); + if (!packagesTable || !depsTable) + return {}; + + std::optional lockfileParentDir = getParentPath(lockfilePath); + if (!lockfileParentDir) + return {}; + + // First pass: parse all package entries + std::unordered_map keyToIdentifier; + std::unordered_map keyToInfo; + std::unordered_map> keyToDepAliases; + + for (const auto& [k, v] : *packagesTable) + { + const std::string* packageKey = k.get_if(); + if (!packageKey) + return {}; + + const Luau::ConfigTable* entry = v.get_if(); + if (!entry || !entry->contains("name")) + return {}; + + const std::string* name = (*entry).find("name")->get_if(); + if (!name) + return {}; + + const std::string* rev = entry->contains("rev") ? (*entry).find("rev")->get_if() : nullptr; + + Package::Identifier id; + id.name = toLower(*name); + id.version = rev ? *rev : ""; + keyToIdentifier[*packageKey] = id; + + Package::Info info; + if (entry->contains("installPath")) + { + const std::string* installPath = (*entry).find("installPath")->get_if(); + if (installPath) + { + std::optional expanded = expandStorePath(*installPath); + if (!expanded) + return {}; + if (isAbsolutePath(*expanded)) + info.rootDirectory = normalizePath(*expanded); + else + info.rootDirectory = joinPaths(*lockfileParentDir, *expanded); + } + else + info.rootDirectory = joinPaths(*lockfileParentDir, "Packages/" + *packageKey); + } + else + { + info.rootDirectory = joinPaths(*lockfileParentDir, "Packages/" + *packageKey); + } + + info.entryFile = getEntryPoint(info.rootDirectory); + keyToInfo[*packageKey] = std::move(info); + + if (entry->contains("dependencies")) + { + const Luau::ConfigTable* entryDeps = (*entry).find("dependencies")->get_if(); + if (entryDeps) + { + std::unordered_map aliases; + for (const auto& [dk, dv] : *entryDeps) + { + const std::string* alias = dk.get_if(); + const std::string* depKey = dv.get_if(); + if (alias && depKey) + aliases[*alias] = *depKey; + } + keyToDepAliases[*packageKey] = std::move(aliases); + } + } + } + + // Second pass: resolve dependency aliases to Identifiers. + // The alias name becomes the identifier name so that require("@alias") + // matches correctly in UserlandVfs::toAliasFallback. + for (auto& [key, info] : keyToInfo) + { + auto aliasIt = keyToDepAliases.find(key); + if (aliasIt != keyToDepAliases.end()) + { + for (const auto& [alias, depKey] : aliasIt->second) + { + auto idIt = keyToIdentifier.find(depKey); + if (idIt != keyToIdentifier.end()) + { + Package::Identifier aliasedId; + aliasedId.name = toLower(alias); + aliasedId.version = idIt->second.version; + info.dependencies.push_back(std::move(aliasedId)); + } + } + } + } + + // Build direct dependencies from dependencies table (alias -> key map) + std::vector directDependencies; + for (const auto& [ak, av] : *depsTable) + { + const std::string* depKey = av.get_if(); + if (!depKey) + continue; + auto it = keyToIdentifier.find(*depKey); + if (it != keyToIdentifier.end()) + directDependencies.push_back(it->second); + } + + // Build all dependencies. Each package is registered under its canonical + // name. Additionally, register alias entries so that require("@alias") + // can find the package by the alias name used in the lockfile. + std::vector> allDependencies; + for (auto& [key, id] : keyToIdentifier) + { + auto infoIt = keyToInfo.find(key); + if (infoIt != keyToInfo.end()) + allDependencies.emplace_back(id, infoIt->second); + } + + // Register alias entries: for each alias in every package's dependency + // map, add an allDependencies entry keyed by the alias name. This allows + // UserlandVfs::jumpToDependencySubtree to find the package by alias. + for (const auto& [key, aliases] : keyToDepAliases) + { + for (const auto& [alias, depKey] : aliases) + { + std::string lowerAlias = toLower(alias); + auto idIt = keyToIdentifier.find(depKey); + auto infoIt = keyToInfo.find(depKey); + if (idIt != keyToIdentifier.end() && infoIt != keyToInfo.end() && lowerAlias != idIt->second.name) + { + Package::Identifier aliasId; + aliasId.name = lowerAlias; + aliasId.version = idIt->second.version; + allDependencies.emplace_back(std::move(aliasId), infoIt->second); + } + } + } + + 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..87f087ba1 --- /dev/null +++ b/lute/cli/src/profiler.cpp @@ -0,0 +1,194 @@ +#include "lute/profiler.h" + +#include "lute/reporter.h" + +#include "lua.h" + +#include +#include +#include +#include + + +struct FrameInfo +{ + std::string name; + std::string file; + int line; + + FrameInfo(const lua_Debug& dbg) + : name(dbg.name ? dbg.name : "(anonymous)") + , file(dbg.source ? &dbg.source[1] : "(unknown)") // TODO(Varun)replace with chunkname utils API + , line(dbg.linedefined) + { + } + + bool operator==(const FrameInfo& other) const + { + return name == other.name && file == other.file && line == other.line; + } +}; + +struct TraceEvent +{ + TraceEvent(char phase, const FrameInfo& fi, uint64_t timestamp) + : phase(phase) + , name(fi.name) + , file(fi.file) + , line(fi.line) + , timestamp(timestamp) + { + } + char phase; // 'B' for begin, 'E' for end + std::string name; + std::string file; + int line; + uint64_t timestamp; // Timestamp in microseconds +}; + +struct Profiler +{ + // static state + lua_Callbacks* callbacks = nullptr; + int frequency = ProfileOptions{}.frequency; + std::thread thread; + + // variables for communication between loop and trigger + std::atomic exit = false; + std::atomic pendingTicks = 0; + + // state for tracking stack changes (only accessed in trigger) + std::vector previousStack; + std::vector events; + uint64_t currentTimestamp = 0; + +} gProfiler; + +static void profilerTrigger(lua_State* L, int gc) +{ + uint64_t ticks = gProfiler.pendingTicks.exchange(0); + if (ticks == 0) + { + gProfiler.callbacks->interrupt = nullptr; + return; + } + + std::vector currentStack; + lua_Debug dbg; + for (int level = 0; lua_getinfo(L, level, "sln", &dbg); ++level) + { + currentStack.emplace_back(dbg); + } + + // Stacks are in reverse order, i.e + // leaf ..... main, so we should walk backwards + size_t commonDepth = 0; + size_t iterPrev = gProfiler.previousStack.size(); + size_t iterCurr = currentStack.size(); + + while (iterPrev > 0 && iterCurr > 0 && gProfiler.previousStack[iterPrev - 1] == currentStack[iterCurr - 1]) + { + commonDepth++; + iterPrev--; + iterCurr--; + } + + for (size_t i = 0; i < gProfiler.previousStack.size() - commonDepth; i++) + { + const FrameInfo& frame = gProfiler.previousStack.at(i); + gProfiler.events.emplace_back('E', frame, gProfiler.currentTimestamp); + } + + for (size_t i = currentStack.size() - commonDepth; i > 0; i--) + { + const FrameInfo& frame = currentStack.at(i - 1); + gProfiler.events.emplace_back('B', frame, gProfiler.currentTimestamp); + } + + gProfiler.currentTimestamp += ticks; + gProfiler.previousStack = std::move(currentStack); + 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)) + { + uint64_t ticks = uint64_t((now - last) * 1e6); + + gProfiler.pendingTicks += ticks; + 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.previousStack.clear(); + gProfiler.events.clear(); + gProfiler.currentTimestamp = 0; + gProfiler.pendingTicks = 0; + + gProfiler.exit = false; + gProfiler.thread = std::thread(profilerLoop); +} + +void profilerStop() +{ + gProfiler.exit = true; + gProfiler.thread.join(); + + // For any of the remaining frames on the stack, insert an artificial end(E) event. + for (size_t i = 0; i < gProfiler.previousStack.size(); i++) + { + const FrameInfo& frame = gProfiler.previousStack.at(i); + gProfiler.events.emplace_back('E', frame, gProfiler.currentTimestamp); + } + gProfiler.previousStack.clear(); +} + +void profilerDump(const char* path, LuteReporter& reporter) +{ + FILE* out = fopen(path, "w"); + if (!out) + { + reporter.formatError("Failed to open profile output file: %s", path); + return; + } + + fprintf(out, "{\"traceEvents\":[\n"); + + for (size_t i = 0; i < gProfiler.events.size(); i++) + { + const TraceEvent& evt = gProfiler.events[i]; + + if (i > 0) + fprintf(out, ",\n"); + + fprintf(out, "{\"ph\":\"%c\",", evt.phase); + fprintf(out, "\"name\":\"%s\",", evt.name.c_str()); + fprintf(out, "\"cat\":\"function\","); + fprintf(out, "\"pid\":1,\"tid\":1,"); + fprintf(out, "\"ts\":%llu", static_cast(evt.timestamp)); + fprintf(out, ",\"args\":{\"file\":\"%s\",\"line\":%d}}", evt.file.c_str(), evt.line); + } + + fprintf(out, "\n]}\n"); + fclose(out); + + reporter.reportOutput("Profile written to " + std::string(path) + " (" + std::to_string(gProfiler.events.size()) + " events)"); +} diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp new file mode 100644 index 000000000..6d59c2f9c --- /dev/null +++ b/lute/cli/src/requiresetup.cpp @@ -0,0 +1,222 @@ +#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* createRunRequireContext(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()}; + + // 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* createCliCommandRequireContext(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* createPkgRunRequireContext( + 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 luauConfigFiles, + 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(luauConfigFiles), 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* setupRunState(Runtime& runtime, std::function preSandboxInit) +{ + runtime.requireContextFactory = createRunRequireContext; + + return setupState( + runtime, + [preSandboxInit = std::move(preSandboxInit)](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createRunRequireContext(L)); + if (preSandboxInit) + preSandboxInit(L); + } + ); +} + +lua_State* setupCliCommandState(Runtime& runtime, std::function preSandboxInit) +{ + runtime.requireContextFactory = createCliCommandRequireContext; + + return setupState( + runtime, + [preSandboxInit = std::move(preSandboxInit)](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createCliCommandRequireContext(L)); + if (preSandboxInit) + preSandboxInit(L); + } + ); +} + +struct PkgData +{ + std::vector directDependencies; + std::vector> allDependencies; +}; + +lua_State* setupPkgRunState( + Runtime& runtime, + std::vector directDependencies, + std::vector> allDependencies +) +{ + std::shared_ptr pkgData = std::make_shared(PkgData{std::move(directDependencies), std::move(allDependencies)}); + runtime.requireContextFactory = [pkgData = std::move(pkgData)](lua_State* L) -> void* + { + return createPkgRunRequireContext(L, pkgData->directDependencies, pkgData->allDependencies); + }; + + return setupState( + runtime, + [&factory = runtime.requireContextFactory](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, factory(L)); + } + ); +} + +struct BundleData +{ + Luau::DenseHashMap luauConfigFiles; + Luau::DenseHashMap bundleMap; +}; + +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luauConfigFiles, + Luau::DenseHashMap bundleMap +) +{ + std::shared_ptr bundleData = std::make_shared(BundleData{std::move(luauConfigFiles), std::move(bundleMap)}); + runtime.requireContextFactory = [bundleData = std::move(bundleData)](lua_State* L) -> void* + { + return createBundleRequireContext(L, bundleData->luauConfigFiles, bundleData->bundleMap); + }; + + return setupState( + runtime, + [&factory = runtime.requireContextFactory](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, factory(L)); + } + ); +} diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp new file mode 100644 index 000000000..310a51f32 --- /dev/null +++ b/lute/cli/src/staticrequires.cpp @@ -0,0 +1,347 @@ +#include "lute/staticrequires.h" + +#include "lute/modulepath.h" +#include "lute/resolvemodule.h" +#include "lute/staticrequires.h" + +#include "Luau/Ast.h" +#include "Luau/Config.h" +#include "Luau/FileUtils.h" +#include "Luau/LuauConfig.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(); + luauConfigFiles.clear(); + + if (!isAbsolutePath(entryPoint)) + { + fprintf(stderr, "Error: %s isn't an absolute path\n", entryPoint.c_str()); + return; + } + + // Temporary set to collect absolute paths to .luaurc files + Luau::DenseHashSet configAbsolutePaths{""}; + + 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 + while (!dir.empty()) + { + std::string luaurcPath = dir + "/" + Luau::kConfigName; + std::string luauConfigPath = dir + "/" + Luau::kLuauConfigName; + + bool luaurcExists = isFile(luaurcPath); + bool luauConfigExists = isFile(luauConfigPath); + + if (luaurcExists && luauConfigExists) + { + reporter.formatError( + "Warning: Both %s and %s exist in '%s', preferring %s\n", + Luau::kConfigName, + Luau::kLuauConfigName, + dir.c_str(), + Luau::kLuauConfigName + ); + configAbsolutePaths.insert(luauConfigPath); + } + else if (luauConfigExists) + { + configAbsolutePaths.insert(luauConfigPath); + } + else if (luaurcExists) + { + configAbsolutePaths.insert(luaurcPath); + } + + if (luaurcExists || luauConfigExists) + 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 = ::resolveModule(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); + } + + // Include .luaurc files in the lowest common root calculation + std::vector allPaths = discovered; + for (const auto& luaurcPath : configAbsolutePaths) + { + allPaths.push_back(luaurcPath); + } + + lowestCommonRoot = findLowestCommonRoot(allPaths); + + // 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 : configAbsolutePaths) + { + // Get the directory and filename of the config file + std::string absoluteDir = absolutePath; + std::string configFileName = absoluteDir; + size_t lastSlash = absoluteDir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + configFileName = absoluteDir.substr(lastSlash + 1); + absoluteDir = absoluteDir.substr(0, lastSlash); + } + + // Convert to relative path and append the config filename + std::string relativeConfig = configFileName; + if (commonRootLen > 0 && absoluteDir.length() > commonRootLen) + { + std::string relativeDir = absoluteDir.substr(commonRootLen); + relativeConfig = relativeDir + "/" + configFileName; + } + + // Read the .luaurc file content + std::optional content = readFile(absolutePath); + if (content) + { + // Store using the config file path (e.g., "dir/.luaurc", ".luaurc", or ".config.luau") + luauConfigFiles[relativeConfig] = *content; + } + else + { + reporter.formatError("Warning: Could not read config 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 (!luauConfigFiles.empty()) + { + reporter.reportOutput("\n.luaurc files found:"); + for (const auto& [configDir, content] : luauConfigFiles) + { + 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..b489d873d 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -1,180 +1,67 @@ #include "lute/tc.h" +#include "lute/configresolver.h" +#include "lute/tcmoduleresolver.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 - -)LUTE_TYPES"; - -struct LuteFileResolver : Luau::FileResolver -{ - std::optional readSource(const Luau::ModuleName& name) override - { - Luau::SourceCode::Type sourceType; - std::optional source = std::nullopt; - - // If the module name is "-", then read source from stdin - if (name == "-") - { - source = readStdin(); - sourceType = Luau::SourceCode::Script; - } - else - { - source = readFile(name); - sourceType = Luau::SourceCode::Module; - } - - if (!source) - return std::nullopt; - - 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; - } -}; +#include "Luau/FileUtils.h" +#include "Luau/Frontend.h" -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 +107,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; } @@ -236,16 +123,12 @@ int typecheck(const std::vector& sourceFilesInput) Luau::FrontendOptions frontendOptions; frontendOptions.retainFullTypeGraphs = annotate; - frontendOptions.runLintChecks = true; - LuteFileResolver fileResolver; - LuteConfigResolver configResolver(mode); - Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions); + Luau::LuteTypeCheckModuleResolver fileResolver{reporter}; + Luau::LuteConfigResolver configResolver(mode); + Luau::Frontend frontend(Luau::SolverMode::New, &fileResolver, &configResolver, frontendOptions); 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 +151,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 +160,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/common/CMakeLists.txt b/lute/common/CMakeLists.txt new file mode 100644 index 000000000..ed351559d --- /dev/null +++ b/lute/common/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(Lute.Common STATIC) + +target_sources(Lute.Common PRIVATE + include/lute/common.h + include/lute/fileutils.h + include/lute/reporter.h + + src/fileutils.cpp +) + +target_compile_features(Lute.Common PUBLIC cxx_std_17) +target_include_directories(Lute.Common PUBLIC include) +target_link_libraries(Lute.Common PRIVATE Luau.CLI.lib Luau.Common) +target_compile_options(Lute.Common PRIVATE ${LUTE_OPTIONS}) + +if(LUTE_EXTERN_C) + target_compile_definitions(Lute.Common PUBLIC "LUTE_API=extern \"C\"") +endif() diff --git a/lute/common/include/lute/common.h b/lute/common/include/lute/common.h new file mode 100644 index 000000000..f5354ddf3 --- /dev/null +++ b/lute/common/include/lute/common.h @@ -0,0 +1,6 @@ +#pragma once + +#include "Luau/Common.h" + +#define LUTE_ASSERT(expr) LUAU_ASSERT(expr) +#define LUTE_UNREACHABLE() LUAU_UNREACHABLE() diff --git a/lute/common/include/lute/fileutils.h b/lute/common/include/lute/fileutils.h new file mode 100644 index 000000000..6edc9aa2d --- /dev/null +++ b/lute/common/include/lute/fileutils.h @@ -0,0 +1,32 @@ +// This file is part of the Lute programming language and is licensed under MIT License +#pragma once + +#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); + +bool isDirectory(const std::string& path); + +// Get the name of a file without its extension +std::string getFilenameWithoutExtension(const std::string& path); + +} // namespace Lute diff --git a/lute/common/include/lute/library.h b/lute/common/include/lute/library.h new file mode 100644 index 000000000..41b0c197c --- /dev/null +++ b/lute/common/include/lute/library.h @@ -0,0 +1,56 @@ +#pragma once + +#include "lua.h" +#include "lualib.h" + +#include + +#ifndef LUTE_API +#define LUTE_API +#endif + +// A uniform interface for Lute libraries. +// +// Each `Derived` Lute library must provide: +// static constexpr const char kName[]; -- the Luau library name (e.g. "task") +// static int pushLibrary(lua_State* L); -- push the library table onto the stack +// static const luaL_Reg lib[]; -- function registration table +// static const char* const properties[]; -- non-function table keys (empty array if none) +template +struct LuteLibrary +{ + // Verifies that the type given as `Derived` satisfies the interface contract. + // + // This must be implemented as a function (not in the class scope) because `Derived` is + // incomplete when `LuteLibrary` is instantiated (CRTP constraint). + static void verifyInterface() + { + static_assert( + std::is_array_v && + std::is_same_v, const char>, + "LuteLibrary must declare: static constexpr const char kName[]" + ); + static_assert( + std::is_same_v, + "LuteLibrary must declare: static int pushLibrary(lua_State* L)" + ); + static_assert( + std::is_array_v && + std::is_same_v, const luaL_Reg>, + "LuteLibrary must declare: static const luaL_Reg lib[]" + ); + static_assert( + std::is_array_v && + std::is_same_v, const char* const>, + "LuteLibrary must declare: static const char* const properties[]" + ); + } + + static int openAsGlobal(lua_State* L) + { + verifyInterface(); + Derived::pushLibrary(L); + lua_setglobal(L, Derived::kName); + return 1; + } +}; diff --git a/lute/common/include/lute/reporter.h b/lute/common/include/lute/reporter.h new file mode 100644 index 000000000..a6ba8ce78 --- /dev/null +++ b/lute/common/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/common/src/fileutils.cpp b/lute/common/src/fileutils.cpp new file mode 100644 index 000000000..1ca5b4422 --- /dev/null +++ b/lute/common/src/fileutils.cpp @@ -0,0 +1,181 @@ +// 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 +} + +bool isDirectory(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + + DWORD attrs = GetFileAttributesW(wpath.c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES) + return false; + + return (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; +#else + struct stat pathStat; + if (stat(path.c_str(), &pathStat) != 0) + return false; + return S_ISDIR(pathStat.st_mode); +#endif +} + +std::string getFilenameWithoutExtension(const std::string& path) +{ + std::string base = path; + size_t lastSlash = base.find_last_of("/\\"); + if (lastSlash != std::string::npos) + base = base.substr(lastSlash + 1); + // Once we've stripped off the last '/' in the path, we need to trim the last '.' to get the filename. + // If a filename ends in .foo.bar, only the .bar will be stripped. + size_t lastDot = base.rfind('.'); + if (lastDot != std::string::npos) + base = base.substr(0, lastDot); + return base; +} + +} // namespace Lute diff --git a/lute/crypto/include/lute/crypto.h b/lute/crypto/include/lute/crypto.h index 5c4e64b5e..2a4a4c5f7 100644 --- a/lute/crypto/include/lute/crypto.h +++ b/lute/crypto/include/lute/crypto.h @@ -1,38 +1,19 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" -#include - // open the library as a standard global luau library -int luaopen_crypto(lua_State* L); +LUTE_API int luaopen_crypto(lua_State* L); // open the library as a table on top of the stack -int luteopen_crypto(lua_State* L); +LUTE_API int luteopen_crypto(lua_State* L); -namespace crypto +struct Crypto : LuteLibrary { - -static const char kHashProperty[] = "hash"; -static const char kPasswordProperty[] = "password"; - -static const char kDigestName[] = "digest"; -int lua_digest(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 constexpr const char kName[] = "crypto"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -static const std::string properties[] = { - kHashProperty, - kPasswordProperty, -}; - -} // namespace crypto diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index fc474e7c0..e4eb09b72 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,157 +15,268 @@ namespace crypto { - struct HashFunction - { - std::string name; - const env_md_st* md; - }; +static const char kHashProperty[] = "hash"; +static const char kSecretboxProperty[] = "secretbox"; +static const char kPasswordProperty[] = "password"; - static const int kHashFunctionTag = 81; +static const char kDigestName[] = "digest"; - static const HashFunction hashFunctions[] = { - {"md5", EVP_md5()}, - {"sha1", EVP_sha1()}, - {"sha256", EVP_sha256()}, - {"sha512", EVP_sha512()}, - {"blake2b256", EVP_blake2b256()}, - }; +static const char kCiphertextField[] = "ciphertext"; +static const char kKeyField[] = "key"; +static const char kNonceField[] = "nonce"; - int makeHashFunctionMap(lua_State* L) - { - lua_createtable(L, 0, std::size(hashFunctions)); +static const char kKeygenName[] = "keygen"; +static const char kSealName[] = "seal"; +static const char kOpenName[] = "open"; +static const char kPasswordHashName[] = "hash"; +static const char kVerifyPasswordHashName[] = "verify"; - for (auto& [name, md] : hashFunctions) - { - lua_pushlightuserdatatagged(L, (void*) md, kHashFunctionTag); - lua_setfield(L, -2, name.c_str()); - } +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)); - return 1; + for (auto& [name, md] : hashFunctions) + { + lua_pushlightuserdatatagged(L, (void*) md, kHashFunctionTag); + lua_setfield(L, -2, name.c_str()); } - const env_md_st* getHashFunction(lua_State* L, int idx) + return 1; +} + +const env_md_st* checkHashFunction(lua_State* L, int idx) +{ + if (auto typ = static_cast(lua_tolightuserdatatagged(L, idx, kHashFunctionTag))) + return typ; + + luaL_typeerrorL(L, idx, "hash function"); +} + +struct BinaryData +{ + const void* data; + size_t length; +}; + +BinaryData checkBinaryData(lua_State* L, int idx) +{ + if (!lua_isstring(L, idx) && !lua_isbuffer(L, idx)) + luaL_typeerrorL(L, idx, "string or buffer"); + + if (lua_isstring(L, idx)) { - if (auto typ = static_cast(lua_tolightuserdatatagged(L, idx, kHashFunctionTag))) - return typ; + size_t length = 0; + const char* data = lua_tolstring(L, idx, &length); - luaL_typeerrorL(L, idx, "hash function"); + return BinaryData{data, length}; } - struct BinaryData - { - const void* data; - size_t length; - }; - BinaryData extractData(lua_State* L, int idx) + if (lua_isbuffer(L, idx)) { - if (!lua_isstring(L, idx) && !lua_isbuffer(L, idx)) - luaL_typeerrorL(L, idx, "string or buffer"); + size_t length = 0; + void* data = lua_tobuffer(L, idx, &length); - if (lua_isstring(L, idx)) - { - size_t length = 0; - const char* data = lua_tolstring(L, idx, &length); + return BinaryData{data, length}; + } - return BinaryData{data, length}; - } + 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) +{ + const env_md_st* hashFunction = checkHashFunction(L, 1); + BinaryData message = checkBinaryData(L, 2); - if (lua_isbuffer(L, idx)) - { - size_t length = 0; - void* data = lua_tobuffer(L, idx, &length); + 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 BinaryData{data, length}; - } + return 1; +} - luaL_error(L, "failed to extract binary data from stack: %d", idx); - } +// keygen(): buffer +int lua_secretbox_keygen(lua_State* L) +{ + uint8_t* key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); - 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); + return 1; +} - const env_md_st* hashFunction = getHashFunction(L, 1); - BinaryData message = extractData(L, 2); +// seal(message: string | buffer, key: buffer?): { ciphertext: buffer, key: buffer, nonce: buffer } +int lua_secretbox_seal(lua_State* L) +{ + bool hasKey = !lua_isnoneornil(L, 2); + BinaryData message = checkBinaryData(L, 1); - 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); + lua_createtable(L, 0, 3); - return 1; - } + // ciphertext + uint8_t* buffer = static_cast(lua_newbuffer(L, crypto_secretbox_macbytes() + message.length)); + lua_setfield(L, -2, kCiphertextField); - // hash(password: string): buffer - int lua_pwhash(lua_State* L) + uint8_t* key; + // user provided a key + if (hasKey) { - int argumentCount = lua_gettop(L); - if (argumentCount != 1) - luaL_error(L, "%s: expected 1 arguments, but got %d", kPasswordHashName, argumentCount); + 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())); - size_t length = 0; - const char* password = luaL_checklstring(L, 1, &length); + lua_pushvalue(L, 2); + lua_setfield(L, -2, kKeyField); + } + else + { + key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); + lua_setfield(L, -2, kKeyField); + } - 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); - } + // nonce + uint8_t* nonce = static_cast(lua_newbuffer(L, crypto_secretbox_noncebytes())); + randombytes_buf(nonce, crypto_secretbox_noncebytes()); + lua_setfield(L, -2, kNonceField); - return 1; - } + // compute the ciphertext based on the message, nonce, and key + crypto_secretbox_easy(buffer, static_cast(message.data), message.length, nonce, key); - // 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); + // freeze the result + lua_setreadonly(L, -1, true); + + return 1; +} - size_t hashLength = crypto_pwhash_STRBYTES; - void* hashedPassword = luaL_checkbuffer(L, 1, &hashLength); +// open(secretbox: { ciphertext: buffer, key: buffer, nonce: buffer }): buffer +int lua_secretbox_open(lua_State* L) +{ + // 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"); - size_t length = 0; - const char* password = luaL_checklstring(L, 2, &length); + size_t ciphertextLength = 0; + uint8_t* ciphertext = static_cast(luaL_checkbuffer(L, 2, &ciphertextLength)); - lua_pushboolean(L, crypto_pwhash_str_verify((const char*) hashedPassword, password, length) == 0); + 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())); - return 1; - } + 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())); - int makePasswordHashLibrary(lua_State* L) - { - lua_createtable(L, 0, 2); + 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; +} + +int makeSecretboxLibrary(lua_State* L) +{ + lua_createtable(L, 0, 3); + + // keygen + lua_pushcfunction(L, lua_secretbox_keygen, kKeygenName); + lua_setfield(L, -2, kKeygenName); - // hash - lua_pushcfunction(L, lua_pwhash, kPasswordHashName); - lua_setfield(L, -2, kPasswordHashName); + // seal + lua_pushcfunction(L, lua_secretbox_seal, kSealName); + lua_setfield(L, -2, kSealName); - // verify - lua_pushcfunction(L, lua_pwhash_verify, kVerifyPasswordHashName); - lua_setfield(L, -2, kVerifyPasswordHashName); + // open + lua_pushcfunction(L, lua_secretbox_open, kOpenName); + lua_setfield(L, -2, kOpenName); - return 1; + lua_setreadonly(L, -1, true); + + return 1; +} + +// hash(password: string): buffer +int lua_pwhash(lua_State* L) +{ + size_t length = 0; + const char* password = luaL_checklstring(L, 1, &length); + + 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); } -} // namespace crypto + return 1; +} -int luaopen_crypto(lua_State* L) +// verify(hash: buffer, password: string) +int lua_pwhash_verify(lua_State* L) { - luteopen_crypto(L); - lua_setglobal(L, "crypto"); + size_t hashLength = crypto_pwhash_STRBYTES; + void* hashedPassword = luaL_checkbuffer(L, 1, &hashLength); + + size_t length = 0; + const char* password = luaL_checklstring(L, 2, &length); + + lua_pushboolean(L, crypto_pwhash_str_verify((const char*)hashedPassword, password, length) == 0); return 1; } -int luteopen_crypto(lua_State* L) +int makePasswordHashLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(crypto::lib) + std::size(crypto::properties)); + 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 + +const char* const Crypto::properties[] = { + crypto::kHashProperty, + crypto::kSecretboxProperty, + crypto::kPasswordProperty, +}; - for (auto& [name, func] : crypto::lib) +const luaL_Reg Crypto::lib[] = { + {crypto::kDigestName, crypto::lua_digest}, + {nullptr, nullptr}, +}; + +int Crypto::pushLibrary(lua_State* L) +{ + lua_createtable(L, 0, std::size(Crypto::lib) + std::size(Crypto::properties)); + + for (auto& [name, func] : Crypto::lib) { if (!name || !func) break; @@ -173,6 +288,9 @@ 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); @@ -180,3 +298,13 @@ int luteopen_crypto(lua_State* L) return 1; } + +LUTE_API int luaopen_crypto(lua_State* L) +{ + return Crypto::openAsGlobal(L); +} + +LUTE_API int luteopen_crypto(lua_State* L) +{ + return Crypto::pushLibrary(L); +} diff --git a/lute/debug/CMakeLists.txt b/lute/debug/CMakeLists.txt new file mode 100644 index 000000000..12217a468 --- /dev/null +++ b/lute/debug/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(Lute.Debug STATIC) + +target_sources(Lute.Debug PRIVATE + src/debugger.h + src/debugger.cpp +) + +target_compile_features(Lute.Debug PUBLIC cxx_std_17) +target_include_directories(Lute.Debug + PUBLIC src +) +target_link_libraries(Lute.Debug + PUBLIC Lute.Runtime + PRIVATE Luau.Compiler Luau.VM Luau.Common +) +target_compile_options(Lute.Debug PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/debug/src/debugger.cpp b/lute/debug/src/debugger.cpp new file mode 100644 index 000000000..d06fb568d --- /dev/null +++ b/lute/debug/src/debugger.cpp @@ -0,0 +1,148 @@ +#include "debugger.h" + +#include "Luau/Compiler.h" +#include "Luau/DenseHash.h" +#include "Luau/StringUtils.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include +#include +#include + +namespace debug +{ +Breakpoint::Breakpoint(int id, std::string sourcePath, int line, BreakpointStatus status) + : id(id) + , sourcePath(sourcePath) + , line(line) + , status(status) +{ +} + +Target::Target(Runtime& runtime, std::string sourcePath) + : runtime(runtime) + , sourcePath(sourcePath) + , loadedSources("") +{ +} + +Breakpoint Target::addBreakpoint(std::string sourcePath, int line) +{ + int id = currentBreakpointId; + currentBreakpointId++; + auto [it, _] = breakpoints.insert_or_assign(id, Breakpoint(id, sourcePath, line, BreakpointStatus::Pending)); + installBreakpoint(runtime.GL, it->second); + return it->second; +} + +bool Target::removeBreakpoint(int bpId) +{ + auto it = breakpoints.find(bpId); + if (it == breakpoints.end()) + return false; + Breakpoint& bp = it->second; + if (bp.status == BreakpointStatus::Installed) + { + auto chunkRef = loadedSources.find(bp.sourcePath); + if (chunkRef) + { + (*chunkRef)->push(runtime.GL); + int removed_line = lua_breakpoint(runtime.GL, -1, bp.line, 0); + lua_pop(runtime.GL, 1); + if (removed_line == -1) + runtime.reporter.reportError( + Luau::format("breakpoint %d installed at line %d in %s could not be removed", bp.id, bp.line, bp.sourcePath.c_str()) + ); + } + else + { + runtime.reporter.reportError( + Luau::format("breakpoint %d installed at line %d in %s is missing a loaded source", bp.id, bp.line, bp.sourcePath.c_str()) + ); + } + } + breakpoints.erase(it); + return true; +} + +std::vector Target::getBreakpoints() const +{ + std::vector all; + all.reserve(breakpoints.size()); + for (auto& [_, bp] : breakpoints) + all.push_back(bp); + return all; +} + +std::vector Target::getBreakpointsByStatus(BreakpointStatus status) const +{ + std::vector statusBps; + statusBps.reserve(breakpoints.size()); + for (auto& [_, bp] : breakpoints) + if (bp.status == status) + statusBps.push_back(bp); + return statusBps; +} + +std::optional Target::getBreakpointById(int breakpointId) const +{ + auto it = breakpoints.find(breakpointId); + if (it != breakpoints.end()) + return it->second; + return std::nullopt; +} + +bool Target::installBreakpoint(lua_State* L, Breakpoint& bp) +{ + auto chunkRef = loadedSources.find(bp.sourcePath); + if (!chunkRef) + return false; + (*chunkRef)->push(L); + int installedLine = lua_breakpoint(L, -1, bp.line, 1); + lua_pop(L, 1); + if (installedLine == -1) + { + bp.status = BreakpointStatus::Invalid; + bp.line = -1; + return false; + } + bp.status = BreakpointStatus::Installed; + bp.line = installedLine; + return true; +} + +void Target::installPendingBreakpoints(lua_State* L) +{ + for (auto& [_, bp] : breakpoints) + { + if (bp.status == BreakpointStatus::Pending) + installBreakpoint(L, bp); + } +} + +bool Target::launch(const std::vector& args) +{ + std::ifstream file(sourcePath); + if (!file.is_open()) + return false; + std::string source((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + Luau::CompileOptions debugOptions = {}; + debugOptions.optimizationLevel = 1; + debugOptions.debugLevel = 2; + std::string bytecode = Luau::compile(source, debugOptions); + lua_State* thread = lua_newthread(runtime.GL); + luaL_sandboxthread(thread); + luau_load(thread, sourcePath.c_str(), bytecode.c_str(), bytecode.size(), 0); + loadedSources[sourcePath] = std::make_shared(thread, -1); + installPendingBreakpoints(thread); + for (const std::string& arg : args) + lua_pushstring(thread, arg.c_str()); + runtime.runningThreads.push_back({true, getRefForThread(thread), static_cast(args.size())}); + lua_pop(runtime.GL, 1); + return true; +} +} // namespace debug diff --git a/lute/debug/src/debugger.h b/lute/debug/src/debugger.h new file mode 100644 index 000000000..44bd84205 --- /dev/null +++ b/lute/debug/src/debugger.h @@ -0,0 +1,64 @@ +#pragma once + +#include "lute/runtime.h" + +#include "Luau/DenseHash.h" + +#include +#include +#include +#include + +struct lua_State; + +namespace debug +{ +enum class BreakpointStatus +{ + Pending, + Installed, + Invalid, +}; + +struct Breakpoint +{ + int id; + std::string sourcePath; + int line; + BreakpointStatus status; + explicit Breakpoint(int id, std::string sourcePath, int line, BreakpointStatus status); +}; + +struct Target +{ + explicit Target(Runtime& runtime, std::string sourcePath); + + // Setting breakpoints is a two step process. We add them to our Target. If they + // involve a source that has already been loaded by the VM, we attempt to install that + // breakpoint. Otherwise, it exists as a pending breakpoint until new sources are loaded. + // We do this because clients may 1) configure breakpoints before launching executables + // 2) we load sources dynamically with @require that a client may want to debug. + // TODO: implement 2 and add some callback when breakpoints get installed + Breakpoint addBreakpoint(std::string sourcePath, int line); + bool removeBreakpoint(int bpId); + + std::vector getBreakpoints() const; + std::vector getBreakpointsByStatus(BreakpointStatus status) const; + std::optional getBreakpointById(int breakpointId) const; + + bool launch(const std::vector& args); + +private: + Runtime& runtime; + std::string sourcePath; + + int currentBreakpointId = 0; + std::unordered_map breakpoints; // breakpoint id -> breakpoint object (this is unordered_map to support erase) + + Luau::DenseHashMap> loadedSources; // source path -> reference to chunk + + bool installBreakpoint(lua_State* L, Breakpoint& bp); + void installPendingBreakpoints(lua_State* L); +}; + +} // namespace debug diff --git a/lute/definitions/CMakeLists.txt b/lute/definitions/CMakeLists.txt new file mode 100644 index 000000000..74f889ba5 --- /dev/null +++ b/lute/definitions/CMakeLists.txt @@ -0,0 +1,19 @@ +add_library(Lute.Lute STATIC) + +if (LUTE_STDLESS) + target_sources(Lute.Lute PRIVATE + include/lute/lutemodules.h + src/lutemodules_stub.cpp + ) +else() + target_sources(Lute.Lute PRIVATE + include/lute/lutemodules.h + src/lutemodules.cpp + src/generated/modules.h + src/generated/modules.cpp + ) +endif() + +target_compile_features(Lute.Lute PUBLIC cxx_std_17) +target_include_directories(Lute.Lute PUBLIC "include") +target_compile_options(Lute.Lute PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/definitions/include/lute/lutemodules.h b/lute/definitions/include/lute/lutemodules.h new file mode 100644 index 000000000..4a02928e7 --- /dev/null +++ b/lute/definitions/include/lute/lutemodules.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +enum class LuteModuleType +{ + Module, + Directory, + NotFound, +}; + +struct LuteModuleResult +{ + LuteModuleType type; + std::string_view contents; +}; + +LuteModuleResult getLuteModule(std::string_view path); diff --git a/lute/definitions/src/lutemodules.cpp b/lute/definitions/src/lutemodules.cpp new file mode 100644 index 000000000..9e77fbf4a --- /dev/null +++ b/lute/definitions/src/lutemodules.cpp @@ -0,0 +1,24 @@ +#include "lute/lutemodules.h" + +// This file provides lutedefinitions and is auto-generated by luthier.luau. +// If it is not present or outdated, re-configure using luthier.luau. +#include "generated/modules.h" + +#include +#include + +LuteModuleResult getLuteModule(std::string_view path) +{ + for (const auto& [pathInLib, contents] : lutedefinitions) + { + if (path != pathInLib) + continue; + + if (contents == "#directory") + return {LuteModuleType::Directory}; + else + return {LuteModuleType::Module, contents}; + } + + return {LuteModuleType::NotFound}; +} diff --git a/lute/definitions/src/lutemodules_stub.cpp b/lute/definitions/src/lutemodules_stub.cpp new file mode 100644 index 000000000..03315a4de --- /dev/null +++ b/lute/definitions/src/lutemodules_stub.cpp @@ -0,0 +1,6 @@ +#include "lute/lutemodules.h" + +LuteModuleResult getLuteModule(std::string_view) +{ + return {LuteModuleType::NotFound}; +} diff --git a/lute/fs/CMakeLists.txt b/lute/fs/CMakeLists.txt index a41db2abd..57f0b5a10 100644 --- a/lute/fs/CMakeLists.txt +++ b/lute/fs/CMakeLists.txt @@ -4,9 +4,11 @@ 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) target_include_directories(Lute.Fs PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Fs PRIVATE Lute.Runtime Lute.Time Luau.VM uv_a) +target_link_libraries(Lute.Fs PRIVATE Lute.Common Lute.Runtime Lute.Time Luau.VM uv_a) target_compile_options(Lute.Fs PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index 33b840c88..c7a533606 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -1,98 +1,19 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" // open the library as a standard global luau library -int luaopen_fs(lua_State* L); +LUTE_API int luaopen_fs(lua_State* L); // open the library as a table on top of the stack -int luteopen_fs(lua_State* L); +LUTE_API int luteopen_fs(lua_State* L); -namespace fs +struct FS : LuteLibrary { - -// TODO: add the ability to open as bytes -/* Takes path: string, a mode: 'r|a|w|x|+' (defaulting to r when omitted) - Returns a table representing a handle to the file the state of a file {fd : number, error : ...} - */ -int open(lua_State* L); - -/* Reads a file into a string. Takes a file handle obtained from openfile */ -int read(lua_State* L); - -/* Writes a string to a file without closing it*/ -int write(lua_State* L); - -/* takes a file handle into a string and then closes it */ -int close(lua_State* L); - -/* reads a whole file into a string and then closes it */ -int readfiletostring(lua_State* L); -/* writes a st */ -int writestringtofile(lua_State* L); - -/* Reads a file without blocking */ -int readasync(lua_State* L); - -/* Removes a file */ -int fs_remove(lua_State* L); - -/* Creates a folder */ -int fs_mkdir(lua_State* L); - -/* Removes a directory */ -int fs_rmdir(lua_State* L); - -/* Gets the metadata of a file */ -int fs_stat(lua_State* L); - -/* Checks if a file exists */ -int fs_exists(lua_State* L); - -/* Copies a file to another path */ -int fs_copy(lua_State* L); - -/* Creates a link to a file */ -int fs_link(lua_State* L); - -/* Creates a symlink to a file */ -int fs_symlink(lua_State* L); - -/* Gets the type of a file entry */ -int type(lua_State* L); - -/* Sets up a filesystem watch event */ -int fs_watch(lua_State* L); - -/* Lists the contents of a directory */ -int listdir(lua_State* L); - -static const luaL_Reg lib[] = { - /* Manual control apis - you are responsible for calling close / open*/ - {"open", open}, - {"read", read}, - {"write", write}, - {"close", close}, - - {"remove", fs_remove}, - - {"stat", fs_stat}, - {"exists", fs_exists}, - {"type", type}, - - {"watch", fs_watch}, - {"link", fs_link}, - {"symlink", fs_symlink}, - {"copy", fs_copy}, - - {"mkdir", fs_mkdir}, - {"listdir", listdir}, - {"rmdir", fs_rmdir}, - - {"readfiletostring", readfiletostring}, - {"writestringtofile", writestringtofile}, - {"readasync", readasync}, - {NULL, NULL}, + static constexpr const char kName[] = "fs"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace fs diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index c257fdbf2..180b0324a 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -1,92 +1,58 @@ #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 +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + #include -#ifdef _WIN32 -#include -#endif -#include -#include -#include #include #include -#include #include -#include - - -#if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) -#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) -#endif -#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) -#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) -#endif - -#if !defined(S_ISCHR) && defined(S_IFMT) && defined(S_IFCHR) -#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) -#endif - -#if !defined(S_ISLNK) && defined(S_IFMT) && defined(S_IFLNK) -#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) -#endif - -#if !defined(S_ISFIFO) && defined(S_IFMT) && defined(S_IFIFO) -#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) -#endif +#include "fs_impl.h" namespace fs { -const char* UV_TYPENAME_UNKNOWN = "unknown"; // UV_DIRENT_UNKNOWN -const char* UV_TYPENAME_FILE = "file"; // UV_DIRENT_FILE -const char* UV_TYPENAME_DIR = "dir"; // UV_DIRENT_DIR -const char* UV_TYPENAME_LINK = "link"; // UV_DIRENT_LINK -const char* UV_TYPENAME_FIFO = "fifo"; // UV_DIRENT_FIFO -const char* UV_TYPENAME_SOCKET = "socket"; // UV_DIRENT_SOCKET -const char* UV_TYPENAME_CHAR = "char"; // UV_DIRENT_CHAR -const char* UV_TYPENAME_BLOCK = "block"; // UV_DIRENT_BLOCK - -const char* UV_DIRENT_TYPES[] = { - UV_TYPENAME_UNKNOWN, - UV_TYPENAME_FILE, - UV_TYPENAME_DIR, - UV_TYPENAME_LINK, - UV_TYPENAME_FIFO, - UV_TYPENAME_SOCKET, - UV_TYPENAME_CHAR, - UV_TYPENAME_BLOCK, -}; +static UVFile* checkFileHandle(lua_State* L, int index) +{ + auto* handle = static_cast(lua_touserdatatagged(L, index, kUVFileTag)); + if (!handle) + { + luaL_errorL(L, "Error: expected file handle"); + } + + return handle; +} -std::optional setFlags(const char* c, int* openFlags) +std::optional setFlags(const char* modeStr, int* openFlags) { int modeFlags = 0x0000; - for (const char* it = c; *it != '\0'; it++) + for (const char* it = modeStr; *it != '\0'; it++) { - char c = *it; - switch (c) + char modeChar = *it; + switch (modeChar) { case 'r': *openFlags |= O_RDONLY; break; case 'w': - *openFlags |= O_WRONLY | O_TRUNC; + *openFlags |= O_WRONLY | O_TRUNC | O_CREAT; + modeFlags = 0666; break; case 'x': - *openFlags |= O_CREAT | O_EXCL; - modeFlags = 0700; + *openFlags |= O_WRONLY | O_CREAT | O_EXCL; + modeFlags = 0666; break; case 'a': - *openFlags |= O_WRONLY | O_APPEND; + *openFlags |= O_WRONLY | O_APPEND | O_CREAT; + modeFlags = 0666; break; case '+': // If we have not set the truncate bit in 'w' mode, @@ -108,434 +74,133 @@ std::optional setFlags(const char* c, int* openFlags) return modeFlags; } -static int createDurationFromTimespec32(lua_State* L, uv_timespec_t timespec) -{ - uv_timespec64_t extended{static_cast(timespec.tv_sec), static_cast(timespec.tv_nsec)}; - return createDurationFromTimespec(L, extended); -} - -static const char* fileModeToType(uint64_t mode) -{ - if (S_ISDIR(mode)) - { - return UV_TYPENAME_DIR; - } - else if (S_ISREG(mode)) - { - return UV_TYPENAME_FILE; - } - else if (S_ISCHR(mode)) - { - return UV_TYPENAME_CHAR; - } - else if (S_ISLNK(mode)) - { - return UV_TYPENAME_LINK; - } -#ifdef S_ISBLK - else if (S_ISBLK(mode)) - { - return UV_TYPENAME_BLOCK; - } -#endif -#ifdef S_ISFIFO - else if (S_ISFIFO(mode)) - { - return UV_TYPENAME_FIFO; - } -#endif -#ifdef S_ISSOCK - else if (S_ISSOCK(mode)) - { - return UV_TYPENAME_SOCKET; - } -#endif - else - { - return UV_TYPENAME_UNKNOWN; - } -} - -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 = checkFileHandle(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 = checkFileHandle(L, 1); + return read_impl(L, handle); } int write(lua_State* L) { - char writeBuffer[4096]; + auto* handle = checkFileHandle(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); - - // 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; - } + const char* data = luaL_checklstring(L, 2, &len); - - 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; - } - if (nArgs < 2) - { - openFlags = O_RDONLY; - } + int openFlags = 0x0000; + const char* mode = "r"; + if (!lua_isnoneornil(L, 2)) + mode = luaL_checkstring(L, 2); - const char* mode = luaL_checkstring(L, 2); - if (std::optional result = openHelper(L, path, mode, &openFlags)) + std::optional modeFlags = setFlags(mode, &openFlags); + if (!modeFlags) { - createFileHandle(L, *result); - return 1; + luaL_errorL(L, "Error decoding mode: %s\n", mode); } - return 0; -} - -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); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - return 0; + const char* path = luaL_checkstring(L, 1); + return remove_impl(L, path); } -int fs_mkdir(lua_State* L) +int mkdir(lua_State* L) { 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, 0777); } -int fs_rmdir(lua_State* L) +int rmdir(lua_State* L) { const char* path = luaL_checkstring(L, 1); - - uv_fs_t rmdir_req; - int err = uv_fs_rmdir(uv_default_loop(), &rmdir_req, path, nullptr); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - return 0; + return rmdir_impl(L, path); } -int fs_stat(lua_State* L) +int stat(lua_State* L) { const char* path = luaL_checkstring(L, 1); - uv_fs_t stat_req; - int err = uv_fs_stat(uv_default_loop(), &stat_req, path, nullptr); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - lua_createtable(L, 0, 6); - - auto stat = stat_req.statbuf; - - auto type = fileModeToType(stat.st_mode); - lua_pushstring(L, type); - lua_setfield(L, -2, "type"); - - // this is fine unless the file is 9 petabytes - lua_pushnumber(L, static_cast(stat.st_size)); - lua_setfield(L, -2, "size"); - - createDurationFromTimespec32(L, stat.st_birthtim); - lua_setfield(L, -2, "created_at"); - - createDurationFromTimespec32(L, stat.st_atim); - lua_setfield(L, -2, "accessed_at"); - - createDurationFromTimespec32(L, stat.st_mtim); - lua_setfield(L, -2, "modified_at"); - - // permissions - lua_createtable(L, 0, 2); - - // libuv writes this correctly cross-platform - bool canAnyWrite = stat.st_mode & 0222; - lua_pushboolean(L, !canAnyWrite); - lua_setfield(L, -2, "readonly"); - - lua_setfield(L, -2, "permissions"); - - return 1; + return stat_impl(L, path); } -static void defaultCallback(uv_fs_t* req) +int copy(lua_State* L) { - auto* request_state = static_cast(req->data); - - if (req->result) - { - request_state->get()->fail(uv_strerror(req->result)); - uv_fs_req_cleanup(req); - delete req; - return; - } - - request_state->get()->complete( - [req](lua_State* L) - { - uv_fs_req_cleanup(req); - - delete req; - - return 0; - } - ); + const char* path = luaL_checkstring(L, 1); + const char* dest = luaL_checkstring(L, 2); + return copy_impl(L, path, dest); } -int fs_copy(lua_State* L) +int link(lua_State* L) { const char* path = luaL_checkstring(L, 1); const char* dest = luaL_checkstring(L, 2); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_copyfile(uv_default_loop(), req, path, dest, 0, defaultCallback); - - if (err) - { - luaL_errorL(L, "%s", uv_strerror(err)); - } - - return lua_yield(L, 0); + return link_impl(L, path, dest); } -int fs_link(lua_State* L) +int symlink(lua_State* L) { const char* path = luaL_checkstring(L, 1); const char* dest = luaL_checkstring(L, 2); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_link(uv_default_loop(), req, path, dest, defaultCallback); - - if (err) - { - luaL_errorL(L, "%s", uv_strerror(err)); - } - - return lua_yield(L, 0); + return symlink_impl(L, path, dest); } -int fs_symlink(lua_State* L) +int rename(lua_State* L) { const char* path = luaL_checkstring(L, 1); const char* dest = luaL_checkstring(L, 2); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - if (std::filesystem::is_directory(path)) - { - req->flags = UV_FS_SYMLINK_DIR; // windows - } - else - { - req->flags = 0; - } - - int err = uv_fs_symlink(uv_default_loop(), req, path, dest, req->flags, defaultCallback); - - if (err) - { - luaL_errorL(L, "%s", uv_strerror(err)); - } - - return lua_yield(L, 0); + return rename_impl(L, path, dest); } struct WatchHandle { - lua_State* L; + WatchHandle(lua_State* L, int callbackIdx) + : runtime(getRuntime(L)) + , callbackReference(std::make_shared(L, callbackIdx)) + , evtHandle(std::make_unique()) + { + evtHandle->data = this; + int err = uv_fs_event_init(runtime->getEventLoop(), evtHandle.get()); + if (err) + luaL_errorL(L, "%s", uv_strerror(err)); + } + + Runtime* runtime; std::shared_ptr callbackReference; bool isClosed = false; - uv_fs_event_t handle; + std::unique_ptr evtHandle; void close() { if (!isClosed) { - int err = uv_fs_event_stop(&handle); - if (err) - { - luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); - } isClosed = true; - - getRuntime(L)->releasePendingToken(); - + uv_fs_event_stop(evtHandle.get()); callbackReference.reset(); + auto raw = evtHandle.release(); + uv_close( + (uv_handle_t*)raw, + [](uv_handle_t* handle) + { + delete (uv_fs_event_t*)handle; + } + ); } } @@ -549,19 +214,12 @@ static int closeWatchHandle(lua_State* L) { luaL_checktype(L, 1, LUA_TUSERDATA); auto* handle = static_cast(lua_touserdatatagged(L, 1, kWatchHandleTag)); - if (!handle) { luaL_errorL(L, "Invalid fs event handle"); 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; @@ -572,71 +230,35 @@ int fs_watch(lua_State* L) const char* path = luaL_checkstring(L, 1); luaL_checktype(L, 2, LUA_TFUNCTION); - auto* event = new (static_cast(lua_newuserdatataggedwithmetatable(L, sizeof(WatchHandle), kWatchHandleTag))) WatchHandle{}; - - event->L = L; - event->callbackReference = std::make_shared(L, 2); - event->handle.data = event; - - int init_err = uv_fs_event_init(uv_default_loop(), &event->handle); - - if (init_err) - { - luaL_errorL(L, "%s", uv_strerror(init_err)); - } + void* storage = lua_newuserdatataggedwithmetatable(L, sizeof(WatchHandle), kWatchHandleTag); + auto* event = new (storage) WatchHandle(L, 2); int event_start_err = uv_fs_event_start( - &event->handle, + event->evtHandle.get(), [](uv_fs_event_t* handle, const char* filenamePtr, int events, int status) { auto* eventHandle = static_cast(handle->data); - lua_State* newThread = lua_newthread(eventHandle->L); - std::shared_ptr ref = getRefForThread(newThread); - Runtime* runtime = getRuntime(newThread); + if (status < 0) + return; std::string filename = filenamePtr ? filenamePtr : ""; - runtime->scheduleLuauResume( - ref, - [=, filename = std::move(filename)](lua_State* L) + eventHandle->runtime->scheduleLuauCallback( + eventHandle->callbackReference, + [filename = std::move(filename), events](lua_State* L) { - // the function to the back of the stack, omit from nret - eventHandle->callbackReference->push(L); + lua_pushlstring(L, filename.c_str(), filename.size()); - // filename - lua_pushstring(L, filename.c_str()); - - // 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; } ); - - uv_stop(handle->loop); }, path, 0 @@ -647,338 +269,54 @@ int fs_watch(lua_State* L) luaL_errorL(L, "%s", uv_strerror(event_start_err)); } - getRuntime(L)->addPendingToken(); - return 1; // return the watch handle } -int fs_exists(lua_State* L) +int exists(lua_State* L) { const char* path = luaL_checkstring(L, 1); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_stat( - uv_default_loop(), - req, - path, - [](uv_fs_t* req) - { - auto* request_state = static_cast(req->data); - - 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); - } - - uv_fs_req_cleanup(req); - - delete req; - - return 1; - } - ); - } - ); - - if (err) - { - luaL_errorL(L, "%s", uv_strerror(err)); - } - - return lua_yield(L, 0); + return exists_impl(L, path); } int type(lua_State* L) { const char* path = luaL_checkstring(L, 1); - uv_fs_t req; - - int err = uv_fs_stat(uv_default_loop(), &req, path, nullptr); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - auto type = fileModeToType(req.statbuf.st_mode); - lua_pushstring(L, type); - uv_fs_req_cleanup(&req); - - return 1; + return type_impl(L, path); } int listdir(lua_State* L) { const char* path = luaL_checkstring(L, 1); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_scandir( - uv_default_loop(), - req, - path, - 0, - [](uv_fs_t* req) - { - auto* request_state = static_cast(req->data); - - request_state->get()->complete( - [req](lua_State* L) - { - lua_createtable(L, 1, 0); - - uv_dirent_t dir; - int i = 0; - int err = 0; - while ((err = uv_fs_scandir_next(req, &dir)) >= 0) - { - lua_pushinteger(L, ++i); - - lua_createtable(L, 0, 2); - - lua_pushstring(L, dir.name); - lua_setfield(L, -2, "name"); - - lua_pushstring(L, UV_DIRENT_TYPES[dir.type]); - lua_setfield(L, -2, "type"); - - lua_settable(L, -3); - } - - uv_fs_req_cleanup(req); - - delete req; - - if (err != UV_EOF) - luaL_errorL(L, "%s", uv_strerror(err)); - - return 1; - } - ); - } - ); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - return lua_yield(L, 0); + return listdir_impl(L, path); } -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)) - { - } - - 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); -} +} // namespace fs -int readasync(lua_State* L) +static void initializeFS(lua_State* L) { - const char* path = luaL_checkstring(L, 1); + init_duration_lib(L); - uv_fs_t* openReq = createRequest(L); - uv_fs_open( - uv_default_loop(), - openReq, - path, - O_RDONLY, - 0, - [](uv_fs_t* req) + luaL_newmetatable(L, "FileHandle"); + lua_pushstring(L, "FileHandle"); + lua_setfield(L, -2, "__type"); + lua_setuserdatadtor( + L, + kUVFileTag, + [](lua_State*, void* ud) { - ResumeCaptureInformation* info = getResumeInformation(req); - int fd = req->result; - - if (fd < 0) + auto* file = static_cast(ud); + if (file->fd.has_value()) { - 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; + uv_fs_t req; + uv_fs_close(nullptr, &req, *file->fd, nullptr); + uv_fs_req_cleanup(&req); } - - // 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; + std::destroy_at(file); } ); + lua_setuserdatametatable(L, kUVFileTag); - return lua_yield(L, 0); -} - -} // namespace fs - -static void initalizeFS(lua_State* L) -{ luaL_newmetatable(L, "WatchHandle"); lua_pushcfunction( @@ -1008,27 +346,45 @@ 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)); } ); lua_setuserdatametatable(L, kWatchHandleTag); } -int luaopen_fs(lua_State* L) -{ - luaL_register(L, "fs", fs::lib); - initalizeFS(L); - return 1; -} +const char* const FS::properties[] = {nullptr}; + +const luaL_Reg FS::lib[] = { + {"open", fs::open}, + {"read", fs::read}, + {"write", fs::write}, + {"close", fs::close}, + + {"remove", fs::remove}, + + {"stat", fs::stat}, + {"exists", fs::exists}, + {"type", fs::type}, -int luteopen_fs(lua_State* L) + {"watch", fs::fs_watch}, + {"link", fs::link}, + {"symlink", fs::symlink}, + {"copy", fs::copy}, + {"move", fs::rename}, + + {"mkdir", fs::mkdir}, + {"listdir", fs::listdir}, + {"rmdir", fs::rmdir}, + + {nullptr, nullptr}, +}; + +int FS::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(fs::lib)); + lua_createtable(L, 0, std::size(FS::lib)); - for (auto& [name, func] : fs::lib) + for (auto& [name, func] : FS::lib) { if (!name || !func) break; @@ -1039,7 +395,17 @@ int luteopen_fs(lua_State* L) lua_setreadonly(L, -1, 1); - initalizeFS(L); + initializeFS(L); return 1; } + +LUTE_API int luaopen_fs(lua_State* L) +{ + return FS::openAsGlobal(L); +} + +LUTE_API int luteopen_fs(lua_State* L) +{ + return FS::pushLibrary(L); +} diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp new file mode 100644 index 000000000..3c4bcc4e9 --- /dev/null +++ b/lute/fs/src/fs_impl.cpp @@ -0,0 +1,1242 @@ +#include "fs_impl.h" + +#include "lute/time.h" +#include "lute/userdatas.h" +#include "lute/uvrequest.h" + +#include "Luau/VecDeque.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +#ifdef _WIN32 +#include +#else +#include +#endif + +#if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#endif + +#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#endif + +#if !defined(S_ISCHR) && defined(S_IFMT) && defined(S_IFCHR) +#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) +#endif + +#if !defined(S_ISLNK) && defined(S_IFMT) && defined(S_IFLNK) +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) +#endif + +#if !defined(S_ISFIFO) && defined(S_IFMT) && defined(S_IFIFO) +#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) +#endif + +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 +{ + using FSRequest::FSRequest; +}; + +struct FSPathPairRequest : FSRequest +{ + FSPathPairRequest(lua_State* L, const char* src, const char* dest) + : FSRequest(L) + , src(src) + , dest(dest) + { + } + + const std::string src; + const std::string dest; +}; + +// Creates a symlink at `dest` pointing to `target`. +// +// On Win32, stats `target` first to determine whether UV_FS_SYMLINK_DIR is needed. +// Calls onDone(0) on success, onDone(negative_uv_error) on failure. Self-deletes. +struct FSCreateSymlink +{ + FSCreateSymlink(uv_loop_t* loop, std::string target, std::string dest, std::function onDone) + : loop(loop) + , target(std::move(target)) + , dest(std::move(dest)) + , onDone(std::move(onDone)) + { + req.data = this; + } + + void start() + { +#if _WIN32 + uv_fs_stat(loop, &req, target.c_str(), FSCreateSymlink::statCallback); +#else + doSymlink(this, 0); +#endif + } + + uv_loop_t* loop; + uv_fs_t req; + std::string target; + std::string dest; + std::function onDone; + +private: + static void doSymlink(FSCreateSymlink* self, int flags) + { + uv_fs_symlink(self->loop, &self->req, self->target.c_str(), self->dest.c_str(), flags, FSCreateSymlink::symlinkCallback); + } + +#if _WIN32 + static void statCallback(uv_fs_t* req) + { + auto* self = static_cast(req->data); + int flags = (req->result >= 0 && S_ISDIR(req->statbuf.st_mode)) ? UV_FS_SYMLINK_DIR : 0; + uv_fs_req_cleanup(req); + doSymlink(self, flags); + } +#endif + + static void symlinkCallback(uv_fs_t* req) + { + auto* self = static_cast(req->data); + int result = req->result; + uv_fs_req_cleanup(req); + self->onDone(result); + delete self; + } +}; + +// Moves a symlink from src to dest. +// +// readlink(src) -> FSCreateSymlink(linkTarget, dest) -> unlink(src). +// Calls onDone(0) on success, onDone(negative_uv_error) on failure. Self-deletes. +struct FSMoveSymlink +{ + FSMoveSymlink(uv_loop_t* loop, std::string src, std::string dest, std::function onDone) + : loop(loop) + , src(std::move(src)) + , dest(std::move(dest)) + , onDone(std::move(onDone)) + { + req.data = this; + } + + void start() + { + uv_fs_readlink(loop, &req, src.c_str(), FSMoveSymlink::readlinkCallback); + } + + uv_loop_t* loop; + uv_fs_t req; + std::string src; + std::string dest; + std::function onDone; + std::string linkTarget; + +private: + static void readlinkCallback(uv_fs_t* req) + { + auto* self = static_cast(req->data); + int result = req->result; + + if (result < 0) + { + uv_fs_req_cleanup(req); + auto done = std::move(self->onDone); + delete self; + done(result); + return; + } + + self->linkTarget = static_cast(req->ptr); + uv_fs_req_cleanup(req); + + (new FSCreateSymlink( + self->loop, + self->linkTarget, + self->dest, + [self](int err) + { + if (err < 0) + { + auto done = std::move(self->onDone); + delete self; + done(err); + return; + } + uv_fs_unlink(self->loop, &self->req, self->src.c_str(), FSMoveSymlink::unlinkCallback); + } + ))->start(); + } + + static void unlinkCallback(uv_fs_t* req) + { + auto* self = static_cast(req->data); + int result = req->result; + uv_fs_req_cleanup(req); + auto done = std::move(self->onDone); + delete self; + done(result); + } +}; + +// Moves a regular file from src to dest. +// +// copyfile(src, dest) -> unlink(src). +// Calls onDone(0) on success, onDone(negative_uv_error) on failure. Self-deletes. +struct FSMoveSingleFile +{ + FSMoveSingleFile(uv_loop_t* loop, std::string src, std::string dest, std::function onDone) + : loop(loop) + , src(std::move(src)) + , dest(std::move(dest)) + , onDone(std::move(onDone)) + { + req.data = this; + } + + void start() + { + uv_fs_copyfile(loop, &req, src.c_str(), dest.c_str(), 0, FSMoveSingleFile::copyCallback); + } + + uv_loop_t* loop; + uv_fs_t req; + std::string src; + std::string dest; + std::function onDone; + +private: + static void copyCallback(uv_fs_t* req) + { + auto* self = static_cast(req->data); + int result = req->result; + uv_fs_req_cleanup(req); + + if (result < 0) + { + auto done = std::move(self->onDone); + delete self; + done(result); + return; + } + + uv_fs_unlink(self->loop, &self->req, self->src.c_str(), FSMoveSingleFile::unlinkCallback); + } + + static void unlinkCallback(uv_fs_t* req) + { + auto* self = static_cast(req->data); + int result = req->result; + uv_fs_req_cleanup(req); + auto done = std::move(self->onDone); + delete self; + done(result); + } +}; + +struct FSRename : FSPathPairRequest +{ + using FSPathPairRequest::FSPathPairRequest; + + static void renameCallback(uv_fs_t* req); + static void crossDeviceFallback(uv_fs_t* req); +}; + +// Handles a recursive move of a direcotry as a sequence of copy-and-delete. +struct FSCopyDirectory +{ + FSCopyDirectory(ResumeToken token, uv_loop_t* loop, std::string src, std::string dest) + : token(std::move(token)) + , loop(loop) + { + req.data = this; + pendingDirs.push_back({std::move(src), std::move(dest)}); + } + + struct DirPair + { + std::string src; + std::string dest; + + DirPair(std::string src, std::string dest) + : src(std::move(src)) + , dest(std::move(dest)) + { + } + }; + + ResumeToken token; + uv_loop_t* loop; + uv_fs_t req; + + Luau::VecDeque pendingDirs; // source dirs awaiting mkdir+scandir + std::vector scannedDirs; // source dirs in breadth-first order; rmdired in reverse + std::vector pendingFiles; // regular files awaiting copyfile+unlink + std::vector pendingLinks; // symlinks awaiting readlink+symlink+unlink + size_t fileIdx = 0; + size_t linkIdx = 0; + size_t removeDirIdx = 0; + std::vector pendingUnknown; // dirent-unknown entries awaiting per-entry lstat classification + size_t unknownIdx = 0; + + static FSCopyDirectory* fromReq(uv_fs_t* req) + { + return static_cast(req->data); + } + + template + void fail(const char* fmt, Args&&... args) + { + token->fail(uvutils::formatUVError(fmt, std::forward(args)...)); + delete this; + } + + void succeed() + { + token->complete( + [](lua_State* L) + { + return 0; + } + ); + delete this; + } + + // Phase 1: mkdir + scandir the next pending source directory, or advance to file copying. + void start() + { + if (!pendingDirs.empty()) + { + uv_fs_mkdir(loop, &req, pendingDirs.front().dest.c_str(), 0777, FSCopyDirectory::mkdirCallback); + return; + } + + startFileCopy(); + } + + // Phase 1a: lstat each UV_DIRENT_UNKNOWN entry from the current scandir to classify it, then loop back to Phase 1. + void startUnknownClassification() + { + if (unknownIdx < pendingUnknown.size()) + { + uv_fs_lstat(loop, &req, pendingUnknown[unknownIdx].src.c_str(), FSCopyDirectory::unknownLstatCallback); + return; + } + + pendingUnknown.clear(); + unknownIdx = 0; + scannedDirs.emplace_back(pendingDirs.front().src); + pendingDirs.pop_front(); + start(); + } + + // Phase 2: copy and unlink the next pending regular file, or advance to symlink recreation. + void startFileCopy() + { + if (fileIdx < pendingFiles.size()) + { + (new FSMoveSingleFile( + loop, + pendingFiles[fileIdx].src, + pendingFiles[fileIdx].dest, + [this](int err) + { + if (err < 0) + { + fail( + "move: Error moving file %s: %s; source and destination may be in an inconsistent state", + pendingFiles[fileIdx].src.c_str(), + uv_strerror(err) + ); + return; + } + ++fileIdx; + startFileCopy(); + } + ))->start(); + return; + } + + startLinkCopy(); + } + + // Phase 3: recreate symlinks at destination via FSMoveSymlink. + void startLinkCopy() + { + if (linkIdx < pendingLinks.size()) + { + (new FSMoveSymlink( + loop, + pendingLinks[linkIdx].src, + pendingLinks[linkIdx].dest, + [this](int err) + { + if (err < 0) + { + fail( + "move: Error moving symlink %s: %s; source and destination may be in an inconsistent state", + pendingLinks[linkIdx].src.c_str(), + uv_strerror(err) + ); + return; + } + ++linkIdx; + startLinkCopy(); + } + ))->start(); + return; + } + + startDirRemoval(); + } + + // Phase 4: rmdir source dirs in reverse breadth-first order (children before parents). + void startDirRemoval() + { + if (removeDirIdx < scannedDirs.size()) + { + const auto& dir = scannedDirs[scannedDirs.size() - 1 - removeDirIdx]; + uv_fs_rmdir(loop, &req, dir.c_str(), FSCopyDirectory::rmdirCallback); + return; + } + + succeed(); + } + + static void mkdirCallback(uv_fs_t* req) + { + auto* self = fromReq(req); + auto result = req->result; + uv_fs_req_cleanup(req); + + if (result < 0 && result != UV_EEXIST) + { + self->fail("move: Error creating directory %s: %s", self->pendingDirs.front().dest.c_str(), uv_strerror(result)); + return; + } + + uv_fs_scandir(self->loop, &self->req, self->pendingDirs.front().src.c_str(), 0, FSCopyDirectory::scandirCallback); + } + + static void scandirCallback(uv_fs_t* req) + { + auto* self = fromReq(req); + auto result = req->result; + + if (result < 0) + { + std::string srcPath = self->pendingDirs.front().src; + uv_fs_req_cleanup(req); + self->fail( + "move: Error reading directory %s: %s; some destination subdirectories may have been created", srcPath.c_str(), uv_strerror(result) + ); + return; + } + + // Collect all entries before req_cleanup, which frees the scandir data. + std::vector> entries; + uv_dirent_t dirent; + int err; + while ((err = uv_fs_scandir_next(req, &dirent)) >= 0) + entries.emplace_back(dirent.name, dirent.type); + + std::string srcDir = self->pendingDirs.front().src; + std::string destDir = self->pendingDirs.front().dest; + uv_fs_req_cleanup(req); + + if (err != UV_EOF) + { + self->fail( + "move: Error reading directory entry in %s: %s; some destination subdirectories may have been created", + srcDir.c_str(), + uv_strerror(err) + ); + return; + } + + for (auto& [name, type] : entries) + { + std::string childSrc = srcDir + "/" + name; + std::string childDest = destDir + "/" + name; + if (type == UV_DIRENT_DIR) + { + self->pendingDirs.push_back({childSrc, childDest}); + } + else if (type == UV_DIRENT_FILE) + { + self->pendingFiles.emplace_back(childSrc, childDest); + } + else if (type == UV_DIRENT_LINK) + { + self->pendingLinks.emplace_back(childSrc, childDest); + } + else if (type == UV_DIRENT_UNKNOWN) + { + self->pendingUnknown.emplace_back(childSrc, childDest); + } + else + { + self->fail("move: Cannot move %s: unsupported file type", childSrc.c_str()); + return; + } + } + + if (!self->pendingUnknown.empty()) + { + self->startUnknownClassification(); + return; + } + + self->scannedDirs.emplace_back(srcDir); + self->pendingDirs.pop_front(); + + self->start(); + } + + static void unknownLstatCallback(uv_fs_t* req) + { + auto* self = fromReq(req); + auto result = req->result; + + if (result < 0) + { + std::string path = self->pendingUnknown[self->unknownIdx].src; + uv_fs_req_cleanup(req); + self->fail("move: Error reading %s: %s; some destination subdirectories may have been created", path.c_str(), uv_strerror(result)); + return; + } + + auto mode = req->statbuf.st_mode; + const auto& entry = self->pendingUnknown[self->unknownIdx]; + + if (S_ISDIR(mode)) + { + self->pendingDirs.push_back({entry.src, entry.dest}); + } + else if (S_ISREG(mode)) + { + self->pendingFiles.emplace_back(entry.src, entry.dest); + } + else if (S_ISLNK(mode)) + { + self->pendingLinks.emplace_back(entry.src, entry.dest); + } + else + { + std::string path = entry.src; + uv_fs_req_cleanup(req); + self->fail("move: Cannot move %s: unsupported file type; some destination subdirectories may have been created", path.c_str()); + return; + } + + uv_fs_req_cleanup(req); + ++self->unknownIdx; + self->startUnknownClassification(); + } + + static void rmdirCallback(uv_fs_t* req) + { + auto* self = fromReq(req); + auto result = req->result; + uv_fs_req_cleanup(req); + + if (result < 0) + { + self->fail( + "move: Error removing source directory %s: %s; the destination is complete but the source directory tree was not fully removed", + self->scannedDirs[self->scannedDirs.size() - 1 - self->removeDirIdx].c_str(), + uv_strerror(result) + ); + return; + } + + ++self->removeDirIdx; + self->startDirRemoval(); + } +}; + +int open_impl(lua_State* L, const char* path, int flags, int mode) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_open( + req->getLoop(), + &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) + { + void* storage = lua_newuserdatataggedwithmetatable(L, sizeof(UVFile), kUVFileTag); + auto* file = new (storage) UVFile(); + file->fd = result; + 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); + + uvutils::ScopedUVRequest scopedReq{std::move(r)}; + uv_fs_req_cleanup(&scopedReq->req); + uv_fs_read(scopedReq->getLoop(), &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->succeedTrivially(); + + 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_req_cleanup(&scopedReq->req); + uv_fs_write(scopedReq->getLoop(), &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(req->getLoop(), &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(req->getLoop(), &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"); + } + + auto fd = handle->fd.value(); + handle->fd = std::nullopt; + + uvutils::ScopedUVRequest req{L}; + uv_fs_close( + req->getLoop(), + &req->req, + fd, + [](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->succeedTrivially(); + } + ); + + return lua_yield(L, 0); +} + +int remove_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_unlink( + req->getLoop(), + &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->succeedTrivially(); + } + ); + + return lua_yield(L, 0); +} + +static const char* fileModeToType(uint64_t mode) +{ + if (S_ISDIR(mode)) + { + return UV_TYPENAME_DIR; + } + else if (S_ISREG(mode)) + { + return UV_TYPENAME_FILE; + } + else if (S_ISCHR(mode)) + { + return UV_TYPENAME_CHAR; + } + else if (S_ISLNK(mode)) + { + return UV_TYPENAME_LINK; + } +#ifdef S_ISBLK + else if (S_ISBLK(mode)) + { + return UV_TYPENAME_BLOCK; + } +#endif +#ifdef S_ISFIFO + else if (S_ISFIFO(mode)) + { + return UV_TYPENAME_FIFO; + } +#endif +#ifdef S_ISSOCK + else if (S_ISSOCK(mode)) + { + return UV_TYPENAME_SOCKET; + } +#endif + else + { + return UV_TYPENAME_UNKNOWN; + } +} + +static int createDurationFromTimespec(lua_State* L, uv_timespec_t timespec) +{ + uv_timespec64_t extended{static_cast(timespec.tv_sec), static_cast(timespec.tv_nsec)}; + return createDurationFromTimespec(L, extended); +} + +int stat_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_stat( + req->getLoop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("Error getting metadata of file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [stat = req->statbuf](lua_State* L) + { + lua_createtable(L, 0, 6); + + auto type = fileModeToType(stat.st_mode); + lua_pushstring(L, type); + lua_setfield(L, -2, "type"); + + // this is fine unless the file is 9 petabytes + lua_pushnumber(L, static_cast(stat.st_size)); + lua_setfield(L, -2, "size"); + + createDurationFromTimespec(L, stat.st_birthtim); + lua_setfield(L, -2, "created"); + + createDurationFromTimespec(L, stat.st_atim); + lua_setfield(L, -2, "accessed"); + + createDurationFromTimespec(L, stat.st_mtim); + lua_setfield(L, -2, "modified"); + + // permissions + lua_createtable(L, 0, 2); + + // libuv writes this correctly cross-platform + bool canAnyWrite = stat.st_mode & 0222; + lua_pushboolean(L, !canAnyWrite); + lua_setfield(L, -2, "readonly"); + + lua_setfield(L, -2, "permissions"); + + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int exists_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_access( + req->getLoop(), + &req->req, + path, + F_OK, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0 && result != UV_ENOENT) + { + r->fail("exists: Error checking existence of %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [exists = (result == 0)](lua_State* L) + { + lua_pushboolean(L, exists); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int type_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_stat( + req->getLoop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("Error getting type of file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [stat = req->statbuf](lua_State* L) + { + auto type = fileModeToType(stat.st_mode); + lua_pushstring(L, type); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int link_impl(lua_State* L, const char* path, const char* dest) +{ + uvutils::ScopedUVRequest req{L, path, dest}; + uv_fs_link( + req->getLoop(), + &req->req, + path, + dest, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("link: Error creating link from %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(result)); + return; + } + + r->succeedTrivially(); + } + ); + + return lua_yield(L, 0); +} + +int symlink_impl(lua_State* L, const char* path, const char* dest) +{ + auto token = getResumeToken(L); + auto* loop = getRuntimeLoop(L); + std::string src{path}, dst{dest}; + (new FSCreateSymlink( + loop, + src, + dst, + [token, src, dst](int err) + { + if (err < 0) + token->fail(uvutils::formatUVError("symlink: Error creating symlink from %s to %s: %s", src.c_str(), dst.c_str(), uv_strerror(err))); + else + token->complete( + [](lua_State* L) + { + return 0; + } + ); + } + ))->start(); + return lua_yield(L, 0); +} + +int copy_impl(lua_State* L, const char* path, const char* dest) +{ + uvutils::ScopedUVRequest req{L, path, dest}; + uv_fs_copyfile( + req->getLoop(), + &req->req, + path, + dest, + 0, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("copy: Error copying file from %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(result)); + return; + } + + r->succeedTrivially(); + } + ); + + return lua_yield(L, 0); +} + +void FSRename::renameCallback(uv_fs_t* req) +{ + auto r = uvutils::retake(req); + auto result = req->result; + + if (result == 0) + { + r->succeedTrivially(); + return; + } + + if (result != UV_EXDEV) + { + r->fail("move: Error moving %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(result)); + return; + } + + // Cross-filesystem: stat the source first to determine whether it's a file or a directory, + // since the fallback copy strategy differs between the two. + uv_fs_req_cleanup(&r->req); + uvutils::ScopedUVRequest statReq{std::move(r)}; + uv_fs_lstat(statReq->getLoop(), &statReq->req, statReq->src.c_str(), FSRename::crossDeviceFallback); +} + +void FSRename::crossDeviceFallback(uv_fs_t* req) +{ + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("move: Error reading source %s: %s", r->src.c_str(), uv_strerror(result)); + return; + } + + if (S_ISLNK(req->statbuf.st_mode)) + { + auto* raw = r.release(); + (new FSMoveSymlink( + raw->loop, + raw->src, + raw->dest, + [raw](int err) + { + std::unique_ptr r(raw); + if (err < 0) + r->fail("move: Error moving symlink %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(err)); + else + r->succeedTrivially(); + } + ))->start(); + return; + } + + if (S_ISDIR(req->statbuf.st_mode)) + { + // Directory: hand off to FSCopyDirectory. Moving the token transfers coroutine + // ownership; r is then destroyed cleanly with an empty token. + auto* copyDir = new FSCopyDirectory(std::move(r->token), r->loop, r->src, r->dest); + copyDir->start(); + return; + } + + if (!S_ISREG(req->statbuf.st_mode)) + { + r->fail("move: Cannot move %s: unsupported file type", r->src.c_str()); + return; + } + + auto* raw = r.release(); + (new FSMoveSingleFile( + raw->loop, + raw->src, + raw->dest, + [raw](int err) + { + std::unique_ptr r(raw); + if (err < 0) + r->fail("move: Error moving file %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(err)); + else + r->succeedTrivially(); + } + ))->start(); +} + +int rename_impl(lua_State* L, const char* path, const char* dest) +{ + uvutils::ScopedUVRequest req{L, path, dest}; + uv_fs_rename(req->getLoop(), &req->req, path, dest, FSRename::renameCallback); + return lua_yield(L, 0); +} + +int mkdir_impl(lua_State* L, const char* path, int mode) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_mkdir( + req->getLoop(), + &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->succeedTrivially(); + } + ); + + return lua_yield(L, 0); +} + +int rmdir_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_rmdir( + req->getLoop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error removing directory %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeedTrivially(); + } + ); + + return lua_yield(L, 0); +} + +int listdir_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_scandir( + req->getLoop(), + &req->req, + path, + 0, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("listdir: Error listing directory %s (%s)", req->path, uv_strerror(result)); + return; + } + + std::vector> entries; + uv_dirent_t dirent; + int err = 0; + while ((err = uv_fs_scandir_next(req, &dirent)) >= 0) + { + entries.emplace_back(dirent.name, dirent.type); + } + + if (err != UV_EOF) + { + r->fail("listdir: Error reading directory entry (%s)", uv_strerror(err)); + return; + } + + r->succeed( + [entries = std::move(entries)](lua_State* L) + { + lua_createtable(L, entries.size(), 0); + for (size_t i = 0; i < entries.size(); ++i) + { + lua_pushinteger(L, i + 1); + + lua_createtable(L, 0, 2); + + lua_pushstring(L, entries[i].first.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, UV_DIRENT_TYPES[entries[i].second]); + lua_setfield(L, -2, "type"); + + lua_settable(L, -3); + } + return 1; + } + ); + } + ); + + 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..591aa57c7 --- /dev/null +++ b/lute/fs/src/fs_impl.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +struct lua_State; + +namespace fs +{ + +constexpr char UV_TYPENAME_UNKNOWN[] = "unknown"; // UV_DIRENT_UNKNOWN +constexpr char UV_TYPENAME_FILE[] = "file"; // UV_DIRENT_FILE +constexpr char UV_TYPENAME_DIR[] = "dir"; // UV_DIRENT_DIR +constexpr char UV_TYPENAME_LINK[] = "link"; // UV_DIRENT_LINK +constexpr char UV_TYPENAME_FIFO[] = "fifo"; // UV_DIRENT_FIFO +constexpr char UV_TYPENAME_SOCKET[] = "socket"; // UV_DIRENT_SOCKET +constexpr char UV_TYPENAME_CHAR[] = "char"; // UV_DIRENT_CHAR +constexpr char UV_TYPENAME_BLOCK[] = "block"; // UV_DIRENT_BLOCK + +constexpr const char* UV_DIRENT_TYPES[] = { + UV_TYPENAME_UNKNOWN, + UV_TYPENAME_FILE, + UV_TYPENAME_DIR, + UV_TYPENAME_LINK, + UV_TYPENAME_FIFO, + UV_TYPENAME_SOCKET, + UV_TYPENAME_CHAR, + UV_TYPENAME_BLOCK, +}; + +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 stat_impl(lua_State* L, const char* path); +int exists_impl(lua_State* L, const char* path); +int type_impl(lua_State* L, const char* path); + +int link_impl(lua_State* L, const char* path, const char* dest); +int symlink_impl(lua_State* L, const char* path, const char* dest); +int copy_impl(lua_State* L, const char* path, const char* dest); + +int rename_impl(lua_State* L, const char* path, const char* dest); + +int mkdir_impl(lua_State* L, const char* path, int mode); +int rmdir_impl(lua_State* L, const char* path); +int listdir_impl(lua_State* L, const char* path); + +} // 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..542da0bb0 --- /dev/null +++ b/lute/io/include/lute/io.h @@ -0,0 +1,19 @@ +#pragma once + +#include "lute/library.h" + +#include "lua.h" +#include "lualib.h" + +// open the library as a standard global luau library +LUTE_API int luaopen_io(lua_State* L); +// open the library as a table on top of the stack +LUTE_API int luteopen_io(lua_State* L); + +struct IO : LuteLibrary +{ + static constexpr const char kName[] = "io"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; +}; diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp new file mode 100644 index 000000000..7959ed01f --- /dev/null +++ b/lute/io/src/io.cpp @@ -0,0 +1,225 @@ +#include "lute/io.h" + +#include "lute/runtime.h" + +#include "Luau/Variant.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +#include + + +namespace +{ + +struct IOHandle +{ + Luau::Variant streamVariant; + uv_loop_t* loop = nullptr; + ResumeToken resumeToken; + std::shared_ptr self; + std::vector buffer; + uv_write_t writeReq; + uv_buf_t iov; + + static void closeCb(uv_handle_t* handle) + { + IOHandle* ioh = static_cast(handle->data); + ioh->self.reset(); + } + + void close() + { + uv_stream_t* stream = getStream(); + uv_read_stop(stream); + uv_close(reinterpret_cast(stream), closeCb); + } + + uv_stream_t* getStream() + { + return Luau::visit( + [](auto& stream) -> uv_stream_t* + { + return reinterpret_cast(&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) + return; + + 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 + { + handle->resumeToken->fail(uv_strerror(nread)); + } + + handle->close(); +} + +static void onWrite(uv_write_t* req, int status) +{ + IOHandle* handle = static_cast(req->data); + + if (status < 0) + handle->resumeToken->fail(uv_strerror(status)); + else + handle->resumeToken->complete( + [](lua_State* L) + { + return 0; + } + ); + + handle->close(); +} + +int write(lua_State* L) +{ + int nargs = lua_gettop(L); + + std::string toWrite; + for (int i = 1; i <= nargs; i++) + { + size_t len; + const char* str = luaL_checklstring(L, i, &len); + toWrite.append(str, len); + } + + if (toWrite.empty()) + return 0; + + auto handle = std::make_shared(); + handle->loop = getRuntimeLoop(L); + handle->resumeToken = getResumeToken(L); + handle->self = handle; + handle->buffer.assign(toWrite.begin(), toWrite.end()); + + uv_handle_type ht = uv_guess_handle(fileno(stdout)); + if (ht == UV_TTY) + { + uv_tty_t& tty = handle->streamVariant.emplace(); + int status = uv_tty_init(handle->loop, &tty, fileno(stdout), 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, &pipe, 0); + if (status < 0) + luaL_error(L, "Failed to initialize pipe: %s", uv_strerror(status)); + uv_pipe_open(&pipe, fileno(stdout)); + } + else + { + luaL_error(L, "Unsupported stdout type"); + } + + uv_stream_t* stream = handle->getStream(); + stream->data = handle.get(); + handle->iov = uv_buf_init(handle->buffer.data(), handle->buffer.size()); + handle->writeReq.data = handle.get(); + uv_write(&handle->writeReq, stream, &handle->iov, 1, onWrite); + + return lua_yield(L, 0); +} + +int read(lua_State* L) +{ + auto handle = std::make_shared(); + handle->loop = getRuntimeLoop(L); + 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, &pipe, 0); + if (status < 0) + luaL_error(L, "Failed to initialize pipe: %s", uv_strerror(status)); + uv_pipe_open(&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); +} + +} // anonymous namespace + +const char* const IO::properties[] = {nullptr}; + +const luaL_Reg IO::lib[] = { + {"write", write}, + {"read", read}, + {nullptr, nullptr}, +}; + +int IO::pushLibrary(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; +} + +LUTE_API int luaopen_io(lua_State* L) +{ + return IO::openAsGlobal(L); +} + +LUTE_API int luteopen_io(lua_State* L) +{ + return IO::pushLibrary(L); +} diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index edbac0404..51a62180d 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -1,12 +1,20 @@ add_library(Lute.Luau STATIC) target_sources(Lute.Luau PRIVATE + include/lute/configresolver.h include/lute/luau.h + include/lute/tcmoduleresolver.h + include/lute/resolvemodule.h + include/lute/type.h + src/configresolver.cpp src/luau.cpp + src/tcmoduleresolver.cpp + src/resolvemodule.cpp + src/type.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.Common Lute.Require Lute.Runtime Lute.Syntax 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..9c5814354 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -1,30 +1,20 @@ #pragma once +#include "lute/library.h" +#include "lute/resolvemodule.h" + #include "lua.h" #include "lualib.h" // open the library as a standard global luau library -int luaopen_luau(lua_State* L); +LUTE_API int luaopen_luau(lua_State* L); // open the library as a table on top of the stack -int luteopen_luau(lua_State* L); +LUTE_API int luteopen_luau(lua_State* L); -namespace luau +struct LuauLib : LuteLibrary { - -int luau_parse(lua_State* L); - -int luau_parseexpr(lua_State* L); - -int compile_luau(lua_State* L); - -int load_luau(lua_State* L); - -static const luaL_Reg lib[] = { - {"parse", luau_parse}, - {"parseexpr", luau_parseexpr}, - {"compile", compile_luau}, - {"load", load_luau}, - {nullptr, nullptr}, + static constexpr const char kName[] = "luau"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace luau diff --git a/lute/luau/include/lute/resolvemodule.h b/lute/luau/include/lute/resolvemodule.h new file mode 100644 index 000000000..3a7437b7d --- /dev/null +++ b/lute/luau/include/lute/resolvemodule.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +struct lua_State; + +struct ResolvedModule +{ + std::string path; + std::string source; +}; + +std::optional resolveModule(std::string requirePath, std::string requirerChunkname, std::string* error); +std::optional resolveForTypeCheck(std::string requirePath, std::string requirerChunkname, std::string* error); + +int resolveModule_luau(lua_State* L); diff --git a/lute/luau/include/lute/tcmoduleresolver.h b/lute/luau/include/lute/tcmoduleresolver.h new file mode 100644 index 000000000..c694cc876 --- /dev/null +++ b/lute/luau/include/lute/tcmoduleresolver.h @@ -0,0 +1,29 @@ +#pragma once + +#include "lute/reporter.h" + +#include "Luau/DenseHash.h" +#include "Luau/FileResolver.h" + +#include +#include + +namespace Luau +{ + +// Based on CliFileResolver in Analyze.cpp. +struct LuteTypeCheckModuleResolver : Luau::FileResolver +{ + LuteTypeCheckModuleResolver(LuteReporter& reporter); + + 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; + std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override; + + Luau::DenseHashMap sourceCache{""}; + LuteReporter& reporter; +}; + +} // namespace Luau diff --git a/lute/luau/include/lute/type.h b/lute/luau/include/lute/type.h new file mode 100644 index 000000000..5ccbcde1f --- /dev/null +++ b/lute/luau/include/lute/type.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Luau/TypeFwd.h" + +#include "lua.h" + +namespace Luau +{ + +int serializeType(lua_State* L, TypeId ty); +int serializeTypePack(lua_State* L, TypePackId tp); + +} // namespace Luau 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..698927bfa 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1,2635 +1,35 @@ #include "lute/luau.h" -#include "Luau/Ast.h" -#include "Luau/Location.h" -#include "Luau/ParseResult.h" -#include "Luau/Parser.h" -#include "Luau/ParseOptions.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) - -namespace luau -{ - -struct StatResult -{ - std::shared_ptr allocator; - std::shared_ptr names; - - Luau::ParseResult parseResult; -}; - -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); - - Luau::ParseOptions options; - options.captureComments = true; - options.allowDeclarationSyntax = false; - options.storeCstData = true; - - auto parseResult = Luau::Parser::parse(source.data(), source.size(), *names, *allocator, options); - - return StatResult{allocator, names, std::move(parseResult)}; -} - -struct ExprResult -{ - std::shared_ptr allocator; - std::shared_ptr names; - - Luau::ParseExprResult 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); - - Luau::ParseOptions options; - options.captureComments = true; - options.allowDeclarationSyntax = false; - options.storeCstData = true; - - auto parseResult = Luau::Parser::parseExpr(source.data(), source.size(), *names, *allocator, options); - - return ExprResult{allocator, names, std::move(parseResult)}; -} - -static std::vector computeLineOffsets(std::string_view content) -{ - std::vector result{}; - result.emplace_back(0); - - for (size_t i = 0; i < content.size(); i++) - { - auto ch = content[i]; - if (ch == '\r' || ch == '\n') - { - if (ch == '\r' && i + 1 < content.size() && content[i + 1] == '\n') - { - i++; - } - result.push_back(i + 1); - } - } - return result; -} - -static std::vector commentsWithinSpan(const std::vector comments, Luau::Location span) -{ - // TODO: O(n), we could probably binary search if there are a lot of comments - std::vector result; - - for (const auto& comment : comments) - if (span.encloses(comment.location)) - result.emplace_back(comment); - - return result; -} - -struct Trivia -{ - enum TriviaKind - { - Whitespace, - SingleLineComment, - MultiLineComment, - }; - - TriviaKind kind; - Luau::Location location; - std::string_view text; -}; - -struct AstSerialize : public Luau::AstVisitor -{ - lua_State* L; - Luau::CstNodeMap cstNodeMap; - std::string_view source; - Luau::Position currentPosition{0, 0}; - std::vector lineOffsets; - std::vector commentLocations; - - // absolute index for the table where we're storing locals - int localTableIndex; - // reference to previously serialized token - int lastTokenRef = LUA_NOREF; - - AstSerialize(lua_State* L, std::string_view source, Luau::CstNodeMap cstNodeMap, std::vector commentLocations) - : L(L) - , cstNodeMap(std::move(cstNodeMap)) - , source(source) - , lineOffsets(computeLineOffsets(source)) - , commentLocations(std::move(commentLocations)) - { - lua_createtable(L, 0, 0); - localTableIndex = lua_absindex(L, -1); - } - - template - Luau::NotNull lookupCstNode(Luau::AstNode* astNode) - { - const auto cstNode = cstNodeMap.find(astNode); - LUAU_ASSERT(cstNode); - return Luau::NotNull{(*cstNode)->as()}; - } - - void advancePosition(std::string_view contents) - { - if (contents.empty()) - return; - - size_t index = 0; - size_t numLines = 0; - while (true) - { - auto newlinePos = contents.find('\n', index); - if (newlinePos == std::string::npos) - break; - numLines++; - index = newlinePos + 1; - } - - currentPosition.line += numLines; - if (numLines > 0) - currentPosition.column = unsigned(contents.size()) - index; - else - currentPosition.column += unsigned(contents.size()); - } - - std::vector extractWhitespace(const Luau::Position& newPos) - { - auto beginPosition = currentPosition; - - LUAU_ASSERT(currentPosition < newPos); - LUAU_ASSERT(currentPosition.line < lineOffsets.size()); - LUAU_ASSERT(newPos.line < lineOffsets.size()); - size_t startOffset = lineOffsets[currentPosition.line] + currentPosition.column; - size_t endOffset = lineOffsets[newPos.line] + newPos.column; - - std::string_view trivia = source.substr(startOffset, endOffset - startOffset); - - // Tokenize whitespace into smaller parts. Whitespace is separated by `\n` characters - std::vector result; - - while (!trivia.empty()) - { - auto index = trivia.find('\n'); - std::string_view part; - if (index == std::string::npos) - part = trivia; - else - { - part = trivia.substr(0, index + 1); - trivia.remove_prefix(index + 1); - } - - advancePosition(part); - result.push_back(Trivia{Trivia::Whitespace, Luau::Location{beginPosition, currentPosition}, part}); - beginPosition = currentPosition; - - if (index == std::string::npos) - break; - } - LUAU_ASSERT(currentPosition == newPos); - - return result; - } - - std::vector extractTrivia(const Luau::Position& newPos) - { - LUAU_ASSERT(currentPosition <= newPos); - if (currentPosition == newPos) - return {}; - - std::vector result; - - const auto comments = commentsWithinSpan(commentLocations, Luau::Location{currentPosition, newPos}); - for (const auto& comment : comments) - { - if (currentPosition < comment.location.begin) - { - auto whitespace = extractWhitespace(comment.location.begin); - result.insert(result.end(), whitespace.begin(), whitespace.end()); - } - - LUAU_ASSERT(comment.location.begin.line < lineOffsets.size()); - LUAU_ASSERT(comment.location.end.line < lineOffsets.size()); - - size_t startOffset = lineOffsets[comment.location.begin.line] + comment.location.begin.column; - size_t endOffset = lineOffsets[comment.location.end.line] + comment.location.end.column; - - std::string_view commentText = source.substr(startOffset, endOffset - startOffset); - - // TODO: advancePosition is more of a debug check - we can probably just set currentPosition directly here - advancePosition(commentText); - LUAU_ASSERT(currentPosition == comment.location.end); - - // TODO: currently the text includes the `--` / `--[[` characters, should it? - LUAU_ASSERT(comment.type != Luau::Lexeme::BrokenComment); - auto kind = comment.type == Luau::Lexeme::Comment ? Trivia::SingleLineComment : Trivia::MultiLineComment; - result.emplace_back(Trivia{kind, comment.location, commentText}); - } - - if (currentPosition < newPos) - { - auto whitespace = extractWhitespace(newPos); - result.insert(result.end(), whitespace.begin(), whitespace.end()); - } - - LUAU_ASSERT(currentPosition == newPos); - - return result; - } - - // Splits a list of trivia into trailing trivia for the previous token, and leading trivia for the next token - // The trailing trivia consists of all trivia up to and including the first '\n' character seen - static std::pair, std::vector> splitTrivia(std::vector trivia) - { - size_t i = 0; - for (i = 0; i < trivia.size(); i++) - { - if (trivia[i].kind == Trivia::Whitespace && trivia[i].text.find('\n') != std::string::npos) - break; - } - - if (i == trivia.size()) - return {trivia, {}}; - - auto middleIter(trivia.begin()); - std::advance(middleIter, i + 1); - - 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"); - - serialize(location.end); - lua_setfield(L, -2, "end"); - } - - void serialize(Luau::AstName& name) - { - lua_rawcheckstack(L, 1); - lua_pushstring(L, name.value); - } - - void serialize(Luau::AstLocal* local, bool createToken = true, std::optional colonPosition = std::nullopt) - { - lua_rawcheckstack(L, 2); - - lua_pushlightuserdata(L, local); - lua_gettable(L, localTableIndex); - - if (lua_isnil(L, -1)) - { - lua_pop(L, 1); - lua_createtable(L, 0, 4); - - // set up reference for this local into the local table - lua_pushlightuserdata(L, local); - lua_pushvalue(L, -2); - lua_settable(L, localTableIndex); - - if (createToken) - { - serializeToken(local->location.begin, local->name.value); - lua_setfield(L, -2, "name"); - - if (local->annotation) - { - LUAU_ASSERT(colonPosition); - serializeToken(*colonPosition, ":"); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "colon"); - - if (local->annotation) - local->annotation->visit(this); - else - lua_pushnil(L); - - lua_setfield(L, -2, "annotation"); - } - - if (local->shadow) - serialize(local->shadow); - else - lua_pushnil(L); - lua_setfield(L, -2, "shadows"); - } - } - - void serialize(Luau::AstExprTable::Item& item, Luau::CstExprTable::Item* cstNode) - { - LUAU_ASSERT(cstNode); - - lua_rawcheckstack(L, 2); - - if (item.kind == Luau::AstExprTable::Item::List) - { - lua_createtable(L, 0, 3); - lua_pushstring(L, "list"); - lua_setfield(L, -2, "kind"); - - visit(item.value); - lua_setfield(L, -2, "value"); - } - else if (item.kind == Luau::AstExprTable::Item::Record) - { - lua_createtable(L, 0, 5); - lua_pushstring(L, "record"); - lua_setfield(L, -2, "kind"); - - const auto& value = item.key->as()->value; - serializeToken(item.key->location.begin, std::string(value.data, value.size).data()); - lua_setfield(L, -2, "key"); - - LUAU_ASSERT(cstNode->equalsPosition); - serializeToken(*cstNode->equalsPosition, "="); - lua_setfield(L, -2, "equals"); - - visit(item.value); - lua_setfield(L, -2, "value"); - } - else if (item.kind == Luau::AstExprTable::Item::General) - { - lua_createtable(L, 0, 7); - lua_pushstring(L, "general"); - lua_setfield(L, -2, "kind"); - - LUAU_ASSERT(cstNode->indexerOpenPosition); - serializeToken(*cstNode->indexerOpenPosition, "["); - 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"); - - LUAU_ASSERT(cstNode->equalsPosition); - serializeToken(*cstNode->equalsPosition, "="); - lua_setfield(L, -2, "equals"); - - visit(item.value); - lua_setfield(L, -2, "value"); - } - - if (cstNode->separator) - serializeToken(*cstNode->separatorPosition, *cstNode->separator == Luau::CstExprTable::Comma ? "," : ";"); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - } - - void withLocation(Luau::Location location) - { - serialize(location); - lua_setfield(L, -2, "location"); - } - - void serialize(Luau::AstExprBinary::Op& op) - { - if (op == Luau::AstExprBinary::Op::Op__Count) - luaL_error(L, "encountered illegal operator: Op__Count"); - - lua_pushstring(L, Luau::toString(op).data()); - } - - // 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) - { - lua_rawcheckstack(L, 2); - - lua_pushstring(L, tag); - lua_setfield(L, -2, "tag"); - - withLocation(node->location); - } - - void serializeTrivia(const std::vector& trivia) - { - lua_rawcheckstack(L, 3); - lua_createtable(L, trivia.size(), 0); - - for (size_t i = 0; i < trivia.size(); i++) - { - lua_createtable(L, 0, 3); - - switch (trivia[i].kind) - { - case Trivia::Whitespace: - lua_pushstring(L, "whitespace"); - break; - case Trivia::SingleLineComment: - lua_pushstring(L, "comment"); - break; - case Trivia::MultiLineComment: - lua_pushstring(L, "blockcomment"); - break; - } - lua_setfield(L, -2, "tag"); - - serialize(trivia[i].location); - lua_setfield(L, -2, "location"); - - lua_pushlstring(L, trivia[i].text.data(), trivia[i].text.size()); - lua_setfield(L, -2, "text"); - - lua_rawseti(L, -2, i + 1); - } - } - - // For correct trivia computation, everything must end up going through serializeToken - void serializeToken(Luau::Position position, const char* text, int nrec = 0) - { - lua_rawcheckstack(L, 3); - lua_createtable(L, 0, nrec + 4); - - const auto trivia = extractTrivia(position); - if (lastTokenRef != LUA_NOREF) - { - const auto [trailingTrivia, leadingTrivia] = splitTrivia(trivia); - - lua_getref(L, lastTokenRef); - LUAU_ASSERT(lua_istable(L, -1)); - - serializeTrivia(trailingTrivia); - lua_setfield(L, -2, "trailingTrivia"); - lua_pop(L, 1); - lua_unref(L, lastTokenRef); - lastTokenRef = LUA_NOREF; - - serializeTrivia(leadingTrivia); - } - else - { - serializeTrivia(trivia); - } - LUAU_ASSERT(lua_istable(L, -2)); - lua_setfield(L, -2, "leadingTrivia"); - - serialize(position); - lua_setfield(L, -2, "position"); - - lua_pushstring(L, text); - lua_setfield(L, -2, "text"); - advancePosition(text); - - lua_createtable(L, 0, 0); - lua_setfield(L, -2, "trailingTrivia"); - - lastTokenRef = lua_ref(L, -1); - } - - void serializeExprs(Luau::AstArray& exprs, size_t nrec = 0) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, exprs.size, nrec); - - for (size_t i = 0; i < exprs.size; i++) - { - exprs.data[i]->visit(this); - lua_rawseti(L, -2, i + 1); - } - } - - void serializeStats(Luau::AstArray& stats, size_t nrec = 0) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, stats.size, nrec); - - for (size_t i = 0; i < stats.size; i++) - { - stats.data[i]->visit(this); - lua_rawseti(L, -2, i + 1); - } - } - - void serializeAttributes(Luau::AstArray& attrs, size_t nrec = 0) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, attrs.size, nrec); - - for (size_t i = 0; i < attrs.size; i++) - { - serializeAttribute(attrs.data[i]); - lua_rawseti(L, -2, i + 1); - } - } - - template - void serializePunctuated(Luau::AstArray nodes, Luau::AstArray separators, const char* separatorText) - { - lua_rawcheckstack(L, 3); - lua_createtable(L, nodes.size, 0); - - for (size_t i = 0; i < nodes.size; i++) - { - lua_createtable(L, 0, 2); - - nodes.data[i]->visit(this); - lua_setfield(L, -2, "node"); - - if (i < separators.size) - serializeToken(separators.data[i], separatorText); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - - lua_rawseti(L, -2, i + 1); - } - } - - void serializePunctuated(Luau::AstArray nodes, Luau::AstArray separators, const char* separatorText) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, nodes.size, 0); - - for (size_t i = 0; i < nodes.size; i++) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - - if (nodes.data[i].type) - nodes.data[i].type->visit(this); - else - nodes.data[i].typePack->visit(this); - lua_setfield(L, -2, "node"); - - if (i < separators.size) - serializeToken(separators.data[i], separatorText); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - - lua_rawseti(L, -2, i + 1); - } - } - - void serializePunctuated( - Luau::AstArray nodes, - Luau::AstArray separators, - const char* separatorText, - Luau::AstArray colonPositions - ) - { - lua_rawcheckstack(L, 3); - lua_createtable(L, nodes.size, 0); - - for (size_t i = 0; i < nodes.size; i++) - { - lua_createtable(L, 0, 2); - - serialize(nodes.data[i], /* createToken=*/true, colonPositions.size > i ? std::make_optional(colonPositions.data[i]) : std::nullopt); - lua_setfield(L, -2, "node"); - - if (i < separators.size) - serializeToken(separators.data[i], separatorText); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - - lua_rawseti(L, -2, i + 1); - } - } - 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"); - } - - void serializeEof(Luau::Position eofPosition) - { - serializeToken(eofPosition, ""); - - lua_pushstring(L, "eof"); - lua_setfield(L, -2, "tag"); - } - - void serialize(Luau::AstExprGroup* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "group"); - - serializeToken(node->location.begin, "("); - 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"); - } - - void serialize(Luau::AstExprConstantNil* node) - { - serializeToken(node->location.begin, "nil", preambleSize); - serializeNodePreamble(node, "nil"); - } - - void serialize(Luau::AstExprConstantBool* node) - { - serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); - serializeNodePreamble(node, "boolean"); - - lua_pushboolean(L, node->value); - lua_setfield(L, -2, "value"); - } - - void serialize(Luau::AstExprConstantNumber* node) - { - const auto cstNode = lookupCstNode(node); - - serializeToken(node->location.begin, cstNode->value.data, preambleSize + 1); - serializeNodePreamble(node, "number"); - - lua_pushnumber(L, node->value); - lua_setfield(L, -2, "value"); - } - - void serialize(Luau::AstExprConstantString* node) - { - const auto cstNode = lookupCstNode(node); - serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 2); - serializeNodePreamble(node, "string"); - - switch (cstNode->quoteStyle) - { - case Luau::CstExprConstantString::QuotedSingle: - lua_pushstring(L, "single"); - break; - case Luau::CstExprConstantString::QuotedDouble: - lua_pushstring(L, "double"); - break; - case Luau::CstExprConstantString::QuotedRaw: - lua_pushstring(L, "block"); - break; - case Luau::CstExprConstantString::QuotedInterp: - lua_pushstring(L, "interp"); - break; - } - lua_setfield(L, -2, "quoteStyle"); - - lua_pushnumber(L, cstNode->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 - LUAU_ASSERT(currentPosition <= node->location.end); - currentPosition = node->location.end; - } - - void serialize(Luau::AstExprLocal* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "local"); - - serializeToken(node->location.begin, node->local->name.value); - lua_setfield(L, -2, "token"), - - serialize(node->local); - lua_setfield(L, -2, "local"); - - lua_pushboolean(L, node->upvalue); - lua_setfield(L, -2, "upvalue"); - } - - void serialize(Luau::AstExprGlobal* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 1); - - serializeNodePreamble(node, "global"); - - serializeToken(node->location.begin, node->name.value); - lua_setfield(L, -2, "name"); - } - - void serialize(Luau::AstExprVarargs* node) - { - serializeToken(node->location.begin, "...", preambleSize); - serializeNodePreamble(node, "vararg"); - } - - void serialize(Luau::AstExprCall* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 6); - - serializeNodePreamble(node, "call"); - - node->func->visit(this); - lua_setfield(L, -2, "func"); - - if (cstNode->openParens) - serializeToken(*cstNode->openParens, "("); - else - lua_pushnil(L); - lua_setfield(L, -2, "openParens"); - - serializePunctuated(node->args, cstNode->commaPositions, ","); - lua_setfield(L, -2, "arguments"); - - lua_pushboolean(L, node->self); - lua_setfield(L, -2, "self"); - - serialize(node->argLocation); - lua_setfield(L, -2, "argLocation"); - - if (cstNode->closeParens) - serializeToken(*cstNode->closeParens, ")"); - else - lua_pushnil(L); - lua_setfield(L, -2, "closeParens"); - } - - void serialize(Luau::AstExprIndexName* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "indexname"); - - node->expr->visit(this); - lua_setfield(L, -2, "expression"); - - serializeToken(node->opPosition, std::string(1, node->op).data()); - lua_setfield(L, -2, "accessor"); - - serializeToken(node->indexLocation.begin, node->index.value); - lua_setfield(L, -2, "index"); - serialize(node->indexLocation); - lua_setfield(L, -2, "indexLocation"); - } - - void serialize(Luau::AstExprIndexExpr* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "index"); - - node->expr->visit(this); - lua_setfield(L, -2, "expression"); - - serializeToken(cstNode->openBracketPosition, "["); - lua_setfield(L, -2, "openBrackets"); - - node->index->visit(this); - lua_setfield(L, -2, "index"); - - serializeToken(cstNode->closeBracketPosition, "]"); - lua_setfield(L, -2, "closeBrackets"); - } - - void serializeFunctionBody(Luau::AstExprFunction* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 3); - lua_createtable(L, 0, 15); - - if (node->generics.size > 0 || node->genericPacks.size > 0) - { - serializeToken(cstNode->openGenericsPosition, "<"); - 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"); - - serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closeGenerics"); - } - - if (node->self) - serialize(node->self, /* createToken= */ false); - else - lua_pushnil(L); - lua_setfield(L, -2, "self"); - - if (node->argLocation) - { - serializeToken(node->argLocation->begin, "("); - lua_setfield(L, -2, "openParens"); - } - - serializePunctuated(node->args, cstNode->argsCommaPositions, ",", cstNode->argsAnnotationColonPositions); - lua_setfield(L, -2, "parameters"); - - if (node->vararg) - serializeToken(node->varargLocation.begin, "..."); - else - lua_pushnil(L); - lua_setfield(L, -2, "vararg"); - - if (node->varargAnnotation) - serializeToken(cstNode->varargAnnotationColonPosition, ":"); - else - lua_pushnil(L); - lua_setfield(L, -2, "varargColon"); - - if (node->varargAnnotation) - { - if (auto variadic = node->varargAnnotation->as()) - serializeTypePack(variadic, true); - else - node->varargAnnotation->visit(this); - } - else - lua_pushnil(L); - 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"); - } - - if (node->returnAnnotation) - serializeToken(cstNode->returnSpecifierPosition, ":"); - else - lua_pushnil(L); - lua_setfield(L, -2, "returnSpecifier"); - - if (node->returnAnnotation) - node->returnAnnotation->visit(this); - else - lua_pushnil(L); - 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"); - } - - void serialize(Luau::AstExprFunction* node) - { - lua_rawcheckstack(L, 3); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "function"); - - serializeAttributes(node->attributes); - lua_setfield(L, -2, "attributes"); - - const auto cstNode = lookupCstNode(node); - - serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); - - serializeFunctionBody(node); - lua_setfield(L, -2, "body"); - } - - void serialize(Luau::AstExprTable* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 3); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "table"); - - serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); - - lua_createtable(L, node->items.size, 0); - for (size_t i = 0; i < node->items.size; i++) - { - serialize(node->items.data[i], &cstNode->items.data[i]); - lua_rawseti(L, -2, i + 1); - } - lua_setfield(L, -2, "entries"); - - serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); - } - - void serialize(Luau::AstExprUnary* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "unary"); - - const auto cstNode = lookupCstNode(node); - serializeToken(cstNode->opPosition, toString(node->op).data()); - lua_setfield(L, -2, "operator"); - - node->expr->visit(this); - lua_setfield(L, -2, "operand"); - } - - void serialize(Luau::AstExprBinary* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "binary"); - - node->left->visit(this); - lua_setfield(L, -2, "lhsoperand"); - - const auto cstNode = lookupCstNode(node); - serializeToken(cstNode->opPosition, Luau::toString(node->op).data()); - lua_setfield(L, -2, "operator"); - - node->right->visit(this); - lua_setfield(L, -2, "rhsoperand"); - } - - void serialize(Luau::AstExprTypeAssertion* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "cast"); - - node->expr->visit(this); - lua_setfield(L, -2, "operand"); - - const auto cstNode = lookupCstNode(node); - serializeToken(cstNode->opPosition, "::"); - lua_setfield(L, -2, "operator"); - - node->annotation->visit(this); - lua_setfield(L, -2, "annotation"); - } - - void serialize(Luau::AstExprIfElse* node) - { - auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 7); - - serializeNodePreamble(node, "conditional"); - - serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifKeyword"); - - node->condition->visit(this); - lua_setfield(L, -2, "condition"); - - if (node->hasThen) - { - serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenKeyword"); - - node->trueExpr->visit(this); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "consequent"); - - lua_createtable(L, 0, preambleSize + 4); - int i = 0; - while (node->hasElse && node->falseExpr->is() && cstNode->isElseIf) - { - lua_createtable(L, 0, 4); - - node = node->falseExpr->as(); - cstNode = lookupCstNode(node); - - serializeToken(node->location.begin, "elseif"); - lua_setfield(L, -2, "elseifKeyword"); - - node->condition->visit(this); - lua_setfield(L, -2, "condition"); - - if (node->hasThen) - { - serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenKeyword"); - node->trueExpr->visit(this); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "consequent"); - - lua_rawseti(L, -2, i + 1); - i++; - } - lua_setfield(L, -2, "elseifs"); - - if (node->hasElse) - { - serializeToken(cstNode->elsePosition, "else"); - lua_setfield(L, -2, "elseKeyword"); - node->falseExpr->visit(this); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "antecedent"); - } - - void serialize(Luau::AstExprInterpString* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 3); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "interpolatedstring"); - - lua_createtable(L, node->strings.size, 0); - lua_createtable(L, node->expressions.size, 0); - - for (size_t i = 0; i < node->strings.size; i++) - { - auto position = i > 0 ? cstNode->stringPositions.data[i] : node->location.begin; - serializeToken(position, std::string(cstNode->sourceStrings.data[i].data, cstNode->sourceStrings.data[i].size).data()); - lua_rawseti(L, -3, i + 1); - - // Unlike normal tokens, interpolated string parts contain extra characters (`, } or {) that were not included during advancement - // For simplicity, lets set the current position manually. We don't have an end position for these parts, so we must compute - // If string part was single line, end position = current position + 2 (start and end character) - // If string parts was multi line, end position = current position + 1 (just end character) - if (position.line == currentPosition.line) - currentPosition.column += 2; - else - currentPosition.column += 1; - - if (i < node->expressions.size) - { - node->expressions.data[i]->visit(this); - lua_rawseti(L, -2, i + 1); - } - } - lua_setfield(L, -3, "expressions"); - lua_setfield(L, -2, "strings"); - } - - void serialize(Luau::AstExprError* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "error"); - - serializeExprs(node->expressions); - lua_setfield(L, -2, "expressions"); - - // TODO: messageIndex reference - } - - void serializeStat(Luau::AstStatBlock* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 1); - - serializeNodePreamble(node, "block"); - - serializeStats(node->body); - lua_setfield(L, -2, "statements"); - } - - void serializeStat(Luau::AstStatIf* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 8); - - serializeNodePreamble(node, "conditional"); - - serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifKeyword"); - - node->condition->visit(this); - lua_setfield(L, -2, "condition"); - - serializeToken(node->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenKeyword"); - - node->thenbody->visit(this); - lua_setfield(L, -2, "consequent"); - - lua_createtable(L, 0, 0); - int i = 0; - while (node->elsebody && node->elsebody->is()) - { - lua_createtable(L, 0, 4); - - auto elseif = node->elsebody->as(); - serializeToken(elseif->location.begin, "elseif"); - lua_setfield(L, -2, "elseifKeyword"); - - elseif->condition->visit(this); - lua_setfield(L, -2, "condition"); - - serializeToken(elseif->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenKeyword"); - - elseif->thenbody->visit(this); - lua_setfield(L, -2, "consequent"); - - lua_rawseti(L, -2, i + 1); - node = elseif; - i++; - } - lua_setfield(L, -2, "elseifs"); - - if (node->elsebody) - { - LUAU_ASSERT(node->elseLocation); - serializeToken(node->elseLocation->begin, "else"); - lua_setfield(L, -2, "elseKeyword"); - - node->elsebody->visit(this); - lua_setfield(L, -2, "antecedent"); - - serializeToken(node->elsebody->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); - } - else - { - lua_pushnil(L); - lua_setfield(L, -2, "elseKeyword"); - - lua_pushnil(L); - lua_setfield(L, -2, "antecedent"); - - serializeToken(node->thenbody->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); - } - } - - void serializeStat(Luau::AstStatWhile* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 5); - - serializeNodePreamble(node, "while"); - - serializeToken(node->location.begin, "while"); - lua_setfield(L, -2, "whileKeyword"); - - node->condition->visit(this); - lua_setfield(L, -2, "condition"); - - if (node->hasDo) - serializeToken(node->doLocation.begin, "do"); - else - lua_pushnil(L); - 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"); - } - - void serializeStat(Luau::AstStatRepeat* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "repeat"); - - serializeToken(node->location.begin, "repeat"); - 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"); - - node->condition->visit(this); - lua_setfield(L, -2, "condition"); - } - - void serializeStat(Luau::AstStatBreak* node) - { - lua_rawcheckstack(L, 2); - serializeToken(node->location.begin, "break", preambleSize); - serializeNodePreamble(node, "break"); - } - - void serializeStat(Luau::AstStatContinue* node) - { - lua_rawcheckstack(L, 2); - serializeToken(node->location.begin, "continue", preambleSize); - serializeNodePreamble(node, "continue"); - } - - void serializeStat(Luau::AstStatReturn* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "return"); - - serializeToken(node->location.begin, "return"); - lua_setfield(L, -2, "returnKeyword"); - - const auto cstNode = lookupCstNode(node); - serializePunctuated(node->list, cstNode->commaPositions, ","); - lua_setfield(L, -2, "expressions"); - } - - void serializeStat(Luau::AstStatExpr* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 1); - - serializeNodePreamble(node, "expression"); - - node->expr->visit(this); - lua_setfield(L, -2, "expression"); - } - - void serializeStat(Luau::AstStatLocal* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "local"); - - serializeToken(node->location.begin, "local"); - lua_setfield(L, -2, "localKeyword"); - - const auto cstNode = lookupCstNode(node); - serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); - lua_setfield(L, -2, "variables"); - - if (node->equalsSignLocation) - serializeToken(node->equalsSignLocation->begin, "="); - else - lua_pushnil(L); - lua_setfield(L, -2, "equals"); - - serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); - lua_setfield(L, -2, "values"); - } - - void serializeStat(Luau::AstStatFor* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 11); - - const auto cstNode = lookupCstNode(node); - - serializeNodePreamble(node, "for"); - - serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forKeyword"); - - serialize(node->var, /* createToken= */ true, std::make_optional(cstNode->annotationColonPosition)); - lua_setfield(L, -2, "variable"); - - serializeToken(cstNode->equalsPosition, "="); - lua_setfield(L, -2, "equals"); - - node->from->visit(this); - lua_setfield(L, -2, "from"); - - serializeToken(cstNode->endCommaPosition, ","); - lua_setfield(L, -2, "toComma"); - - node->to->visit(this); - lua_setfield(L, -2, "to"); - - if (cstNode->stepCommaPosition) - { - serializeToken(*cstNode->stepCommaPosition, ","); - lua_setfield(L, -2, "stepComma"); - } - - if (node->step) - node->step->visit(this); - else - lua_pushnil(L); - lua_setfield(L, -2, "step"); - - if (node->hasDo) - serializeToken(node->doLocation.begin, "do"); - else - lua_pushnil(L); - 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"); - } - - void serializeStat(Luau::AstStatForIn* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 7); - - const auto cstNode = lookupCstNode(node); - - serializeNodePreamble(node, "forin"); - - serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forKeyword"); - - serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); - lua_setfield(L, -2, "variables"); - - if (node->hasIn) - serializeToken(node->inLocation.begin, "in"); - else - lua_pushnil(L); - lua_setfield(L, -2, "inKeyword"); - - serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); - lua_setfield(L, -2, "values"); - - if (node->hasDo) - serializeToken(node->doLocation.begin, "do"); - else - lua_pushnil(L); - 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"); - } - - void serializeStat(Luau::AstStatAssign* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - const auto cstNode = lookupCstNode(node); - - serializeNodePreamble(node, "assign"); - - serializePunctuated(node->vars, cstNode->varsCommaPositions, ","); - lua_setfield(L, -2, "variables"); - - serializeToken(cstNode->equalsPosition, "="); - lua_setfield(L, -2, "equals"); - - serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); - lua_setfield(L, -2, "values"); - } - - void serializeStat(Luau::AstStatCompoundAssign* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "compoundassign"); - - node->var->visit(this); - lua_setfield(L, -2, "variable"); - - const auto cstNode = lookupCstNode(node); - serializeToken(cstNode->opPosition, (Luau::toString(node->op) + "=").data()); - lua_setfield(L, -2, "operand"); - - node->value->visit(this); - lua_setfield(L, -2, "value"); - } - - void serializeStat(Luau::AstStatFunction* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "function"); - - const auto cstNode = lookupCstNode(node); - - serializeAttributes(node->func->attributes); - lua_setfield(L, -2, "attributes"); - - serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); - - node->name->visit(this); - lua_setfield(L, -2, "name"); - - serializeFunctionBody(node->func); - lua_setfield(L, -2, "body"); - } - - void serializeStat(Luau::AstStatLocalFunction* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 5); - - serializeNodePreamble(node, "localfunction"); - - serializeAttributes(node->func->attributes); - lua_setfield(L, -2, "attributes"); - - const auto cstNode = lookupCstNode(node); - - serializeToken(cstNode->localKeywordPosition, "local"); - lua_setfield(L, -2, "localKeyword"); - - serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); - - serialize(node->name); - lua_setfield(L, -2, "name"); - - serializeFunctionBody(node->func); - lua_setfield(L, -2, "body"); - } - - static Luau::AstArray splitArray(Luau::AstArray arr, size_t index) - { - if (arr.size < index) - return arr; - return {arr.data + index, arr.size - index}; - } - - void serializeStat(Luau::AstStatTypeAlias* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 9); - - serializeNodePreamble(node, "typealias"); - - const auto cstNode = lookupCstNode(node); - - if (node->exported) - serializeToken(node->location.begin, "export"); - else - lua_pushnil(L); - lua_setfield(L, -2, "export"); - - serializeToken(cstNode->typeKeywordPosition, "type"); - lua_setfield(L, -2, "typeToken"); - - serializeToken(node->nameLocation.begin, node->name.value); - lua_setfield(L, -2, "name"); - - if (node->generics.size > 0 || node->genericPacks.size > 0) - { - serializeToken(cstNode->genericsOpenPosition, "<"); - 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"); - - serializeToken(cstNode->genericsClosePosition, ">"); - lua_setfield(L, -2, "closeGenerics"); - } - - serializeToken(cstNode->equalsPosition, "="); - lua_setfield(L, -2, "equals"); - - node->type->visit(this); - lua_setfield(L, -2, "type"); - } - - void serializeStat(Luau::AstStatTypeFunction* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 5); - - const auto cstNode = lookupCstNode(node); - - serializeNodePreamble(node, "typefunction"); - - if (node->exported) - serializeToken(node->location.begin, "export"); - else - lua_pushnil(L); - lua_setfield(L, -2, "export"); - - serializeToken(cstNode->typeKeywordPosition, "type"); - lua_setfield(L, -2, "type"); - - serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); - - serializeToken(node->nameLocation.begin, node->name.value); - lua_setfield(L, -2, "name"); - - serializeFunctionBody(node->body); - lua_setfield(L, -2, "body"); - } - - void serializeStat(Luau::AstStatDeclareFunction* node) - { - // TODO: declarations - } - - void serializeStat(Luau::AstStatDeclareGlobal* node) - { - // TODO: declarations - } - - void serializeStat(Luau::AstStatDeclareExternType* node) - { - // TODO: declarations - } - - void serializeStat(Luau::AstStatError* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "error"); - - serializeExprs(node->expressions); - lua_setfield(L, -2, "expressions"); - - serializeStats(node->statements); - lua_setfield(L, -2, "statements"); - - // TODO: messageIndex reference - } - - void serializeType(Luau::AstTypeReference* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 6); - - serializeNodePreamble(node, "reference"); - - const auto cstNode = node->prefix || node->hasParameterList ? lookupCstNode(node).get() : nullptr; - - if (node->prefix) - { - LUAU_ASSERT(node->prefixLocation); - serializeToken(node->prefixLocation->begin, node->prefix->value); - lua_setfield(L, -2, "prefix"); - - LUAU_ASSERT(cstNode); - LUAU_ASSERT(cstNode->prefixPointPosition); - serializeToken(*cstNode->prefixPointPosition, "."); - lua_setfield(L, -2, "prefixPoint"); - } - - serializeToken(node->nameLocation.begin, node->name.value); - lua_setfield(L, -2, "name"); - - if (node->hasParameterList) - { - LUAU_ASSERT(cstNode); - serializeToken(cstNode->openParametersPosition, "<"); - lua_setfield(L, -2, "openParameters"); - - serializePunctuated(node->parameters, cstNode->parametersCommaPositions, ","); - lua_setfield(L, -2, "parameters"); - - serializeToken(cstNode->closeParametersPosition, ">"); - lua_setfield(L, -2, "closeParameters"); - } - } - - void serializeType(Luau::AstTypeTable* node) - { - const auto cstNode = lookupCstNode(node); - - if (cstNode->isArray) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "array"); - - serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); - - if (node->indexer->accessLocation) - { - LUAU_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); - serializeToken(node->indexer->accessLocation->begin, node->indexer->access == Luau::AstTableAccess::Read ? "read" : "write"); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "access"); - - node->indexer->resultType->visit(this); - lua_setfield(L, -2, "type"); - - serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); - - return; - } - - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "table"); - - serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); - - lua_createtable(L, cstNode->items.size, 0); - const Luau::AstTableProp* prop = node->props.begin(); - for (size_t i = 0; i < cstNode->items.size; i++) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 8); - - Luau::CstTypeTable::Item item = cstNode->items.data[i]; - - if (item.kind == Luau::CstTypeTable::Item::Kind::Indexer) - { - LUAU_ASSERT(node->indexer); - - lua_pushstring(L, "indexer"); - lua_setfield(L, -2, "kind"); - - if (node->indexer->accessLocation) - { - LUAU_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); - serializeToken(node->indexer->accessLocation->begin, node->indexer->access == Luau::AstTableAccess::Read ? "read" : "write"); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "access"); - - serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); - - node->indexer->indexType->visit(this); - lua_setfield(L, -2, "key"); - - serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); - - serializeToken(item.colonPosition, ":"); - lua_setfield(L, -2, "colon"); - - node->indexer->resultType->visit(this); - lua_setfield(L, -2, "value"); - - if (item.separator) - serializeToken(*item.separatorPosition, item.separator == Luau::CstExprTable::Comma ? "," : ";"); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - } - else - { - if (item.kind == Luau::CstTypeTable::Item::Kind::StringProperty) - { - lua_pushstring(L, "stringproperty"); - lua_setfield(L, -2, "kind"); - } - else - { - lua_pushstring(L, "property"); - lua_setfield(L, -2, "kind"); - } - - if (prop->accessLocation) - { - LUAU_ASSERT(prop->access != Luau::AstTableAccess::ReadWrite); - serializeToken(prop->accessLocation->begin, prop->access == Luau::AstTableAccess::Read ? "read" : "write"); - } - else - lua_pushnil(L); - lua_setfield(L, -2, "access"); - - if (item.kind == Luau::CstTypeTable::Item::Kind::StringProperty) - { - serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); - - { - auto initialPosition = item.stringPosition; - serializeToken(item.stringPosition, item.stringInfo->sourceString.data); - - switch (item.stringInfo->quoteStyle) - { - case Luau::CstExprConstantString::QuotedSingle: - lua_pushstring(L, "single"); - break; - case Luau::CstExprConstantString::QuotedDouble: - lua_pushstring(L, "double"); - break; - default: - LUAU_ASSERT(false); - } - 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 - // If string part was single line, end position = current position + 2 (start and end character) - // If string parts was multi line, end position = current position + 1 (just end character) - if (initialPosition.line == currentPosition.line) - currentPosition.column += 2; - else - currentPosition.column += 1; - } - lua_setfield(L, -2, "key"); - - serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); - } - else - { - serializeToken(prop->location.begin, prop->name.value); - lua_setfield(L, -2, "key"); - } - - serializeToken(item.colonPosition, ":"); - lua_setfield(L, -2, "colon"); - - prop->type->visit(this); - lua_setfield(L, -2, "value"); - - if (item.separator) - serializeToken(*item.separatorPosition, item.separator == Luau::CstExprTable::Comma ? "," : ";"); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - - ++prop; - } - lua_rawseti(L, -2, i + 1); - } - lua_setfield(L, -2, "entries"); - - serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); - } - - void serializeType(Luau::AstTypeFunction* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 10); - - serializeNodePreamble(node, "function"); - - const auto cstNode = lookupCstNode(node); - - if (node->generics.size > 0 || node->genericPacks.size > 0) - { - serializeToken(cstNode->openGenericsPosition, "<"); - 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"); - - serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closeGenerics"); - } - - serializeToken(cstNode->openArgsPosition, "("); - lua_setfield(L, -2, "openParens"); - - lua_createtable(L, node->argTypes.types.size, 0); - for (size_t i = 0; i < node->argTypes.types.size; i++) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 3); - if (i < node->argNames.size && node->argNames.data[i].has_value()) - serializeToken(node->argNames.data[i]->second.begin, node->argNames.data[i]->first.value); - else - lua_pushnil(L); - lua_setfield(L, -2, "name"); - - if (i < cstNode->argumentNameColonPositions.size && cstNode->argumentNameColonPositions.data[i].has_value()) - serializeToken(*cstNode->argumentNameColonPositions.data[i], ":"); - else - lua_pushnil(L); - lua_setfield(L, -2, "colon"); - - node->argTypes.types.data[i]->visit(this); - lua_setfield(L, -2, "type"); - } - lua_setfield(L, -2, "node"); - - if (i < cstNode->argumentsCommaPositions.size) - serializeToken(cstNode->argumentsCommaPositions.data[i], ","); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - - lua_rawseti(L, -2, i + 1); - } - lua_setfield(L, -2, "parameters"); - - if (node->argTypes.tailType) - node->argTypes.tailType->visit(this); - else - lua_pushnil(L); - lua_setfield(L, -2, "vararg"); - - serializeToken(cstNode->closeArgsPosition, ")"); - lua_setfield(L, -2, "closeParens"); - - serializeToken(cstNode->returnArrowPosition, "->"); - lua_setfield(L, -2, "returnArrow"); - - node->returnTypes->visit(this); - lua_setfield(L, -2, "returnTypes"); - } - - void serializeType(Luau::AstTypeTypeof* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "typeof"); - - serializeToken(node->location.begin, "typeof"); - lua_setfield(L, -2, "typeof"); - - const auto cstNode = lookupCstNode(node); - serializeToken(cstNode->openPosition, "("); - lua_setfield(L, -2, "openParens"); - - node->expr->visit(this); - lua_setfield(L, -2, "expression"); - - serializeToken(cstNode->closePosition, ")"); - lua_setfield(L, -2, "closeParens"); - } - - void serializeType(Luau::AstTypeUnion* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "union"); - - if (cstNode->leadingPosition) - serializeToken(*cstNode->leadingPosition, "|"); - else - lua_pushnil(L); - lua_setfield(L, -2, "leading"); - - lua_createtable(L, node->types.size, 0); - size_t separatorPositions = 0; - for (size_t i = 0; i < node->types.size; i++) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - - if (node->types.data[i]->is()) - { - serializeToken(node->types.data[i]->location.begin, "?", 1); - lua_pushstring(L, "optional"); - lua_setfield(L, -2, "tag"); - lua_setfield(L, -2, "node"); - - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - } - else - { - node->types.data[i]->visit(this); - lua_setfield(L, -2, "node"); - - if (i < node->types.size - 1 && !node->types.data[i + 1]->is() && - separatorPositions < cstNode->separatorPositions.size) - serializeToken(cstNode->separatorPositions.data[separatorPositions], "|"); - else - lua_pushnil(L); - lua_setfield(L, -2, "separator"); - separatorPositions++; - } - - lua_rawseti(L, -2, i + 1); - } - lua_setfield(L, -2, "types"); - } - - void serializeType(Luau::AstTypeIntersection* node) - { - const auto cstNode = lookupCstNode(node); - - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "intersection"); - - if (cstNode->leadingPosition) - serializeToken(*cstNode->leadingPosition, "&"); - else - lua_pushnil(L); - lua_setfield(L, -2, "leading"); - - serializePunctuated(node->types, cstNode->separatorPositions, "&"); - lua_setfield(L, -2, "types"); - } - - void serializeType(Luau::AstTypeSingletonBool* node) - { - serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); - serializeNodePreamble(node, "boolean"); - - lua_pushboolean(L, node->value); - lua_setfield(L, -2, "value"); - } - - void serializeType(Luau::AstTypeSingletonString* node) - { - const auto cstNode = lookupCstNode(node); - serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 1); - serializeNodePreamble(node, "string"); - - switch (cstNode->quoteStyle) - { - case Luau::CstExprConstantString::QuotedSingle: - lua_pushstring(L, "single"); - break; - case Luau::CstExprConstantString::QuotedDouble: - lua_pushstring(L, "double"); - break; - default: - LUAU_ASSERT(false); - } - 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 - LUAU_ASSERT(currentPosition <= node->location.end); - currentPosition = node->location.end; - } - - void serializeType(Luau::AstTypeGroup* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "group"); - - serializeToken(node->location.begin, "("); - 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"); - } - - void serializeType(Luau::AstGenericType* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "generic"); - - const auto cstNode = lookupCstNode(node); - - serializeToken(node->location.begin, node->name.value); - lua_setfield(L, -2, "name"); - - if (node->defaultValue) - serializeToken(*cstNode->defaultEqualsPosition, "="); - else - lua_pushnil(L); - lua_setfield(L, -2, "equals"); - - if (node->defaultValue) - node->defaultValue->visit(this); - else - lua_pushnil(L); - lua_setfield(L, -2, "default"); - } - - void serializeType(Luau::AstGenericTypePack* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "generic"); - - const auto cstNode = lookupCstNode(node); - - serializeToken(node->location.begin, node->name.value); - lua_setfield(L, -2, "name"); - - serializeToken(cstNode->ellipsisPosition, "..."); - lua_setfield(L, -2, "ellipsis"); - - if (node->defaultValue) - serializeToken(*cstNode->defaultEqualsPosition, "="); - else - lua_pushnil(L); - lua_setfield(L, -2, "equals"); - - if (node->defaultValue) - node->defaultValue->visit(this); - else - lua_pushnil(L); - lua_setfield(L, -2, "default"); - } - - void serializeType(Luau::AstTypeError* node) - { - // TODO: types - } - - void serializeTypePack(Luau::AstTypePackExplicit* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); - - serializeNodePreamble(node, "explicit"); - - const auto cstNode = lookupCstNode(node); - - if (cstNode->hasParentheses) - serializeToken(cstNode->openParenthesesPosition, "("); - else - lua_pushnil(L); - lua_setfield(L, -2, "openParens"); - - serializePunctuated(node->typeList.types, cstNode->commaPositions, ","); - lua_setfield(L, -2, "types"); - - if (node->typeList.tailType) - node->typeList.tailType->visit(this); - else - lua_pushnil(L); - lua_setfield(L, -2, "tailType"); - - if (cstNode->hasParentheses) - serializeToken(cstNode->closeParenthesesPosition, ")"); - else - lua_pushnil(L); - lua_setfield(L, -2, "closeParens"); - } - - void serializeTypePack(Luau::AstTypePackGeneric* node) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "generic"); - - serializeToken(node->location.begin, node->genericName.value); - lua_setfield(L, -2, "name"); - - const auto cstNode = lookupCstNode(node); - serializeToken(cstNode->ellipsisPosition, "..."); - lua_setfield(L, -2, "ellipsis"); - } - - void serializeTypePack(Luau::AstTypePackVariadic* node, bool forVarArg = false) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 2); - - serializeNodePreamble(node, "variadic"); - - if (!forVarArg) - serializeToken(node->location.begin, "..."); - else - lua_pushnil(L); - lua_setfield(L, -2, "ellipsis"); - - node->variadicType->visit(this); - lua_setfield(L, -2, "type"); - } - - bool visit(Luau::AstExpr* node) override - { - node->visit(this); - return false; - } - - bool visit(Luau::AstExprGroup* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprConstantNil* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprConstantBool* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprConstantNumber* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprConstantString* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprLocal* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprGlobal* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprVarargs* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprCall* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprIndexName* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprIndexExpr* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprFunction* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprTable* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprUnary* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprBinary* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprTypeAssertion* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprIfElse* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprInterpString* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstExprError* node) override - { - serialize(node); - return false; - } - - bool visit(Luau::AstStat* node) override - { - node->visit(this); - return false; - } - - bool visit(Luau::AstStatBlock* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatIf* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatWhile* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatRepeat* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatBreak* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatContinue* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatReturn* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatExpr* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatLocal* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatFor* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatForIn* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatAssign* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatCompoundAssign* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatFunction* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatLocalFunction* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatTypeAlias* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatTypeFunction* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatDeclareFunction* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatDeclareGlobal* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatDeclareExternType* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstStatError* node) override - { - serializeStat(node); - return false; - } - - bool visit(Luau::AstType* node) override - { - return true; - } - - bool visit(Luau::AstTypeReference* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeTable* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeFunction* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeTypeof* node) override - { - serializeType(node); - return false; - } +#include "lute/common.h" +#include "lute/configresolver.h" +#include "lute/runtime.h" +#include "lute/tcmoduleresolver.h" +#include "lute/type.h" - bool visit(Luau::AstTypeUnion* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeIntersection* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeSingletonBool* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeSingletonString* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstTypeError* node) override - { - return true; - } - - bool visit(Luau::AstTypePack* node) override - { - return true; - } - - bool visit(Luau::AstTypePackExplicit* node) override - { - serializeTypePack(node); - return false; - } - - bool visit(Luau::AstTypePackVariadic* node) override - { - serializeTypePack(node); - return false; - } - - bool visit(Luau::AstTypePackGeneric* node) override - { - serializeTypePack(node); - return false; - } - - bool visit(Luau::AstTypeGroup* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstGenericType* node) override - { - serializeType(node); - return false; - } - - bool visit(Luau::AstGenericTypePack* node) override - { - serializeType(node); - return false; - } -}; - -int luau_parse(lua_State* L) -{ - std::string source = luaL_checkstring(L, 1); - - StatResult result = parse(source); - - auto& errors = result.parseResult.errors; - - if (!errors.empty()) - { - std::vector locationStrings{}; - locationStrings.reserve(errors.size()); - - size_t size = 0; - for (auto error : errors) - { - locationStrings.emplace_back(Luau::toString(error.getLocation())); - size += locationStrings.back().size() + 2 + error.getMessage().size() + 1; - } - - std::string fullError; - fullError.reserve(size); - - for (size_t i = 0; i < errors.size(); i++) - { - fullError += locationStrings[i]; - fullError += ": "; - fullError += errors[i].getMessage(); - fullError += "\n"; - } - - luaL_error(L, "parsing failed:\n%s", fullError.c_str()); - } - - lua_rawcheckstack(L, 6); - - lua_createtable(L, 0, 4); - - AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; - serializer.visit(result.parseResult.root); - lua_setfield(L, -2, "root"); - - serializer.serializeEof(result.parseResult.root->location.end); - lua_setfield(L, -2, "eof"); - - lua_pushnumber(L, result.parseResult.lines); - lua_setfield(L, -2, "lines"); +#include "Luau/Ast.h" +#include "Luau/BuiltinDefinitions.h" +#include "Luau/Compiler.h" +#include "Luau/Frontend.h" +#include "Luau/Location.h" +#include "Luau/NotNull.h" +#include "Luau/ParseOptions.h" +#include "Luau/Parser.h" +#include "Luau/ParseResult.h" +#include "Luau/ToString.h" - lua_createtable(L, serializer.lineOffsets.size(), 0); - for (size_t i = 0; i < serializer.lineOffsets.size(); i++) - { - lua_pushinteger(L, i + 1); - lua_pushnumber(L, serializer.lineOffsets[i]); - lua_settable(L, -3); - } - lua_setfield(L, -2, "lineOffsets"); +#include "lua.h" +#include "lualib.h" - return 1; -} +#include +#include +#include +#include +#include -int luau_parseexpr(lua_State* L) +namespace luau { - std::string source = luaL_checkstring(L, 1); - - ExprResult result = parseExpr(source); - - auto& errors = result.parseResult.errors; - - if (!errors.empty()) - { - std::vector locationStrings{}; - locationStrings.reserve(errors.size()); - - size_t size = 0; - for (auto error : errors) - { - locationStrings.emplace_back(Luau::toString(error.getLocation())); - size += locationStrings.back().size() + 2 + error.getMessage().size() + 1; - } - - std::string fullError; - fullError.reserve(size); - - for (size_t i = 0; i < errors.size(); i++) - { - fullError += locationStrings[i]; - fullError += ": "; - fullError += errors[i].getMessage(); - fullError += "\n"; - } - - luaL_error(L, "parsing failed:\n%s", fullError.c_str()); - } - - AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; - serializer.visit(result.parseResult.expr); - return 1; -} +static constexpr const char kCompileResultType[] = "CompileResult"; inline int check_int_field(lua_State* L, int obj_idx, const char* field_name, int default_value) { @@ -2661,74 +61,113 @@ 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::LuteTypeCheckModuleResolver moduleResolver{getRuntime(L)->reporter}; + 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; + // Serialize and push the return type + serializeTypePack(L, modulePtr->returnType); + + 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); return 1; } -int luaopen_luau(lua_State* L) -{ - luaL_register(L, "luau", luau::lib); +} // namespace luau - return init_luau_lib(L); -} +const char* const LuauLib::properties[] = {nullptr}; -int luteopen_luau(lua_State* L) +const luaL_Reg LuauLib::lib[] = { + {"compile", luau::compile_luau}, + {"load", luau::load_luau}, + {"resolveModule", resolveModule_luau}, + {"typeofModule", luau::typeofModule_luau}, + {nullptr, nullptr}, +}; + +int LuauLib::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(luau::lib)); + lua_createtable(L, 0, std::size(LuauLib::lib) + std::size(LuauLib::properties)); - for (auto& [name, func] : luau::lib) + for (auto& [name, func] : LuauLib::lib) { if (!name || !func) break; @@ -2739,5 +178,15 @@ int luteopen_luau(lua_State* L) lua_setreadonly(L, -1, 1); - return init_luau_lib(L); + return luau::initLuauLibrary(L); +} + +LUTE_API int luaopen_luau(lua_State* L) +{ + return LuauLib::openAsGlobal(L); +} + +LUTE_API int luteopen_luau(lua_State* L) +{ + return LuauLib::pushLibrary(L); } diff --git a/lute/luau/src/resolvemodule.cpp b/lute/luau/src/resolvemodule.cpp new file mode 100644 index 000000000..17a8caaf1 --- /dev/null +++ b/lute/luau/src/resolvemodule.cpp @@ -0,0 +1,400 @@ +#include "lute/resolvemodule.h" + +#include "lute/batteriesvfs.h" +#include "lute/filevfs.h" +#include "lute/lutevfs.h" +#include "lute/modulepath.h" +#include "lute/stdlibvfs.h" + +#include "Luau/FileUtils.h" +#include "Luau/RequireNavigator.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include + +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 +class FileVfsContext : public Luau::Require::NavigationContext +{ +public: + FileVfsContext(std::string requirerChunkname); + + NavigateResult resetToRequirer() 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 getConfig() const override; + + FileVfs vfs; + std::string requirerChunkname; +}; + +FileVfsContext::FileVfsContext(std::string requirerChunkname) + : requirerChunkname(std::move(requirerChunkname)) +{ +} + +NC::NavigateResult FileVfsContext::resetToRequirer() +{ + return convert(vfs.resetToPath(requirerChunkname)); +} + +NC::NavigateResult FileVfsContext::jumpToAlias(const std::string& path) +{ + return convert(vfs.jumpToAlias(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::getConfig() const +{ + return vfs.getConfig(); +} + +struct LuteTypeCheckVfs +{ + FileVfs fileVfs; + StdLibVfs stdLibVfs; + LuteVfs luteVfs; + BatteriesVfs batteriesVfs; + + enum class VFSType + { + Disk, + Std, + Lute, + Batteries, + }; + VFSType vfsType = VFSType::Disk; + + NavigationStatus jumpToAlias(const std::string& path) + { + if (vfsType == VFSType::Disk) + { + return fileVfs.jumpToAlias(path); + } + return resetToPath(path); + } + + NavigationStatus resetToPath(const std::string& path) + { + if (path.rfind("@std", 0) == 0) + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath(path); + } + else if (path.rfind("@lute", 0) == 0) + { + vfsType = VFSType::Lute; + return luteVfs.resetToPath(path); + } + else if (path.rfind("@batteries", 0) == 0) + { + vfsType = VFSType::Batteries; + return batteriesVfs.resetToPath(path); + } + else + { + vfsType = VFSType::Disk; + std::string diskPath = path; + if (!diskPath.empty() && diskPath[0] == '@') + diskPath = diskPath.substr(1); + return fileVfs.resetToPath(diskPath); + } + } + + NavigationStatus toParent() + { + switch (vfsType) + { + case VFSType::Disk: + return fileVfs.toParent(); + case VFSType::Std: + return stdLibVfs.toParent(); + case VFSType::Lute: + return luteVfs.toParent(); + case VFSType::Batteries: + return batteriesVfs.toParent(); + } + return NavigationStatus::NotFound; + } + + NavigationStatus toChild(const std::string& component) + { + switch (vfsType) + { + case VFSType::Disk: + return fileVfs.toChild(component); + case VFSType::Std: + return stdLibVfs.toChild(component); + case VFSType::Lute: + return luteVfs.toChild(component); + case VFSType::Batteries: + return batteriesVfs.toChild(component); + } + return NavigationStatus::NotFound; + } + + std::string getIdentifier() const + { + switch (vfsType) + { + case VFSType::Disk: + return fileVfs.getAbsoluteFilePath(); + case VFSType::Std: + return stdLibVfs.getIdentifier(); + case VFSType::Lute: + return luteVfs.getIdentifier(); + case VFSType::Batteries: + return batteriesVfs.getIdentifier(); + } + return ""; + } + + std::optional readSource() const + { + std::string identifier = getIdentifier(); + if (identifier.empty()) + return std::nullopt; + + switch (vfsType) + { + case VFSType::Std: + return stdLibVfs.getContents(identifier); + case VFSType::Lute: + return luteVfs.getContents(identifier); + case VFSType::Batteries: + return batteriesVfs.getContents(identifier); + default: + return readFile(identifier); + } + } +}; + +class LuteTypeCheckContext : public Luau::Require::NavigationContext +{ +public: + LuteTypeCheckContext(std::string requirerChunkname) + : requirerChunkname(std::move(requirerChunkname)) + { + } + + NavigateResult resetToRequirer() override + { + return convert(vfs.resetToPath(requirerChunkname)); + } + + NavigateResult jumpToAlias(const std::string& path) override + { + return convert(vfs.jumpToAlias(path)); + } + + NavigateResult toParent() override + { + return convert(vfs.toParent()); + } + + NavigateResult toChild(const std::string& component) override + { + return convert(vfs.toChild(component)); + } + + NavigateResult toAliasOverride(const std::string& aliasUnprefixed) override + { + if (aliasUnprefixed == "std") + { + vfs.vfsType = LuteTypeCheckVfs::VFSType::Std; + return convert(vfs.stdLibVfs.resetToPath("@std")); + } + else if (aliasUnprefixed == "lute") + { + vfs.vfsType = LuteTypeCheckVfs::VFSType::Lute; + return convert(vfs.luteVfs.resetToPath("@lute")); + } + else if (aliasUnprefixed == "batteries" && vfs.vfsType != LuteTypeCheckVfs::VFSType::Disk) + { + vfs.vfsType = LuteTypeCheckVfs::VFSType::Batteries; + return convert(vfs.batteriesVfs.resetToPath("@batteries")); + } + return NC::NavigateResult::NotFound; + } + + ConfigStatus getConfigStatus() const override + { + if (vfs.vfsType == LuteTypeCheckVfs::VFSType::Disk) + return convert(vfs.fileVfs.getConfigStatus()); + return NC::ConfigStatus::Absent; + } + + ConfigBehavior getConfigBehavior() const override + { + return NC::ConfigBehavior::GetConfig; + } + + std::optional getConfig() const override + { + if (vfs.vfsType == LuteTypeCheckVfs::VFSType::Disk) + return vfs.fileVfs.getConfig(); + return std::nullopt; + } + + LuteTypeCheckVfs vfs; + std::string requirerChunkname; +}; + +// 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 resolveModule(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; +} + +std::optional resolveForTypeCheck(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; + } + + LuteTypeCheckContext context{requirerChunkname}; + 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::optional source = context.vfs.readSource(); + if (!source) + { + if (error) + *error = "failed to read source for '" + context.vfs.getIdentifier() + "'"; + return std::nullopt; + } + + ResolvedModule result; + result.path = context.vfs.getIdentifier(); + result.source = std::move(*source); + + return result; +} + +int resolveModule_luau(lua_State* L) +{ + std::string requirePath = luaL_checkstring(L, 1); + std::string requirerChunkname = luaL_checkstring(L, 2); + + std::string error; + std::optional absolutePath = resolveModule(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/luau/src/tcmoduleresolver.cpp b/lute/luau/src/tcmoduleresolver.cpp new file mode 100644 index 000000000..4d9cd2439 --- /dev/null +++ b/lute/luau/src/tcmoduleresolver.cpp @@ -0,0 +1,73 @@ +#include "lute/tcmoduleresolver.h" + +#include "lute/resolvemodule.h" + +#include "Luau/Ast.h" +#include "Luau/FileUtils.h" + +namespace Luau +{ + +LuteTypeCheckModuleResolver::LuteTypeCheckModuleResolver(LuteReporter& reporter) + : reporter(reporter) +{ +} + +std::optional LuteTypeCheckModuleResolver::readSource(const Luau::ModuleName& name) +{ + if (name == "-") + { + std::optional source = readStdin(); + if (!source) + return std::nullopt; + return Luau::SourceCode{*source, Luau::SourceCode::Script}; + } + + if (const std::string* source = sourceCache.find(name)) + return Luau::SourceCode{*source, Luau::SourceCode::Module}; + + 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 LuteTypeCheckModuleResolver::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; + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + requirerChunkname = "@" + requirerChunkname; + + std::optional resolved = ::resolveForTypeCheck(requirePath, std::move(requirerChunkname), &error); + if (!resolved) + { + reporter.formatError("Failed to resolve require: %s\n", error.c_str()); + return std::nullopt; + } + + sourceCache[resolved->path] = resolved->source; + + return Luau::ModuleInfo{resolved->path}; + } + + return std::nullopt; +} + +std::string LuteTypeCheckModuleResolver::getHumanReadableModuleName(const Luau::ModuleName& name) const +{ + if (name == "-") + return "stdin"; + return name; +} + +} // namespace Luau diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp new file mode 100644 index 000000000..d51c80429 --- /dev/null +++ b/lute/luau/src/type.cpp @@ -0,0 +1,710 @@ +#include "lute/type.h" + +#include "lute/common.h" + +#include "Luau/ToString.h" +#include "Luau/Type.h" +#include "Luau/TypeFwd.h" +#include "Luau/VisitType.h" + +#include "lua.h" +#include "lualib.h" + +namespace Luau +{ + +static void checkStack(lua_State* L, int n) +{ + if (!lua_checkstack(L, n)) + luaL_error(L, "stack overflow while serializing type"); +} + +// Serializer for runtime types, modeled after AstSerialize but for TypeId/TypePackId +struct TypeSerialize final : public Luau::TypeVisitor +{ + lua_State* L; + int refsTableIndex; // Reference table for handling cyclic & shared type references. Maps TypeId/TypePackId (as lightuserdata) to the corresponding serialized table on the stack. + + explicit TypeSerialize(lua_State* L) + : Luau::TypeVisitor{"TypeSerialize", /*skipBoundTypes=*/false} + , L(L) + { + // Create the refs table and store a reference to it. This table will be used to track already serialized types to handle cycles and shared references. + lua_createtable(L, 0, 0); + refsTableIndex = lua_gettop(L); + } + + ~TypeSerialize() + { + // Pop the refs table from the stack when the serializer is destroyed + if (refsTableIndex > 0 && lua_gettop(L) >= refsTableIndex) + lua_remove(L, refsTableIndex); + else + luaL_error(L, "TypeSerialize: reference table index corrupted during serialization"); + } + + void cycle(TypeId ty) override + { + checkStack(L, 1); + lua_pushlightuserdata(L, (void*)ty); + lua_gettable(L, refsTableIndex); + + if (lua_isnil(L, -1)) + { + luaL_error(L, "TypeSerialize: cycle detected while serializing type, but TypeId %s was not found in refs table", Luau::toString(ty).data()); + } + } + + void cycle(TypePackId tp) override + { + checkStack(L, 1); + lua_pushlightuserdata(L, (void*)tp); + lua_gettable(L, refsTableIndex); + + if (lua_isnil(L, -1)) + { + luaL_error(L, "TypeSerialize: cycle detected while serializing type pack, but TypePackId %s was not found in refs table", Luau::toString(tp).data()); + } + } + + // Register this type's serialized table in the refs table, using the TypeId as the key. + // Called after creating the serialized table for a type, but before traversing any child types. + void registerType(TypeId ty) + { + checkStack(L, 3); + lua_pushlightuserdata(L, (void*)ty); // TypeId pointer as key + lua_pushvalue(L, -2); // Reference to the serialized type table as value + lua_rawset(L, refsTableIndex); // refs[ty] = serializedTable + } + + // Similar to registerType but for TypePackId + void registerTypePack(TypePackId tp) + { + checkStack(L, 3); + lua_pushlightuserdata(L, (void*)tp); + lua_pushvalue(L, -2); + lua_rawset(L, refsTableIndex); + } + + // Helper to push the "tag" field that every Luau type has + void pushTag(const char* tag) + { + lua_pushstring(L, tag); + lua_setfield(L, -2, "tag"); + } + + // Helper to push a Name, Property pair into the properties table + void pushProperty(const std::string& name, const Property& prop) + { + checkStack(L, 3); // 1 for key + 1 for value table + 1 for traverse/nil + + // push the Name string as the key for the property + lua_pushstring(L, name.c_str()); + + // Create property value table with read/write types + lua_createtable(L, 0, 2); + if (prop.readTy) + traverse(*prop.readTy); + else + lua_pushnil(L); + lua_setfield(L, -2, "read"); + + if (prop.writeTy) + traverse(*prop.writeTy); + else + lua_pushnil(L); + lua_setfield(L, -2, "write"); + + lua_settable(L, -3); + } + + // Helper to set the "properties" field for table and extern types + void setPropertiesFields(const std::map& props) + { + checkStack(L, 1); // 1 for properties table + lua_createtable(L, 0, props.size()); + + for (const auto& [name, prop] : props) + pushProperty(name, prop); + + lua_setfield(L, -2, "properties"); + } + + // Luau primitive type + // tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic" + void serialize(TypeId ty, const PrimitiveType& ptv) + { + checkStack(L, 2); // 1 for table + 1 for space to push/set remaining fields + lua_createtable(L, 0, 1); + registerType(ty); + + const char* tag = nullptr; + switch (ptv.type) + { + case PrimitiveType::NilType: + tag = "nil"; + break; + case PrimitiveType::Boolean: + tag = "boolean"; + break; + case PrimitiveType::Number: + tag = "number"; + break; + case PrimitiveType::String: + tag = "string"; + break; + case PrimitiveType::Thread: + tag = "thread"; + break; + case PrimitiveType::Buffer: + tag = "buffer"; + break; + default: + luaL_error(L, "unexpected primitive type"); + } + + pushTag(tag); + } + + void serialize(TypeId ty, const AnyType& atv) + { + checkStack(L, 2); + lua_createtable(L, 0, 1); + registerType(ty); + + pushTag("any"); + } + + void serialize(TypeId ty, const UnknownType& utv) + { + checkStack(L, 2); + lua_createtable(L, 0, 1); + registerType(ty); + + pushTag("unknown"); + } + + void serialize(TypeId ty, const NeverType& ntv) + { + checkStack(L, 2); + lua_createtable(L, 0, 1); + registerType(ty); + + pushTag("never"); + } + + // Luau singleton type: + // value: string | boolean | nil + void serialize(TypeId ty, const SingletonType& stv) + { + checkStack(L, 2); + lua_createtable(L, 0, 2); + registerType(ty); + + pushTag("singleton"); + + if (auto boolSingleton = get(&stv)) + lua_pushboolean(L, boolSingleton->value); + else if (auto strSingleton = get(&stv)) + lua_pushstring(L, strSingleton->value.c_str()); + else + lua_pushnil(L); + lua_setfield(L, -2, "value"); + } + + // Luau negation type: + // inner: type + void serialize(TypeId ty, const NegationType& ntv) + { + checkStack(L, 2); + lua_createtable(L, 0, 2); + registerType(ty); + + pushTag("negation"); + + traverse(ntv.ty); + lua_setfield(L, -2, "inner"); + } + + // Luau union type: + // components: { type } + void serialize(TypeId ty, const UnionType& utv) + { + checkStack(L, 3); // 1 for table + 1 for components table + 1 for traverse + lua_createtable(L, 0, 2); + registerType(ty); + + pushTag("union"); + + lua_createtable(L, utv.options.size(), 0); + for (size_t i = 0; i < utv.options.size(); i++) + { + traverse(utv.options[i]); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "components"); + } + + // Luau intersection type: + // components: { type } + void serialize(TypeId ty, const IntersectionType& itv) + { + checkStack(L, 3); // 1 for table + 1 for components table + 1 for traverse + lua_createtable(L, 0, 2); + registerType(ty); + + pushTag("intersection"); + + lua_createtable(L, itv.parts.size(), 0); + for (size_t i = 0; i < itv.parts.size(); i++) + { + traverse(itv.parts[i]); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "components"); + } + + // Luau generic type: + // name: string? + // ispack: boolean + void serialize(TypeId ty, const GenericType& gtv) + { + checkStack(L, 2); + lua_createtable(L, 0, 3); + registerType(ty); + + pushTag("generic"); + + if (!gtv.name.empty()) + lua_pushstring(L, gtv.name.c_str()); + else + lua_pushnil(L); + lua_setfield(L, -2, "name"); + + lua_pushboolean(L, false); // GenericType is not a pack + lua_setfield(L, -2, "ispack"); + } + + // Luau function type: + // parameters: { head: {type}?, tail: type? }, + // argnames: { string? }, + // returns: { head: {type}?, tail: type? }, + // generics: {type}, + // genericpacks: {typepack} + void serialize(TypeId ty, const FunctionType& ftv) + { + checkStack(L, 3); // max 1 for table + 1 for subtable + 1 for traverse + lua_createtable(L, 0, 6); + registerType(ty); + + pushTag("function"); + + // Parameters + traverse(ftv.argTypes); + lua_setfield(L, -2, "parameters"); + + // ArgNames + lua_createtable(L, ftv.argNames.size(), 0); + for (size_t i = 0; i < ftv.argNames.size(); i++) + { + if (!ftv.argNames[i].has_value()) + lua_pushnil(L); + else + lua_pushstring(L, ftv.argNames[i]->name.c_str()); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "argnames"); + + // Returns + traverse(ftv.retTypes); + lua_setfield(L, -2, "returns"); + + // Generics + lua_createtable(L, ftv.generics.size(), 0); + for (size_t i = 0; i < ftv.generics.size(); i++) + { + traverse(ftv.generics[i]); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "generics"); + + // Generic packs + lua_createtable(L, ftv.genericPacks.size(), 0); + for (size_t i = 0; i < ftv.genericPacks.size(); i++) + { + traverse(ftv.genericPacks[i]); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "genericpacks"); + } + + // Luau table type: + // properties: { string: { read: type?, write: type? } }, + // indexer: { index: type, readresult: type, writeresult: type }?, + // [TODO] readindexer: { index: type, result: type } }?, + // [TODO] writeindexer: { index: type, result: type } }?, + void serialize(TypeId ty, const TableType& ttv) + { + checkStack(L, 2); + lua_createtable(L, 0, 3); + registerType(ty); + + pushTag("table"); + + // Properties + if (!ttv.props.empty()) + { + setPropertiesFields(ttv.props); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "properties"); + } + + // Indexer + if (ttv.indexer) + { + lua_createtable(L, 0, 3); + traverse(ttv.indexer->indexType); + lua_setfield(L, -2, "index"); + + lua_setfield(L, -2, "indexer"); + + // TODO: implement readindexer and writeindexer from ttv.indexer->indexResultType + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "indexer"); + } + } + + // Luau metatable type: + // same as table type, but with an additional field: + // metatable: type? + void serialize(TypeId ty, const MetatableType& mtv) + { + traverse(mtv.table); + registerType(ty); + + traverse(mtv.metatable); + lua_setfield(L, -2, "metatable"); + } + + // Luau extern type: + // 'properties', 'metatable' are shared with table type + // parent: type? + void serialize(TypeId ty, const ExternType& etv) + { + checkStack(L, 2); + lua_createtable(L, 0, 4); + registerType(ty); + + pushTag("extern"); + + // Properties + if (!etv.props.empty()) + { + setPropertiesFields(etv.props); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "properties"); + } + + // Parent + if (etv.parent) + traverse(*etv.parent); + else + lua_pushnil(L); + lua_setfield(L, -2, "parent"); + + // Metatable + if (etv.metatable) + traverse(*etv.metatable); + else + lua_pushnil(L); + lua_setfield(L, -2, "metatable"); + } + + // Luau TypePack is + // head: {type}? + // tail: typepack? + void serialize(TypePackId tp, const TypePack& pack) + { + checkStack(L, 3); // 1 for root table + 1 for subtable + 1 for traverse + lua_createtable(L, 0, 3); + registerTypePack(tp); + + pushTag("typepack"); + + // Head + if (!pack.head.empty()) + { + lua_createtable(L, int(pack.head.size()), 0); + for (size_t i = 0; i < pack.head.size(); i++) + { + traverse(pack.head[i]); + lua_rawseti(L, -2, int(i + 1)); + } + } + else + { + lua_pushnil(L); + } + lua_setfield(L, -2, "head"); + + // Tail + if (pack.tail) + traverse(*pack.tail); + else + lua_pushnil(L); + lua_setfield(L, -2, "tail"); + } + + // Luau VariadicTypePack is + // type: type + // hidden: boolean + void serialize(TypePackId tp, const VariadicTypePack& vtp) + { + checkStack(L, 2); + lua_createtable(L, 0, 3); + registerTypePack(tp); + + pushTag("variadic"); + + traverse(vtp.ty); + lua_setfield(L, -2, "type"); + lua_pushboolean(L, vtp.hidden); + lua_setfield(L, -2, "hidden"); + } + + // Luau GenericTypePack is + // name: string? + // ispack: boolean + void serialize(TypePackId tp, const GenericTypePack& gtp) + { + checkStack(L, 2); + lua_createtable(L, 0, 3); + registerTypePack(tp); + + pushTag("generic"); + + if (!gtp.name.empty()) + lua_pushstring(L, gtp.name.c_str()); + else + lua_pushnil(L); + lua_setfield(L, -2, "name"); + + lua_pushboolean(L, true); // GenericTypePack is a pack + lua_setfield(L, -2, "ispack"); + } + + // Luau Type visitors + bool visit(TypeId ty, const PrimitiveType& ptv) override + { + serialize(ty, ptv); + return false; + } + + bool visit(TypeId ty, const AnyType& atv) override + { + serialize(ty, atv); + return false; + } + + bool visit(TypeId ty, const UnknownType& utv) override + { + serialize(ty, utv); + return false; + } + + bool visit(TypeId ty, const NeverType& ntv) override + { + serialize(ty, ntv); + return false; + } + + bool visit(TypeId ty, const SingletonType& stv) override + { + serialize(ty, stv); + return false; + } + + bool visit(TypeId ty, const NegationType& ntv) override + { + serialize(ty, ntv); + return false; + } + + bool visit(TypeId ty, const UnionType& utv) override + { + serialize(ty, utv); + return false; + } + + bool visit(TypeId ty, const IntersectionType& itv) override + { + serialize(ty, itv); + return false; + } + + bool visit(TypeId ty, const GenericType& gtv) override + { + serialize(ty, gtv); + return false; + } + + bool visit(TypeId ty, const FunctionType& ftv) override + { + serialize(ty, ftv); + return false; + } + + bool visit(TypeId ty, const TableType& ttv) override + { + serialize(ty, ttv); + return false; + } + + bool visit(TypeId ty, const MetatableType& mtv) override + { + serialize(ty, mtv); + return false; + } + + bool visit(TypeId ty, const ExternType& etv) override + { + serialize(ty, etv); + return false; + } + + // Luau TypePack visitors + bool visit(TypePackId tp, const TypePack& pack) override + { + serialize(tp, pack); + return false; + } + + bool visit(TypePackId tp, const VariadicTypePack& vtp) override + { + serialize(tp, vtp); + return false; + } + + bool visit(TypePackId tp, const GenericTypePack& gtp) override + { + serialize(tp, gtp); + return false; + } + + // Non-Serialized Types + + bool visit(TypeId ty) override + { + // NOTE: `TypeSerialize` should explicitly visit _all_ types and type packs, + // otherwise it's prone to serializing types that should not be serialized. + LUTE_ASSERT(false); + LUTE_UNREACHABLE(); + } + + bool visit(TypeId ty, const BoundType& btv) override + { + luaL_error(L, "TypeSerialize: cannot serialize BoundType"); + return false; + } + + bool visit(TypeId ty, const FreeType& ftv) override + { + luaL_error(L, "TypeSerialize: cannot serialize FreeType"); + return false; + } + + bool visit(TypeId ty, const ErrorType& etv) override + { + luaL_error(L, "TypeSerialize: cannot serialize ErrorType"); + return false; + } + + bool visit(TypeId ty, const NoRefineType& nrt) override + { + luaL_error(L, "TypeSerialize: cannot serialize NoRefineType"); + return false; + } + + bool visit(TypeId ty, const BlockedType& btv) override + { + luaL_error(L, "TypeSerialize: cannot serialize BlockedType"); + return false; + } + + bool visit(TypeId ty, const PendingExpansionType& petv) override + { + luaL_error(L, "TypeSerialize: cannot serialize PendingExpansionType"); + return false; + } + + bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override + { + luaL_error(L, "TypeSerialize: cannot serialize TypeFunctionInstanceType"); + return false; + } + + bool visit(TypePackId tp) override + { + // NOTE: `TypeSerialize` should explicitly visit _all_ types and type packs, + // otherwise it's prone to serializing type packs that should not be serialized. + LUTE_ASSERT(false); + LUTE_UNREACHABLE(); + } + + bool visit(TypePackId tp, const BoundTypePack& btp) override + { + luaL_error(L, "TypeSerialize: cannot serialize BoundTypePack"); + return false; + } + + bool visit(TypePackId tp, const FreeTypePack& ftp) override + { + luaL_error(L, "TypeSerialize: cannot serialize FreeTypePack"); + return false; + } + + bool visit(TypePackId tp, const ErrorTypePack& etp) override + { + luaL_error(L, "TypeSerialize: cannot serialize ErrorTypePack"); + return false; + } + + bool visit(TypePackId tp, const BlockedTypePack& btp) override + { + luaL_error(L, "TypeSerialize: cannot serialize BlockedTypePack"); + return false; + } + + bool visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp) override + { + luaL_error(L, "TypeSerialize: cannot serialize TypeFunctionInstanceTypePack"); + return false; + } +}; + +int serializeType(lua_State* L, TypeId ty) +{ + TypeSerialize serializer(L); + serializer.traverse(ty); + lua_setreadonly(L, -1, 1); + return 1; +} + +int serializeTypePack(lua_State* L, TypePackId tp) +{ + TypeSerialize serializer(L); + serializer.traverse(tp); + lua_setreadonly(L, -1, 1); + return 1; +} + +} // namespace Luau diff --git a/lute/net/CMakeLists.txt b/lute/net/CMakeLists.txt index 2b9a9218f..82eee80a2 100644 --- a/lute/net/CMakeLists.txt +++ b/lute/net/CMakeLists.txt @@ -3,10 +3,18 @@ add_library(Lute.Net STATIC) target_sources(Lute.Net PRIVATE include/lute/net.h + src/client.cpp + src/httpclient.cpp src/net.cpp + src/system_ca.cpp + src/system_ca.h + src/wsclient.cpp + src/wscommon.cpp + src/wscommon.h + src/server.cpp ) target_compile_features(Lute.Net PUBLIC cxx_std_17) target_include_directories(Lute.Net PUBLIC "include" ${LIBUV_INCLUDE_DIR} ${UWEBSOCKETS_INCLUDE_DIR}) -target_link_libraries(Lute.Net PRIVATE Lute.Runtime Luau.VM uv_a libcurl uWS zlibstatic) +target_link_libraries(Lute.Net PRIVATE Lute.Common Lute.Runtime Luau.VM uv_a libcurl uWS zlibstatic) target_compile_options(Lute.Net PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/net/include/lute/net.h b/lute/net/include/lute/net.h index 04a4fde17..4d5a933d8 100644 --- a/lute/net/include/lute/net.h +++ b/lute/net/include/lute/net.h @@ -1,24 +1,38 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" // open the library as a standard global luau library -int luaopen_net(lua_State* L); +LUTE_API int luaopen_net(lua_State* L); // open the library as a table on top of the stack -int luteopen_net(lua_State* L); - -namespace net -{ +LUTE_API int luteopen_net(lua_State* L); -int request(lua_State* L); +LUTE_API int luteopen_net_client(lua_State* L); +LUTE_API int luteopen_net_server(lua_State* L); -int lua_serve(lua_State* L); +struct NetClient : LuteLibrary +{ + static constexpr const char kName[] = "net.client"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; +}; -static const luaL_Reg lib[] = { - {"request", request}, - {"serve", lua_serve}, - {nullptr, nullptr}, +struct NetServer : LuteLibrary +{ + static constexpr const char kName[] = "net.server"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; -} // namespace net +struct Net : LuteLibrary +{ + static constexpr const char kName[] = "net"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; +}; diff --git a/lute/net/src/client.cpp b/lute/net/src/client.cpp new file mode 100644 index 000000000..63a8b5553 --- /dev/null +++ b/lute/net/src/client.cpp @@ -0,0 +1,119 @@ +#include "lute/common.h" +#include "lute/net.h" +#include "lute/userdatas.h" + +#include "lua.h" +#include "lualib.h" + +#include "curl/curl.h" + +#include +#include + +namespace net::client +{ +struct WebSocketHandle; +int request(lua_State* L); +int websocket(lua_State* L); +int ws_send(lua_State* L); +int ws_close(lua_State* L); +} // namespace net::client + +namespace +{ +struct CurlHolder +{ + CurlHolder() + { + curl_global_init(CURL_GLOBAL_DEFAULT); + } + + ~CurlHolder() + { + curl_global_cleanup(); + } +}; + +static CurlHolder& globalCurlInit() +{ + static CurlHolder holder; + return holder; +} + +static void initializeNetClient(lua_State* L) +{ + luaL_newmetatable(L, "WebSocketHandle"); + + lua_pushcfunction( + L, + [](lua_State* L) + { + const char* index = luaL_checkstring(L, -1); + + if (strcmp(index, "send") == 0) + { + lua_pushcfunction(L, net::client::ws_send, "WebSocketHandle.send"); + return 1; + } + + if (strcmp(index, "close") == 0) + { + lua_pushcfunction(L, net::client::ws_close, "WebSocketHandle.close"); + return 1; + } + + return 0; + }, + "WebSocketHandle.__index" + ); + lua_setfield(L, -2, "__index"); + + lua_pushstring(L, "WebSocketHandle"); + lua_setfield(L, -2, "__type"); + + lua_setuserdatadtor( + L, + kWebSocketHandleTag, + [](lua_State*, void* ud) + { + std::destroy_at(static_cast*>(ud)); + } + ); + + lua_setuserdatametatable(L, kWebSocketHandleTag); +} +} // namespace + +const char* const NetClient::properties[] = {nullptr}; + +const luaL_Reg NetClient::lib[] = { + {"request", net::client::request}, + {"websocket", net::client::websocket}, + {nullptr, nullptr}, +}; + +int NetClient::pushLibrary(lua_State* L) +{ + globalCurlInit(); + initializeNetClient(L); + + lua_createtable(L, 0, std::size(NetClient::lib)); + + for (auto& [name, func] : NetClient::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return 1; +} + +LUTE_API int luteopen_net_client(lua_State* L) +{ + return NetClient::pushLibrary(L); +} diff --git a/lute/net/src/httpclient.cpp b/lute/net/src/httpclient.cpp new file mode 100644 index 000000000..989ca0123 --- /dev/null +++ b/lute/net/src/httpclient.cpp @@ -0,0 +1,724 @@ +#include "lute/common.h" +#include "lute/runtime.h" + +#include "Luau/DenseHash.h" + +#include "lua.h" +#include "lualib.h" + +#include "curl/curl.h" + +#include +#include +#include +#include +#include +#include + +#include "system_ca.h" + +namespace net::client +{ + +static const std::string kEmptyHeaderKey = ""; +static constexpr long kDefaultRequestTimeoutMs = 5 * 60 * 1000; +static constexpr long kDefaultConnectTimeoutMs = 30 * 1000; + +struct CurlResponse +{ + std::vector body; + Luau::DenseHashMap headers; + long status = 0; + + CurlResponse() + : headers(kEmptyHeaderKey) + { + } +}; + +struct CurlMultiManager; + +struct HttpRequestState +{ + CURL* easy = nullptr; + curl_slist* headerList = nullptr; + std::string url; + std::string method; + std::string body; + CurlResponse response; + char errorBuffer[CURL_ERROR_SIZE] = {}; + ResumeToken token; + + ~HttpRequestState() + { + if (headerList) + curl_slist_free_all(headerList); + + if (easy) + curl_easy_cleanup(easy); + } +}; + +struct CurlSocketState +{ + curl_socket_t socket = CURL_SOCKET_BAD; + uv_poll_t poll{}; + CurlMultiManager* manager = nullptr; + int activeEvents = 0; + bool closing = false; +}; + +static size_t writeFunction(char* ptr, size_t size, size_t nmemb, void* userdata) +{ + auto* state = static_cast(userdata); + LUTE_ASSERT(state); + + std::vector* target = &state->response.body; + LUTE_ASSERT(target); + + size_t fullsize = size * nmemb; + target->insert(target->end(), ptr, ptr + fullsize); + return fullsize; +} + +static void collectResponseHeaders(HttpRequestState& state) +{ + curl_header* prev = nullptr; + curl_header* h; + + while ((h = curl_easy_nextheader(state.easy, CURLH_HEADER, -1, prev))) + { + std::string name = h->name; + std::string value = h->value; + + if (state.response.headers.contains(name)) + { + state.response.headers[name] += ", " + value; + } + else + { + state.response.headers[name] = value; + } + prev = h; + } +} + +static int pushResponse(lua_State* L, CurlResponse resp) +{ + lua_createtable(L, 0, 4); + + lua_pushstring(L, "body"); + lua_pushlstring(L, resp.body.empty() ? "" : resp.body.data(), resp.body.size()); + lua_settable(L, -3); + + lua_pushstring(L, "headers"); + lua_createtable(L, 0, resp.headers.size()); + for (const auto& header : resp.headers) + { + lua_pushlstring(L, header.first.data(), header.first.size()); + lua_pushlstring(L, header.second.data(), header.second.size()); + lua_settable(L, -3); + } + lua_settable(L, -3); + + lua_pushstring(L, "status"); + lua_pushinteger(L, resp.status); + lua_settable(L, -3); + + lua_pushstring(L, "ok"); + lua_pushboolean(L, (resp.status >= 200 && resp.status < 300)); + lua_settable(L, -3); + + return 1; +} + +struct CurlMultiManager +{ + explicit CurlMultiManager(Runtime* runtime) + : runtime(runtime) + { + multi = curl_multi_init(); + if (!multi) + { + initError = "failed to initialize curl multi handle"; + return; + } + + int timerResult = uv_timer_init(runtime->getEventLoop(), &timer); + if (timerResult != 0) + { + initError = std::string("failed to initialize curl timer: ") + uv_strerror(timerResult); + return; + } + + timerInitialized = true; + timer.data = this; + + curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, &CurlMultiManager::socketCallback); + curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, this); + curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, &CurlMultiManager::timerCallback); + curl_multi_setopt(multi, CURLMOPT_TIMERDATA, this); + } + + Runtime* runtime = nullptr; + CURLM* multi = nullptr; + uv_timer_t timer{}; + bool timerInitialized = false; + bool timerClosing = false; + bool closing = false; + bool deleteWhenClosed = false; + int pendingCloseHandles = 0; + std::string initError; + std::unordered_map sockets; + std::unordered_map> requests; + + bool ok() const + { + return initError.empty(); + } + + bool addRequest(std::unique_ptr state, std::string& error) + { + if (closing || !multi) + { + error = "curl multi manager is closing"; + return false; + } + + CURL* easy = state->easy; + requests[easy] = std::move(state); + + CURLMcode result = curl_multi_add_handle(multi, easy); + if (result != CURLM_OK) + { + auto it = requests.find(easy); + if (it != requests.end()) + requests.erase(it); + + error = curl_multi_strerror(result); + return false; + } + + return true; + } + + void socketAction(curl_socket_t socket, int action) + { + if (closing || !multi) + return; + + int runningHandles = 0; + CURLMcode result = curl_multi_socket_action(multi, socket, action, &runningHandles); + if (result != CURLM_OK) + { + failAll(std::string("curl multi socket action failed: ") + curl_multi_strerror(result)); + return; + } + + drainCompletions(); + } + + void drainCompletions() + { + if (closing || !multi) + return; + + int messagesLeft = 0; + CURLMsg* message = nullptr; + while (!closing && multi && (message = curl_multi_info_read(multi, &messagesLeft))) + { + if (message->msg != CURLMSG_DONE) + continue; + + completeRequest(message->easy_handle, message->data.result); + } + } + + void completeRequest(CURL* easy, CURLcode result) + { + auto it = requests.find(easy); + if (it == requests.end()) + return; + + curl_multi_remove_handle(multi, easy); + + std::unique_ptr state = std::move(it->second); + requests.erase(it); + + if (result != CURLE_OK) + { + std::string error = state->errorBuffer[0] != '\0' ? state->errorBuffer : curl_easy_strerror(result); + state->token->fail("network request failed: " + error); + } + else + { + curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &state->response.status); + collectResponseHeaders(*state); + + state->token->complete( + [resp = std::move(state->response)](lua_State* L) mutable + { + return pushResponse(L, std::move(resp)); + } + ); + } + + maybeDestroyWhenIdle(); + } + + CurlSocketState* createSocketState(curl_socket_t socket) + { + auto* state = new CurlSocketState(); + state->socket = socket; + state->manager = this; + state->poll.data = state; + + int result = uv_poll_init_socket(runtime->getEventLoop(), &state->poll, socket); + if (result != 0) + { + delete state; + return nullptr; + } + + sockets[socket] = state; + curl_multi_assign(multi, socket, state); + return state; + } + + bool updateSocket(CurlSocketState* state, int events) + { + if (!state || state->closing) + return false; + + if (state->activeEvents == events) + return true; + + if (events == 0) + { + uv_poll_stop(&state->poll); + state->activeEvents = 0; + return true; + } + + int result = uv_poll_start( + &state->poll, + events, + [](uv_poll_t* handle, int status, int events) + { + auto* state = static_cast(handle->data); + if (!state || state->closing || !state->manager) + return; + + int action = 0; + if (status < 0) + action = CURL_CSELECT_ERR; + else + { + if (events & UV_READABLE) + action |= CURL_CSELECT_IN; + if (events & UV_WRITABLE) + action |= CURL_CSELECT_OUT; + } + + state->manager->socketAction(state->socket, action); + } + ); + + if (result != 0) + return false; + + state->activeEvents = events; + return true; + } + + void closeSocket(CurlSocketState* state) + { + if (!state || state->closing) + return; + + state->closing = true; + sockets.erase(state->socket); + + if (multi) + curl_multi_assign(multi, state->socket, nullptr); + + uv_poll_stop(&state->poll); + pendingCloseHandles++; + uv_close( + reinterpret_cast(&state->poll), + [](uv_handle_t* handle) + { + auto* state = static_cast(handle->data); + CurlMultiManager* manager = state ? state->manager : nullptr; + delete state; + + if (manager) + manager->onHandleClosed(); + } + ); + } + + void failAll(std::string error) + { + if (multi) + { + for (auto& request : requests) + curl_multi_remove_handle(multi, request.first); + } + + std::vector> pending; + pending.reserve(requests.size()); + for (auto& request : requests) + pending.push_back(std::move(request.second)); + requests.clear(); + + for (auto& request : pending) + request->token->fail(error); + + maybeDestroyWhenIdle(); + } + + void beginClose(bool deleteAfterClose) + { + if (closing) + return; + + closing = true; + deleteWhenClosed = deleteAfterClose; + + if (!requests.empty()) + failAll("network request cancelled"); + + std::vector socketStates; + socketStates.reserve(sockets.size()); + for (auto& socket : sockets) + socketStates.push_back(socket.second); + + for (CurlSocketState* state : socketStates) + closeSocket(state); + + if (timerInitialized && !timerClosing) + { + uv_timer_stop(&timer); + timerClosing = true; + pendingCloseHandles++; + uv_close( + reinterpret_cast(&timer), + [](uv_handle_t* handle) + { + auto* manager = static_cast(handle->data); + if (manager) + manager->onHandleClosed(); + } + ); + } + + if (multi) + { + curl_multi_cleanup(multi); + multi = nullptr; + } + + finishCloseIfReady(); + } + + void onHandleClosed() + { + pendingCloseHandles--; + finishCloseIfReady(); + } + + void finishCloseIfReady() + { + if (deleteWhenClosed && pendingCloseHandles == 0) + delete this; + } + + void maybeDestroyWhenIdle(); + + static int socketCallback(CURL*, curl_socket_t socket, int what, void* userp, void* socketp) + { + auto* manager = static_cast(userp); + if (!manager || manager->closing) + return 0; + + auto* state = static_cast(socketp); + + if (what == CURL_POLL_REMOVE) + { + manager->closeSocket(state); + return 0; + } + + if (!state) + { + state = manager->createSocketState(socket); + if (!state) + return -1; + } + + int events = 0; + if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) + events |= UV_READABLE; + if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) + events |= UV_WRITABLE; + + return manager->updateSocket(state, events) ? 0 : -1; + } + + static int timerCallback(CURLM*, long timeoutMs, void* userp) + { + auto* manager = static_cast(userp); + if (!manager || manager->closing || !manager->timerInitialized) + return 0; + + if (timeoutMs < 0) + { + uv_timer_stop(&manager->timer); + return 0; + } + + uint64_t delay = timeoutMs == 0 ? 0 : static_cast(timeoutMs); + uv_timer_start( + &manager->timer, + [](uv_timer_t* handle) + { + auto* manager = static_cast(handle->data); + if (manager) + manager->socketAction(CURL_SOCKET_TIMEOUT, 0); + }, + delay, + 0 + ); + + return 0; + } +}; + +static std::unordered_map curlManagers; + +void CurlMultiManager::maybeDestroyWhenIdle() +{ + if (!requests.empty()) + return; + + auto it = curlManagers.find(runtime); + if (it != curlManagers.end() && it->second == this) + curlManagers.erase(it); + + beginClose(true); +} + +static CurlMultiManager* getCurlMultiManager(Runtime* runtime, std::string& error) +{ + auto it = curlManagers.find(runtime); + if (it != curlManagers.end()) + return it->second; + + auto* manager = new CurlMultiManager(runtime); + if (!manager->ok()) + { + error = manager->initError; + manager->beginClose(true); + return nullptr; + } + + curlManagers[runtime] = manager; + return manager; +} + +static bool isValidHeaderNameChar(unsigned char c) +{ + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || + c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || + c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; +} + +static bool isValidHeaderName(const std::string& name) +{ + if (name.empty()) + return false; + + for (unsigned char c : name) + { + if (!isValidHeaderNameChar(c)) + return false; + } + + return true; +} + +static bool isValidHeaderValue(const std::string& value) +{ + for (unsigned char c : value) + { + if (c == '\t') + continue; + + if (c >= 0x20 && c <= 0x7e) + continue; + + return false; + } + + return true; +} + +static std::unique_ptr createRequestState( + std::string url, + std::string method, + std::string body, + std::vector> headers, + ResumeToken token, + std::string& error +) +{ + CURL* curl = curl_easy_init(); + if (!curl) + { + error = "failed to initialize curl easy handle"; + return nullptr; + } + + auto state = std::make_unique(); + state->easy = curl; + state->url = std::move(url); + state->method = std::move(method); + state->body = std::move(body); + state->token = std::move(token); + + curl_easy_setopt(curl, CURLOPT_URL, state->url.c_str()); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, state->errorBuffer); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, state.get()); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, kDefaultRequestTimeoutMs); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, kDefaultConnectTimeoutMs); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 20L); + applySystemCA(curl); + + if (state->method == "HEAD") + { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } + + if (state->method != "GET") + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, state->method.c_str()); + + if (!state->body.empty()) + { + auto maxCurlOff = (std::numeric_limits::max)(); + if (state->body.size() > static_cast(maxCurlOff)) + { + error = "request body is too large"; + return nullptr; + } + + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, state->body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(state->body.size())); + } + + for (const auto& headerPair : headers) + { + std::string headerString = headerPair.first + ": " + headerPair.second; + curl_slist* next = curl_slist_append(state->headerList, headerString.c_str()); + if (!next) + { + error = "failed to append request header"; + return nullptr; + } + state->headerList = next; + } + + if (state->headerList) + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, state->headerList); + + return state; +} + +int request(lua_State* L) +{ + std::string url = luaL_checkstring(L, 1); + std::string method = "GET"; + std::string body = ""; + std::vector> headers; + + if (lua_istable(L, 2)) + { + lua_getfield(L, 2, "method"); + if (lua_isstring(L, -1)) + method = lua_tostring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 2, "body"); + if (lua_isstring(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"); + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + if (lua_type(L, -2) == LUA_TSTRING && lua_isstring(L, -1)) + { + size_t keyLen = 0; + size_t valueLen = 0; + const char* keyData = lua_tolstring(L, -2, &keyLen); + const char* valueData = lua_tolstring(L, -1, &valueLen); + std::string key(keyData, keyLen); + std::string value(valueData, valueLen); + headers.emplace_back(key, value); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + + if ((method == "GET" || method == "HEAD") && !body.empty()) + luaL_error(L, "%s requests cannot include a body", method.c_str()); + + for (const auto& header : headers) + { + if (!isValidHeaderName(header.first)) + luaL_error(L, "invalid request header name"); + + if (!isValidHeaderValue(header.second)) + luaL_error(L, "invalid request header value"); + } + + auto token = getResumeToken(L); + + std::string error; + std::unique_ptr state = + createRequestState(std::move(url), std::move(method), std::move(body), std::move(headers), token, error); + + if (!state) + { + token->fail("network request failed: " + error); + return lua_yield(L, 0); + } + + CurlMultiManager* manager = getCurlMultiManager(token->runtime, error); + if (!manager) + { + token->fail("network request failed: " + error); + return lua_yield(L, 0); + } + + if (!manager->addRequest(std::move(state), error)) + { + token->fail("network request failed: " + error); + return lua_yield(L, 0); + } + + return lua_yield(L, 0); +} + +} // namespace net::client diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 6f8faa077..24fbc2e30 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -1,734 +1,38 @@ #include "lute/net.h" -#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 -#include -#include - -namespace net -{ - -static const std::string kEmptyHeaderKey = ""; -struct CurlResponse { - std::string error; - std::vector body; - Luau::DenseHashMap headers; - long status = 0; - - CurlResponse() : headers(kEmptyHeaderKey) {} -}; - -static size_t writeFunction(void* contents, size_t size, size_t nmemb, void* context) -{ - std::vector& target = *(std::vector*)context; - size_t fullsize = size * nmemb; - - target.insert(target.end(), (char*)contents, (char*)contents + fullsize); - - return fullsize; -} - -static CurlResponse requestData( - const std::string& url, - const std::string& method, - const std::string& body, - const std::vector>& headers -) -{ - CURL* curl = curl_easy_init(); - CurlResponse resp; - if (!curl) - { - resp.error = "failed to initialize"; - return resp; - } - - std::vector data; - std::vector headerData; - curl_slist* headerList = nullptr; - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); - - if (method != "GET") - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); - - if (!body.empty()) - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - - if (!headers.empty()) - { - for (const auto& header_pair : headers) - { - std::string header_str = header_pair.first + ": " + header_pair.second; - headerList = curl_slist_append(headerList, header_str.c_str()); - } - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); - } - - CURLcode res = curl_easy_perform(curl); - - if (headerList) - curl_slist_free_all(headerList); - - if (res != CURLE_OK) - { - resp.error = curl_easy_strerror(res); - curl_easy_cleanup(curl); - return resp; - } - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp.status); - - resp.body = std::move(data); - - curl_header* prev = nullptr; - curl_header* h; - - while ((h = curl_easy_nextheader(curl, CURLH_HEADER, 0, prev))) - { - std::string name = h->name; - std::string value = h->value; - - if (resp.headers.contains(name)) - { - resp.headers[name] += ", " + value; - } - else - { - resp.headers[name] = value; - } - prev = h; - } - - curl_easy_cleanup(curl); - return resp; -} - -int request(lua_State* L) -{ - std::string url = luaL_checkstring(L, 1); - std::string method = "GET"; - std::string body = ""; - std::vector> headers; - if (lua_istable(L, 2)) - { - lua_getfield(L, 2, "method"); - if (lua_isstring(L, -1)) - method = lua_tostring(L, -1); - lua_pop(L, 1); +#include - lua_getfield(L, 2, "body"); - if (lua_isstring(L, -1)) - body = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 2, "headers"); - if (lua_istable(L, -1)) - { - lua_pushnil(L); - while (lua_next(L, -2)) - { - if (lua_isstring(L, -2) && lua_isstring(L, -1)) - { - std::string key = lua_tostring(L, -2); - std::string value = lua_tostring(L, -1); - headers.emplace_back(key, value); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - } - - auto token = getResumeToken(L); - - // TODO: add cancellations - token->runtime->runInWorkQueue( - [=] - { - CurlResponse resp = requestData(url, method, body, headers); - if (!resp.error.empty()) - { - token->fail("network request failed: " + resp.error); - return; - } - - token->complete( - [resp = std::move(resp)](lua_State* L) - { - lua_createtable(L, 0, 4); - - lua_pushstring(L, "body"); - lua_pushlstring(L, resp.body.data(), resp.body.size()); - lua_settable(L, -3); - - lua_pushstring(L, "headers"); - lua_createtable(L, 0, resp.headers.size()); - for (const auto& header : resp.headers) - { - lua_pushlstring(L, header.first.data(), header.first.size()); - lua_pushlstring(L, header.second.data(), header.second.size()); - lua_settable(L, -3); - } - lua_settable(L, -3); - - lua_pushstring(L, "status"); - lua_pushinteger(L, resp.status); - lua_settable(L, -3); - - lua_pushstring(L, "ok"); - lua_pushboolean(L, (resp.status >= 200 && resp.status < 300)); - lua_settable(L, -3); - - return 1; - } - ); - } - ); - - return lua_yield(L, 0); -} - -using uWSApp = Luau::Variant, std::unique_ptr>; - -static const int kEmptyServerKey = 0; -static Luau::DenseHashMap serverInstances(kEmptyServerKey); -static Luau::DenseHashMap> serverStates(kEmptyServerKey); -static int nextServerId = 1; - -struct ServerLoopState -{ - Luau::Variant app; - Runtime* runtime; - bool running = true; - std::function loopFunction; - std::shared_ptr handlerRef; - std::string hostname; - int port; - bool reusePort = false; +const luaL_Reg Net::lib[] = { + {nullptr, nullptr}, }; -static void parseQuery(const std::string_view& query, lua_State* L) -{ - lua_createtable(L, 0, 0); - size_t start = 1; // Skip the '?' - size_t end = query.find('&'); - while (end != std::string::npos) - { - std::string_view pair = std::string_view(query.data() + start, end - start); - size_t eq = pair.find('='); - if (eq != std::string::npos) - { - std::string_view key = std::string_view(pair.data(), eq); - std::string_view value = uWS::getDecodedQueryValue(key, query); - lua_pushlstring(L, key.data(), key.size()); - lua_pushlstring(L, value.data(), value.size()); - lua_settable(L, -3); - } - start = end + 1; - end = query.find('&', start); - } - std::string_view pair = std::string_view(query.data() + start, query.size()); - size_t eq = pair.find('='); - if (eq != std::string::npos) - { - std::string_view key = std::string_view(pair.data(), eq); - std::string_view value = uWS::getDecodedQueryValue(key, query); - lua_pushlstring(L, key.data(), key.size()); - lua_pushlstring(L, value.data(), value.size()); - lua_settable(L, -3); - } -} +const char* const Net::properties[] = {"client", "server"}; -static void parseHeaders(auto* req, lua_State* L) +int Net::pushLibrary(lua_State* L) { - lua_createtable(L, 0, 0); - for (const auto& header : *req) - { - lua_pushlstring(L, header.first.data(), header.first.size()); - lua_pushlstring(L, header.second.data(), header.second.size()); - lua_settable(L, -3); - } -} - -static void handleResponse(auto* res, lua_State* L, int responseIndex) -{ - // Check if the response is a string or a table - if (lua_isstring(L, responseIndex)) - { - std::string body = lua_tostring(L, responseIndex); - res->writeStatus("200 OK"); - res->writeHeader("Content-Type", "text/html"); - res->end(body); - return; - } - - if (!lua_istable(L, responseIndex)) - { - res->writeStatus("500 Internal Server Error"); - res->end("Handler must return a string or a response table"); - return; - } - - - lua_getfield(L, responseIndex, "status"); - int status = lua_isnumber(L, -1) ? lua_tointeger(L, -1) : 200; - lua_pop(L, 1); - - std::string statusText; - switch (status) - { - case 200: - statusText = "200 OK"; - break; - case 201: - statusText = "201 Created"; - break; - case 204: - statusText = "204 No Content"; - break; - case 400: - statusText = "400 Bad Request"; - break; - case 401: - statusText = "401 Unauthorized"; - break; - case 403: - statusText = "403 Forbidden"; - break; - case 404: - statusText = "404 Not Found"; - break; - case 500: - statusText = "500 Internal Server Error"; - break; - default: - statusText = std::to_string(status) + " Status"; - break; - } - res->writeStatus(statusText); - - lua_getfield(L, responseIndex, "headers"); - if (lua_istable(L, -1)) - { - lua_pushnil(L); - while (lua_next(L, -2)) - { - if (lua_isstring(L, -2) && lua_isstring(L, -1)) - { - std::string headerName = lua_tostring(L, -2); - std::string headerValue = lua_tostring(L, -1); - res->writeHeader(headerName, headerValue); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - - lua_getfield(L, responseIndex, "body"); - std::string body = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; - lua_pop(L, 1); - - res->end(body); -} - -static void processRequest( - std::shared_ptr state, - auto* res, - auto* req, - const std::string& method, - const std::string_view& path, - const std::string_view& query, - const std::string_view& body -) -{ - lua_State* L = lua_newthread(state->runtime->GL); - luaL_sandboxthread(L); - std::shared_ptr threadRef = getRefForThread(L); - lua_pop(state->runtime->GL, 1); - - lua_createtable(L, 0, 5); - - lua_pushstring(L, "method"); - lua_pushstring(L, method.c_str()); - lua_settable(L, -3); - - lua_pushstring(L, "path"); - lua_pushlstring(L, path.data(), path.size()); - lua_settable(L, -3); - - lua_pushstring(L, "query"); - parseQuery(query, L); - lua_settable(L, -3); + lua_createtable(L, 0, std::size(Net::properties)); - lua_pushstring(L, "headers"); - parseHeaders(req, L); - lua_settable(L, -3); + NetClient::verifyInterface(); + NetClient::pushLibrary(L); + lua_setfield(L, -2, "client"); - lua_pushstring(L, "body"); - lua_pushlstring(L, body.data(), body.size()); - lua_settable(L, -3); + NetServer::verifyInterface(); + NetServer::pushLibrary(L); + lua_setfield(L, -2, "server"); - state->handlerRef->push(L); - - lua_pushvalue(L, -2); - lua_remove(L, -3); - - int status = lua_resume(L, nullptr, 1); - if (status != LUA_OK && status != LUA_YIELD) - { - std::string error = lua_tostring(L, -1); - lua_pop(L, 1); - - res->writeStatus("500 Internal Server Error"); - res->end("Server error: " + error); - return; - } - - handleResponse(res, L, -1); - - lua_pop(L, 1); -} - -void setupAppAndListen(auto* app, std::shared_ptr state, bool& success) -{ - app->any( - "/*", - [state](auto* res, auto* req) - { - std::string method = std::string(req->getMethod()); - std::transform(method.begin(), method.end(), method.begin(), ::toupper); - std::string_view url = req->getFullUrl(); - std::string_view path = url; - - // Split URL into path and query - size_t queryPos = url.find('?'); - std::string query; - if (queryPos != std::string::npos) - { - path = std::string_view(url.data(), queryPos); - query = std::string_view(url.data() + queryPos, url.size() - queryPos); - } - - res->onAborted( - []() - { - // TODO: handle aborted requests - } - ); - - std::unique_ptr bodyBuffer; - res->onData( - [state, res, req, method, path, query, bodyBuffer = std::move(bodyBuffer)](std::string_view data, bool last) mutable - { - if (last) - { - if (bodyBuffer.get()) - { - bodyBuffer->append(data); - processRequest(state, res, req, method, path, query, *bodyBuffer); - } - else - { - processRequest(state, res, req, method, path, query, data); - } - } - else - { - if (bodyBuffer.get()) - { - bodyBuffer->append(data); - } - else - { - bodyBuffer = std::make_unique(data); - } - } - } - ); - } - ); - - int options = state->reusePort ? LIBUS_LISTEN_DEFAULT : LIBUS_LISTEN_EXCLUSIVE_PORT; - - app->listen( - state->hostname, - state->port, - options, - [&success](auto* listen_socket) - { - success = (listen_socket != nullptr); - } - ); -} - -bool closeServer(int serverId) -{ - if (!serverInstances.contains(serverId) || !serverStates.contains(serverId)) - { - return false; - } - - Luau::visit( - [](auto* appPtr) - { - if (appPtr) - appPtr->close(); - }, - serverStates[serverId]->app - ); - serverStates[serverId]->running = false; - - Luau::visit( - [](auto& ptr) - { - if (ptr) - ptr.reset(); - }, - serverInstances[serverId] - ); - serverStates[serverId] = nullptr; - - return true; -} - -int lua_serve(lua_State* L) -{ - std::string hostname = "0.0.0.0"; - int port = 3000; - bool reusePort = false; - std::optional tlsOptions; - int handlerIndex = 1; - - // Check if first argument is a table (config) or function (handler) - if (lua_istable(L, 1)) - { - lua_getfield(L, 1, "hostname"); - if (lua_isstring(L, -1)) - { - hostname = lua_tostring(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "port"); - if (lua_isnumber(L, -1)) - { - port = lua_tointeger(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "reuseport"); - if (lua_isboolean(L, -1)) - { - reusePort = lua_toboolean(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "tls"); - if (lua_istable(L, -1)) - { - tlsOptions.emplace(); - - lua_getfield(L, -1, "certfilename"); - if (!lua_isstring(L, -1)) - { - luaL_errorL(L, "tls config requires 'certfilename' (string)"); - return 0; - } - tlsOptions->cert_file_name = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, -1, "keyfilename"); - if (!lua_isstring(L, -1)) - { - luaL_errorL(L, "tls config requires 'keyfilename' (string)"); - return 0; - } - tlsOptions->key_file_name = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, -1, "passphrase"); - if (lua_isstring(L, -1)) - { - tlsOptions->passphrase = lua_tostring(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, -1, "cafilename"); - if (lua_isstring(L, -1)) - { - tlsOptions->ca_file_name = lua_tostring(L, -1); - } - lua_pop(L, 1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "handler"); - if (!lua_isfunction(L, -1)) - { - lua_pop(L, 1); - luaL_errorL(L, "handler function is required in config table"); - return 0; - } - lua_insert(L, -1); - handlerIndex = lua_gettop(L); - } - else if (!lua_isfunction(L, 1)) - { - luaL_errorL(L, "serve requires a handler function or config table"); - return 0; - } - - Runtime* runtime = getRuntime(L); - - int serverId = nextServerId++; - - auto state = std::make_shared(); - state->runtime = runtime; - state->hostname = hostname; - state->port = port; - state->reusePort = reusePort; - - lua_pushvalue(L, handlerIndex); - state->handlerRef = std::make_shared(L, -1); - lua_pop(L, 1); - - uWSApp app; - bool success = false; - - if (tlsOptions) - { - auto ssl_app = std::make_unique(*tlsOptions); - state->app = ssl_app.get(); - setupAppAndListen(ssl_app.get(), state, success); - app = std::move(ssl_app); - } - else - { - auto plain_app = std::make_unique(); - state->app = plain_app.get(); - setupAppAndListen(plain_app.get(), state, success); - app = std::move(plain_app); - } - - if (!success) - { - luaL_errorL(L, "failed to listen on port %d, is it already in use? consider the reuseport option", port); - 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"); - lua_pushstring(L, hostname.c_str()); - lua_settable(L, -3); - - lua_pushstring(L, "port"); - lua_pushinteger(L, port); - lua_settable(L, -3); - - lua_pushstring(L, "close"); - lua_pushinteger(L, serverId); - lua_pushcclosurek( - L, - [](lua_State* L) -> int - { - int serverId = lua_tointeger(L, lua_upvalueindex(1)); - - lua_pushboolean(L, closeServer(serverId)); - return 1; - }, - "server_close", - 1, - nullptr - ); - lua_settable(L, -3); + lua_setreadonly(L, -1, 1); return 1; } -} // namespace net - -struct CurlHolder -{ - CurlHolder() - { - curl_global_init(CURL_GLOBAL_DEFAULT); - } - - ~CurlHolder() - { - curl_global_cleanup(); - } -}; - -static CurlHolder& globalCurlInit() -{ - static CurlHolder holder; - return holder; -} - -int luaopen_net(lua_State* L) +LUTE_API int luaopen_net(lua_State* L) { - globalCurlInit(); - - luaL_register(L, "net", net::lib); - - return 1; + return Net::openAsGlobal(L); } -int luteopen_net(lua_State* L) +LUTE_API int luteopen_net(lua_State* L) { - globalCurlInit(); - - lua_createtable(L, 0, std::size(net::lib)); - - for (auto& [name, func] : net::lib) - { - if (!name || !func) - break; - - lua_pushcfunction(L, func, name); - lua_setfield(L, -2, name); - } - - lua_setreadonly(L, -1, 1); - - return 1; + return Net::pushLibrary(L); } diff --git a/lute/net/src/server.cpp b/lute/net/src/server.cpp new file mode 100644 index 000000000..a1eca23c6 --- /dev/null +++ b/lute/net/src/server.cpp @@ -0,0 +1,1242 @@ +#include "lute/common.h" +#include "lute/net.h" +#include "lute/runtime.h" +#include "lute/userdatas.h" + +#include "Luau/DenseHash.h" +#include "Luau/Variant.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "App.h" +#include "Loop.h" +#include "wscommon.h" + +namespace net::server +{ + +using uWSApp = Luau::Variant, std::unique_ptr>; + +static const int kEmptyServerKey = 0; +static Luau::DenseHashMap serverInstances(kEmptyServerKey); +static Luau::DenseHashMap> serverStates(kEmptyServerKey); +static int nextServerId = 1; +static int kRequestUpgradeKey = 0; +static constexpr unsigned int kWebSocketMaxPayloadLength = 16 * 1024 * 1024; + +struct ServerLoopState +{ + Luau::Variant app; + Runtime* runtime; + bool running = true; + std::function loopFunction; + std::shared_ptr handlerRef; + std::shared_ptr serverRef; + std::shared_ptr wsOpenRef; + std::shared_ptr wsMessageRef; + std::shared_ptr wsCloseRef; + std::shared_ptr wsDrainRef; + bool hasWebSocket = false; + std::string hostname; + int port; + bool reusePort = false; +}; + +template +struct PerSocketData; + +template +struct RequestUpgradeContext +{ + uWS::HttpResponse* res = nullptr; + uWS::HttpRequest* req = nullptr; + us_socket_context_t* socketContext = nullptr; + bool attempted = false; + bool upgraded = false; + + bool canAttempt() const + { + return !attempted && res && req && socketContext; + } + + void invalidatePointers() + { + res = nullptr; + req = nullptr; + socketContext = nullptr; + } +}; + +template +using RequestUpgradeContextPtr = std::shared_ptr>; + +struct ServerWebSocketHandle +{ + void* wsPtr = nullptr; + std::atomic closed{false}; + std::shared_ptr userdataRef; + int (*sendFn)(void* wsPtr, std::string_view data, bool binary) = nullptr; + void (*closeFn)(void* wsPtr, uint16_t code, std::string_view message) = nullptr; +}; + +template +struct PerSocketData +{ + std::shared_ptr handle; +}; + +struct RequestRouteData +{ + std::string method; + std::string path; + std::string query; +}; + +using RequestHeaders = std::vector>; + +template +static RequestHeaders extractRequestHeaders(ReqT* req) +{ + RequestHeaders headers; + for (const auto& header : *req) + { + headers.emplace_back(std::string(header.first), std::string(header.second)); + } + return headers; +} + +template +static RequestRouteData extractRequestRouteData(ReqT* req) +{ + RequestRouteData route; + route.method = std::string(req->getMethod()); + std::transform( + route.method.begin(), + route.method.end(), + route.method.begin(), + [](unsigned char ch) + { + return char(std::toupper(ch)); + } + ); + + std::string_view url = req->getFullUrl(); + size_t queryPos = url.find('?'); + if (queryPos == std::string::npos) + { + route.path.assign(url.data(), url.size()); + return route; + } + + route.path.assign(url.data(), queryPos); + route.query.assign(url.data() + queryPos, url.size() - queryPos); + return route; +} + +static void parseQuery(const std::string_view& query, lua_State* L) +{ + lua_createtable(L, 0, 0); + size_t start = (!query.empty() && query[0] == '?') ? 1 : 0; + if (start >= query.size()) + return; + + while (start < query.size()) + { + size_t end = query.find('&', start); + size_t pairLength = (end == std::string::npos) ? (query.size() - start) : (end - start); + std::string_view pair = std::string_view(query.data() + start, pairLength); + size_t eq = pair.find('='); + if (eq != std::string::npos) + { + std::string_view key = std::string_view(pair.data(), eq); + std::string_view value = uWS::getDecodedQueryValue(key, query); + lua_pushlstring(L, key.data(), key.size()); + lua_pushlstring(L, value.data(), value.size()); + lua_settable(L, -3); + } + + if (end == std::string::npos) + break; + + start = end + 1; + } +} + +static void pushHeadersTable(const RequestHeaders& headers, lua_State* L) +{ + lua_createtable(L, 0, int(headers.size())); + for (const auto& [key, value] : headers) + { + lua_pushlstring(L, key.data(), key.size()); + lua_pushlstring(L, value.data(), value.size()); + lua_settable(L, -3); + } +} + +static void handleResponse(auto* res, lua_State* L, int responseIndex) +{ + if (lua_isstring(L, responseIndex)) + { + size_t bodyLength = 0; + const char* bodyData = lua_tolstring(L, responseIndex, &bodyLength); + res->writeStatus("200 OK"); + res->writeHeader("Content-Type", "text/html"); + res->end(std::string_view(bodyData, bodyLength)); + return; + } + + if (!lua_istable(L, responseIndex)) + { + res->writeStatus("500 Internal Server Error"); + res->end("Handler must return a string or a response table"); + return; + } + + lua_getfield(L, responseIndex, "status"); + int status = lua_isnumber(L, -1) ? lua_tointeger(L, -1) : 200; + lua_pop(L, 1); + + std::string statusText; + switch (status) + { + case 200: + statusText = "200 OK"; + break; + case 201: + statusText = "201 Created"; + break; + case 204: + statusText = "204 No Content"; + break; + case 400: + statusText = "400 Bad Request"; + break; + case 401: + statusText = "401 Unauthorized"; + break; + case 403: + statusText = "403 Forbidden"; + break; + case 404: + statusText = "404 Not Found"; + break; + case 500: + statusText = "500 Internal Server Error"; + break; + default: + statusText = std::to_string(status) + " Status"; + break; + } + res->writeStatus(statusText); + + lua_getfield(L, responseIndex, "headers"); + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + if (lua_isstring(L, -2) && lua_isstring(L, -1)) + { + size_t headerNameLength = 0; + size_t headerValueLength = 0; + const char* headerName = lua_tolstring(L, -2, &headerNameLength); + const char* headerValue = lua_tolstring(L, -1, &headerValueLength); + if (headerName && headerValue) + { + res->writeHeader( + std::string_view(headerName, headerNameLength), + std::string_view(headerValue, headerValueLength) + ); + } + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_getfield(L, responseIndex, "body"); + + std::string_view body; + if (!lua_isnil(L, -1)) + { + size_t bodyLength = 0; + const char* bodyData = lua_tolstring(L, -1, &bodyLength); + if (bodyData) + body = std::string_view(bodyData, bodyLength); + } + + res->end(body); + lua_pop(L, 1); +} + +template +struct HttpYieldContext +{ + ResT* res = nullptr; + std::atomic aborted{false}; + std::shared_ptr threadRef; +}; + +template +static void finishHttpYield(lua_State* L, int status, const std::shared_ptr>& ctx) +{ + if (!ctx) + { + lua_settop(L, 0); + return; + } + + ResT* res = ctx->res; + + if (!res || ctx->aborted.load()) + { + lua_settop(L, 0); + return; + } + + if (status == LUA_OK) + { + handleResponse(res, L, -1); + } + else + { + std::string error = lua_isstring(L, -1) ? lua_tostring(L, -1) : "Server error"; + res->writeStatus("500 Internal Server Error"); + res->end("Server error: " + error); + } + + ctx->res = nullptr; + lua_settop(L, 0); +} + +static void resumeWith(std::shared_ptr state, const std::shared_ptr& callback, std::function argPusher) +{ + if (!callback) + return; + + state->runtime->scheduleLuauCallback(callback, std::move(argPusher)); +} + +template +static bool performWebSocketUpgrade(uWS::HttpResponse* res, uWS::HttpRequest* req, us_socket_context_t* context) +{ + std::string_view key = req->getHeader("sec-websocket-key"); + std::string_view protocol = req->getHeader("sec-websocket-protocol"); + std::string_view extensions = req->getHeader("sec-websocket-extensions"); + + if (key.empty()) + return false; + + PerSocketData userData; + res->template upgrade>(std::move(userData), key, protocol, extensions, context); + return true; +} + +static int server_upgrade_noop(lua_State* L) +{ + lua_pushboolean(L, 0); + return 1; +} + +static int server_upgrade(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, 2, LUA_TTABLE); + + if (!lua_getmetatable(L, 2)) + { + lua_pushboolean(L, 0); + return 1; + } + + lua_pushlightuserdata(L, &kRequestUpgradeKey); + lua_rawget(L, -2); + if (!lua_isfunction(L, -1)) + { + lua_pop(L, 2); + lua_pushboolean(L, 0); + return 1; + } + + lua_call(L, 0, 1); + lua_remove(L, -2); + return 1; +} + +template +static RequestUpgradeContext* getRequestUpgradeContext(lua_State* L) +{ + auto* contextPtr = static_cast*>(lua_touserdata(L, lua_upvalueindex(1))); + return contextPtr ? contextPtr->get() : nullptr; +} + +template +static void pushRequestUpgradeContext(lua_State* L, const RequestUpgradeContextPtr& upgradeContext) +{ + auto* storage = static_cast*>(lua_newuserdatadtor( + L, + sizeof(RequestUpgradeContextPtr), + [](void* ptr) + { + std::destroy_at(static_cast*>(ptr)); + } + )); + + new (storage) RequestUpgradeContextPtr(upgradeContext); +} + +template +static int server_upgrade_do(lua_State* L) +{ + auto* upgradeContext = getRequestUpgradeContext(L); + if (!upgradeContext || !upgradeContext->canAttempt()) + { + lua_pushboolean(L, 0); + return 1; + } + + upgradeContext->attempted = true; + bool upgraded = performWebSocketUpgrade(upgradeContext->res, upgradeContext->req, upgradeContext->socketContext); + upgradeContext->upgraded = upgraded; + if (upgraded) + upgradeContext->invalidatePointers(); + + lua_pushboolean(L, upgraded); + return 1; +} + +template +static int wsSendImpl(void* wsPtr, std::string_view data, bool binary) +{ + auto* ws = static_cast>*>(wsPtr); + auto status = ws->send(data, binary ? uWS::OpCode::BINARY : uWS::OpCode::TEXT); + + if (status == decltype(status)::BACKPRESSURE) + return -1; + + if (status == decltype(status)::DROPPED) + return 0; + + return int(data.size() > 0 ? data.size() : 1); +} + +template +static void wsCloseImpl(void* wsPtr, uint16_t code, std::string_view message) +{ + auto* ws = static_cast>*>(wsPtr); + ws->end(int(code), message); +} + +static int server_ws_send(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TUSERDATA); + auto* handlePtr = static_cast*>(lua_touserdatatagged(L, 1, kServerWebSocketHandleTag)); + if (!handlePtr || !(*handlePtr) || (*handlePtr)->closed.load()) + { + lua_pushinteger(L, 0); + return 1; + } + + WebSocketPayload payload = checkWebSocketPayload(L, 2); + + int result = 0; + if (!(*handlePtr)->closed.load() && (*handlePtr)->wsPtr && (*handlePtr)->sendFn) + result = (*handlePtr)->sendFn((*handlePtr)->wsPtr, std::string_view(payload.data, payload.length), payload.binary); + + lua_pushinteger(L, result); + return 1; +} + +static int server_ws_close(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TUSERDATA); + auto* handlePtr = static_cast*>(lua_touserdatatagged(L, 1, kServerWebSocketHandleTag)); + if (!handlePtr || !(*handlePtr) || (*handlePtr)->closed.load() || !(*handlePtr)->wsPtr) + return 0; + + int code = 1000; + if (!lua_isnoneornil(L, 2)) + { + code = int(luaL_checkinteger(L, 2)); + if (code < 0 || code > std::numeric_limits::max()) + luaL_errorL(L, "invalid websocket close code %d", code); + } + + std::string message; + if (!lua_isnoneornil(L, 3)) + { + size_t messageLength = 0; + const char* messageData = luaL_checklstring(L, 3, &messageLength); + message.assign(messageData, messageLength); + } + + if (!(*handlePtr)->closed.load() && (*handlePtr)->wsPtr && (*handlePtr)->closeFn) + { + (*handlePtr)->closeFn((*handlePtr)->wsPtr, uint16_t(code), message); + } + + return 0; +} + +static void pushServerWebSocket(lua_State* L, const std::shared_ptr& handle, const std::shared_ptr& retainedRef = nullptr) +{ + if (retainedRef) + { + retainedRef->push(L); + return; + } + + if (handle->userdataRef) + { + handle->userdataRef->push(L); + return; + } + + auto* storage = new (static_cast*>( + lua_newuserdatataggedwithmetatable(L, sizeof(std::shared_ptr), kServerWebSocketHandleTag) + )) std::shared_ptr(handle); + (void)storage; + handle->userdataRef = std::make_shared(L, -1); +} + +struct HandlerThread +{ + lua_State* L = nullptr; + std::shared_ptr threadRef; +}; + +static HandlerThread createHandlerThread(Runtime* runtime) +{ + LUTE_ASSERT(runtime); + + lua_State* L = lua_newthread(runtime->GL); + luaL_sandboxthread(L); + lua_checkstack(L, 64); + std::shared_ptr threadRef = getRefForThread(L); + lua_pop(runtime->GL, 1); + return {L, std::move(threadRef)}; +} + +template +static void pushRequestTable( + lua_State* L, + const RequestHeaders& headers, + const RequestRouteData& route, + std::string_view body, + lua_CFunction upgradeFn, + PushUpvalues pushUpvalues +) +{ + lua_createtable(L, 0, 5); + + lua_pushstring(L, "method"); + lua_pushstring(L, route.method.c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "path"); + lua_pushlstring(L, route.path.data(), route.path.size()); + lua_settable(L, -3); + + lua_pushstring(L, "query"); + parseQuery(route.query, L); + lua_settable(L, -3); + + lua_pushstring(L, "headers"); + pushHeadersTable(headers, L); + lua_settable(L, -3); + + lua_pushstring(L, "body"); + lua_pushlstring(L, body.data() != nullptr ? body.data() : "", body.size()); + lua_settable(L, -3); + + int requestIndex = lua_absindex(L, -1); + lua_createtable(L, 0, 1); + lua_pushlightuserdata(L, &kRequestUpgradeKey); + int nUpvalues = pushUpvalues(L); + lua_pushcclosure(L, upgradeFn, "request.upgrade", nUpvalues); + lua_rawset(L, -3); + lua_setmetatable(L, requestIndex); +} + +static void pushServerTable(lua_State* L, const std::shared_ptr& serverRef) +{ + if (serverRef) + { + serverRef->push(L); + return; + } + + lua_newtable(L); + lua_pushcfunction(L, server_upgrade, "server.upgrade"); + lua_setfield(L, -2, "upgrade"); +} + +static HandlerThread prepareHttpHandlerThread( + const std::shared_ptr& state, + const RequestHeaders& headers, + const RequestRouteData& route, + std::string_view body +) +{ + LUTE_ASSERT(state); + LUTE_ASSERT(state->runtime); + LUTE_ASSERT(state->handlerRef); + + HandlerThread thread = createHandlerThread(state->runtime); + lua_State* L = thread.L; + auto pushNoUpvalues = [](lua_State*) + { + return 0; + }; + + // `lua_resume(L, nullptr, 2)` expects the stack shape `[handler, request, server]`. + state->handlerRef->push(L); + pushRequestTable(L, headers, route, body, server_upgrade_noop, pushNoUpvalues); + pushServerTable(L, state->serverRef); + return thread; +} + +template +static HandlerThread prepareUpgradeHandlerThread( + const std::shared_ptr& state, + const RequestHeaders& headers, + const RequestRouteData& route, + const RequestUpgradeContextPtr& upgradeContext +) +{ + LUTE_ASSERT(state); + LUTE_ASSERT(state->runtime); + LUTE_ASSERT(state->handlerRef); + + auto pushUpgradeUpvalues = [upgradeContext](lua_State* L) + { + int top = lua_gettop(L); + pushRequestUpgradeContext(L, upgradeContext); + return lua_gettop(L) - top; + }; + + HandlerThread thread = createHandlerThread(state->runtime); + lua_State* L = thread.L; + + // `lua_resume(L, nullptr, 2)` expects the stack shape `[handler, request, server]`. + state->handlerRef->push(L); + pushRequestTable(L, headers, route, {}, server_upgrade_do, pushUpgradeUpvalues); + pushServerTable(L, state->serverRef); + return thread; +} + +template +static void processRequest( + const std::shared_ptr& state, + ResT* res, + const RequestHeaders& headers, + const RequestRouteData& route, + std::string_view body +) +{ + if (!state->handlerRef) + { + res->writeStatus("404 Not Found"); + res->end("No handler configured"); + return; + } + + HandlerThread thread = prepareHttpHandlerThread(state, headers, route, body); + lua_State* L = thread.L; + int status = lua_resume(L, nullptr, 2); + if (status == LUA_YIELD) + { + auto ctx = std::make_shared>(); + ctx->res = res; + ctx->threadRef = std::move(thread.threadRef); + + res->onAborted( + [ctx]() + { + ctx->aborted.store(true); + ctx->res = nullptr; + } + ); + + ThreadCompletionHandler completion; + completion.onFinish = [ctx](lua_State* L, int completionStatus) + { + finishHttpYield(L, completionStatus, ctx); + }; + + state->runtime->addThreadCompletionHandler(L, std::move(completion)); + lua_settop(L, 0); + return; + } + + if (status != LUA_OK) + { + std::string error = lua_isstring(L, -1) ? lua_tostring(L, -1) : "Server error"; + lua_pop(L, 1); + + res->writeStatus("500 Internal Server Error"); + res->end("Server error: " + error); + return; + } + + handleResponse(res, L, -1); + lua_pop(L, 1); +} + +template +static void installWebSocketRoutes(AppT* app, const std::shared_ptr& state) +{ + if (!state->hasWebSocket) + return; + + typename uWS::TemplatedApp::template WebSocketBehavior> behavior{}; + behavior.maxPayloadLength = kWebSocketMaxPayloadLength; + behavior.upgrade = [state](auto* res, auto* req, auto* context) + { + if (!state->handlerRef) + { + if (!performWebSocketUpgrade(res, req, context)) + { + res->writeStatus("426 Upgrade Required"); + res->end("WebSocket upgrade required"); + } + return; + } + + RequestRouteData route = extractRequestRouteData(req); + RequestHeaders headers = extractRequestHeaders(req); + auto upgradeContext = std::make_shared>(); + upgradeContext->res = res; + upgradeContext->req = req; + upgradeContext->socketContext = context; + + HandlerThread thread = prepareUpgradeHandlerThread(state, headers, route, upgradeContext); + lua_State* L = thread.L; + int status = lua_resume(L, nullptr, 2); + upgradeContext->invalidatePointers(); + bool upgraded = upgradeContext->upgraded; + + if (status == LUA_YIELD) + { + lua_resetthread(L); + + if (!upgraded) + { + res->writeStatus("500 Internal Server Error"); + res->end("upgrade handler cannot yield"); + } + return; + } + + if (status != LUA_OK) + { + std::string error = lua_isstring(L, -1) ? lua_tostring(L, -1) : "Server error"; + if (!upgraded) + { + res->writeStatus("500 Internal Server Error"); + res->end("Server error: " + error); + } + lua_pop(L, 1); + return; + } + + if (!upgraded) + handleResponse(res, L, -1); + + lua_pop(L, 1); + }; + behavior.open = [state](auto* ws) + { + auto* data = ws->getUserData(); + data->handle = std::make_shared(); + data->handle->wsPtr = ws; + data->handle->sendFn = &wsSendImpl; + data->handle->closeFn = &wsCloseImpl; + + resumeWith( + state, + state->wsOpenRef, + [handle = data->handle](lua_State* L) + { + pushServerWebSocket(L, handle); + return 1; + } + ); + }; + behavior.message = [state](auto* ws, std::string_view message, uWS::OpCode opCode) + { + auto handle = ws->getUserData()->handle; + std::string payload(message.data(), message.size()); + bool binary = (opCode == uWS::OpCode::BINARY); + + resumeWith( + state, + state->wsMessageRef, + [handle, payload = std::move(payload), binary](lua_State* L) + { + pushServerWebSocket(L, handle); + if (binary) + { + void* buf = lua_newbuffer(L, payload.size()); + if (!payload.empty()) + memcpy(buf, payload.data(), payload.size()); + } + else + { + lua_pushlstring(L, payload.data(), payload.size()); + } + return 2; + } + ); + }; + behavior.drain = [state](auto* ws) + { + auto handle = ws->getUserData()->handle; + + resumeWith( + state, + state->wsDrainRef, + [handle](lua_State* L) + { + pushServerWebSocket(L, handle); + return 1; + } + ); + }; + behavior.close = [state](auto* ws, int code, std::string_view message) + { + auto handle = ws->getUserData()->handle; + std::shared_ptr userdataRef; + if (handle) + { + handle->closed.store(true); + handle->wsPtr = nullptr; + userdataRef = std::move(handle->userdataRef); + } + + std::string payload(message.data(), message.size()); + + resumeWith( + state, + state->wsCloseRef, + [handle, userdataRef = std::move(userdataRef), code, payload = std::move(payload)](lua_State* L) + { + pushServerWebSocket(L, handle, userdataRef); + lua_pushinteger(L, code); + lua_pushlstring(L, payload.data(), payload.size()); + return 3; + } + ); + }; + + app->template ws>("/*", std::move(behavior)); +} + +template +static void installHttpRoutes(AppT* app, const std::shared_ptr& state) +{ + app->any( + "/*", + [state](auto* res, auto* req) + { + RequestRouteData route = extractRequestRouteData(req); + RequestHeaders headers = extractRequestHeaders(req); + + res->onAborted( + []() + { + // TODO: handle aborted requests + } + ); + + std::unique_ptr bodyBuffer; + res->onData( + [state, res, route = std::move(route), headers = std::move(headers), bodyBuffer = std::move(bodyBuffer)]( + std::string_view data, bool last + ) mutable + { + if (last) + { + if (bodyBuffer.get()) + { + bodyBuffer->append(data); + processRequest(state, res, headers, route, *bodyBuffer); + } + else + { + processRequest(state, res, headers, route, data); + } + } + else + { + if (bodyBuffer.get()) + { + bodyBuffer->append(data); + } + else + { + bodyBuffer = std::make_unique(data); + } + } + } + ); + } + ); +} + +template +static void listenApp(AppT* app, const std::shared_ptr& state, bool& success) +{ + int options = state->reusePort ? LIBUS_LISTEN_DEFAULT : LIBUS_LISTEN_EXCLUSIVE_PORT; + + app->listen( + state->hostname, + state->port, + options, + [state, &success](auto* listen_socket) + { + success = (listen_socket != nullptr); + if (listen_socket) + state->port = us_socket_local_port(SSL, (struct us_socket_t*)listen_socket); + } + ); +} + +static bool closeServer(int serverId) +{ + if (!serverInstances.contains(serverId) || !serverStates.contains(serverId) || !serverStates[serverId]) + { + return false; + } + + Luau::visit( + [](auto* appPtr) + { + if (appPtr) + appPtr->close(); + }, + serverStates[serverId]->app + ); + serverStates[serverId]->running = false; + + Luau::visit( + [](auto& ptr) + { + if (ptr) + ptr.reset(); + }, + serverInstances[serverId] + ); + serverStates[serverId] = nullptr; + + return true; +} + +int serve(lua_State* L) +{ + uWS::Loop::get(getRuntimeLoop(L)); + + std::string hostname = "127.0.0.1"; + int port = 3000; + bool reusePort = false; + std::optional tlsOptions; + int handlerIndex = 0; + int websocketIndex = 0; + + if (lua_istable(L, 1)) + { + lua_getfield(L, 1, "hostname"); + if (lua_isstring(L, -1)) + { + hostname = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "port"); + if (lua_isnumber(L, -1)) + { + port = lua_tointeger(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "reuseport"); + if (lua_isboolean(L, -1)) + { + reusePort = lua_toboolean(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "tls"); + if (lua_istable(L, -1)) + { + tlsOptions.emplace(); + + lua_getfield(L, -1, "certfilename"); + if (!lua_isstring(L, -1)) + { + luaL_errorL(L, "tls config requires 'certfilename' (string)"); + return 0; + } + tlsOptions->cert_file_name = lua_tostring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "keyfilename"); + if (!lua_isstring(L, -1)) + { + luaL_errorL(L, "tls config requires 'keyfilename' (string)"); + return 0; + } + tlsOptions->key_file_name = lua_tostring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "passphrase"); + if (lua_isstring(L, -1)) + { + tlsOptions->passphrase = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "cafilename"); + if (lua_isstring(L, -1)) + { + tlsOptions->ca_file_name = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "handler"); + if (lua_isfunction(L, -1)) + { + handlerIndex = lua_gettop(L); + } + else + { + lua_pop(L, 1); + } + + lua_getfield(L, 1, "websocket"); + if (lua_istable(L, -1)) + { + websocketIndex = lua_gettop(L); + } + else + { + lua_pop(L, 1); + } + + if (handlerIndex == 0 && websocketIndex == 0) + { + luaL_errorL(L, "config table requires a handler function, websocket config, or both"); + return 0; + } + } + else if (!lua_isfunction(L, 1)) + { + luaL_errorL(L, "serve requires a handler function or config table"); + return 0; + } + else + { + handlerIndex = 1; + } + + Runtime* runtime = getRuntime(L); + + int serverId = nextServerId++; + + auto state = std::make_shared(); + state->runtime = runtime; + state->hostname = hostname; + state->port = port; + state->reusePort = reusePort; + + if (handlerIndex != 0) + { + lua_pushvalue(L, handlerIndex); + state->handlerRef = std::make_shared(L, -1); + lua_pop(L, 1); + } + + if (websocketIndex != 0) + { + state->hasWebSocket = true; + + lua_getfield(L, websocketIndex, "open"); + if (lua_isfunction(L, -1)) + state->wsOpenRef = std::make_shared(L, -1); + lua_pop(L, 1); + + lua_getfield(L, websocketIndex, "message"); + if (lua_isfunction(L, -1)) + state->wsMessageRef = std::make_shared(L, -1); + lua_pop(L, 1); + + lua_getfield(L, websocketIndex, "close"); + if (lua_isfunction(L, -1)) + state->wsCloseRef = std::make_shared(L, -1); + lua_pop(L, 1); + + lua_getfield(L, websocketIndex, "drain"); + if (lua_isfunction(L, -1)) + state->wsDrainRef = std::make_shared(L, -1); + lua_pop(L, 1); + } + + uWSApp app; + bool success = false; + + if (tlsOptions) + { + auto ssl_app = std::make_unique(*tlsOptions); + state->app = ssl_app.get(); + installWebSocketRoutes(ssl_app.get(), state); + installHttpRoutes(ssl_app.get(), state); + listenApp(ssl_app.get(), state, success); + app = std::move(ssl_app); + } + else + { + auto plain_app = std::make_unique(); + state->app = plain_app.get(); + installWebSocketRoutes(plain_app.get(), state); + installHttpRoutes(plain_app.get(), state); + listenApp(plain_app.get(), state, success); + app = std::move(plain_app); + } + + if (!success) + { + luaL_errorL(L, "failed to listen on port %d, is it already in use? consider the reuseport option", port); + return 0; + } + + serverInstances[serverId] = std::move(app); + serverStates[serverId] = state; + + lua_createtable(L, 0, 4); + + lua_pushstring(L, "hostname"); + lua_pushstring(L, hostname.c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "port"); + lua_pushinteger(L, state->port); + lua_settable(L, -3); + + lua_pushstring(L, "close"); + lua_pushinteger(L, serverId); + lua_pushcclosurek( + L, + [](lua_State* L) -> int + { + int serverId = lua_tointeger(L, lua_upvalueindex(1)); + + lua_pushboolean(L, closeServer(serverId)); + return 1; + }, + "server_close", + 1, + nullptr + ); + lua_settable(L, -3); + + lua_pushstring(L, "upgrade"); + lua_pushcfunction(L, server_upgrade, "server.upgrade"); + lua_settable(L, -3); + + state->serverRef = std::make_shared(L, -1); + + return 1; +} + +} // namespace net::server + +static void initializeNetServer(lua_State* L) +{ + luaL_newmetatable(L, "ServerWebSocketHandle"); + + lua_pushcfunction( + L, + [](lua_State* L) + { + const char* index = luaL_checkstring(L, -1); + + if (strcmp(index, "send") == 0) + { + lua_pushcfunction(L, net::server::server_ws_send, "ServerWebSocketHandle.send"); + return 1; + } + + if (strcmp(index, "close") == 0) + { + lua_pushcfunction(L, net::server::server_ws_close, "ServerWebSocketHandle.close"); + return 1; + } + + return 0; + }, + "ServerWebSocketHandle.__index" + ); + lua_setfield(L, -2, "__index"); + + lua_pushstring(L, "ServerWebSocketHandle"); + lua_setfield(L, -2, "__type"); + + lua_setuserdatadtor( + L, + kServerWebSocketHandleTag, + [](lua_State*, void* ud) + { + std::destroy_at(static_cast*>(ud)); + } + ); + + lua_setuserdatametatable(L, kServerWebSocketHandleTag); +} + +const char* const NetServer::properties[] = {nullptr}; + +const luaL_Reg NetServer::lib[] = { + {"serve", net::server::serve}, + {nullptr, nullptr}, +}; + +int NetServer::pushLibrary(lua_State* L) +{ + initializeNetServer(L); + + lua_createtable(L, 0, std::size(NetServer::lib)); + + for (auto& [name, func] : NetServer::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return 1; +} + +LUTE_API int luteopen_net_server(lua_State* L) +{ + return NetServer::pushLibrary(L); +} diff --git a/lute/net/src/system_ca.cpp b/lute/net/src/system_ca.cpp new file mode 100644 index 000000000..3351e6095 --- /dev/null +++ b/lute/net/src/system_ca.cpp @@ -0,0 +1,64 @@ +#include "system_ca.h" + +#include "curl/curl.h" + +#if defined(__linux__) +#include +#endif + +namespace net +{ + +#if defined(__linux__) +namespace +{ + +// Mirrors the lists in Go's crypto/x509/root_linux.go. +constexpr const char* kCAFiles[] = { + "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 + "/etc/ssl/ca-bundle.pem", // OpenSUSE + "/etc/pki/tls/cacert.pem", // OpenELEC + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 + "/etc/ssl/cert.pem", // Alpine Linux +}; + +constexpr const char* kCADirs[] = { + "/etc/ssl/certs", // SLES10/SLES11 + "/etc/pki/tls/certs", // Fedora/RHEL +}; + +const char* findPath(const char* const* paths, size_t count, bool wantDir) +{ + struct stat st; + for (size_t i = 0; i < count; ++i) + { + if (::stat(paths[i], &st) != 0) + continue; + if (wantDir ? S_ISDIR(st.st_mode) : S_ISREG(st.st_mode)) + return paths[i]; + } + return nullptr; +} + +} // namespace +#endif + +void applySystemCA(CURL* curl) +{ +#if defined(_WIN32) + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#elif defined(__APPLE__) + // macOS 12+ always ships /etc/ssl/cert.pem + curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/cert.pem"); +#elif defined(__linux__) + static const char* cafile = findPath(kCAFiles, sizeof(kCAFiles) / sizeof(kCAFiles[0]), false); + static const char* capath = findPath(kCADirs, sizeof(kCADirs) / sizeof(kCADirs[0]), true); + if (cafile) + curl_easy_setopt(curl, CURLOPT_CAINFO, cafile); + if (capath) + curl_easy_setopt(curl, CURLOPT_CAPATH, capath); +#endif +} + +} // namespace net diff --git a/lute/net/src/system_ca.h b/lute/net/src/system_ca.h new file mode 100644 index 000000000..db9b1da34 --- /dev/null +++ b/lute/net/src/system_ca.h @@ -0,0 +1,11 @@ +#pragma once + +typedef void CURL; + +namespace net +{ + +// Configures `curl` to use the operating system's trusted CA store. +void applySystemCA(CURL* curl); + +} // namespace net diff --git a/lute/net/src/wsclient.cpp b/lute/net/src/wsclient.cpp new file mode 100644 index 000000000..1d79f3227 --- /dev/null +++ b/lute/net/src/wsclient.cpp @@ -0,0 +1,761 @@ +#include "lute/runtime.h" +#include "lute/userdatas.h" + +#include "Luau/VecDeque.h" + +#include "lua.h" +#include "lualib.h" + +#include "curl/curl.h" +#include "curl/websockets.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "system_ca.h" +#include "wscommon.h" + +namespace net::client +{ +struct WebSocketConnection; +struct WebSocketHandle; + +struct PendingSend +{ + std::string payload; + bool binary = false; + size_t offset = 0; +}; + +struct WebSocketPollState +{ + uv_poll_t handle{}; + std::shared_ptr owner; +}; + +static void clearWebSocketPollState(WebSocketPollState*& pollState, int& activePollEvents) +{ + if (!pollState) + return; + + WebSocketPollState* state = pollState; + pollState = nullptr; + activePollEvents = 0; + + uv_poll_stop(&state->handle); + uv_close( + reinterpret_cast(&state->handle), + [](uv_handle_t* handle) + { + delete static_cast(handle->data); + } + ); +} + +static void pushWebSocketMessageToLua(lua_State* L, const std::string& message, bool binary) +{ + if (binary) + { + void* buffer = lua_newbuffer(L, message.size()); + if (!message.empty()) + memcpy(buffer, message.data(), message.size()); + return; + } + + lua_pushlstring(L, message.data(), message.size()); +} + +static std::pair parseClosePayload(const std::string& payload) +{ + int closeCode = 1000; + std::string closeReason; + + if (payload.size() >= 2) + { + const unsigned char* bytes = reinterpret_cast(payload.data()); + closeCode = int((bytes[0] << 8) | bytes[1]); + if (payload.size() > 2) + closeReason.assign(payload.data() + 2, payload.size() - 2); + } + + return {closeCode, std::move(closeReason)}; +} + +struct WebSocketConnection : std::enable_shared_from_this +{ + CURL* curl = nullptr; + curl_slist* headerList = nullptr; + WebSocketPollState* pollState = nullptr; + std::mutex curlMutex; + int activePollEvents = 0; + std::vector recvBuffer = std::vector(16 * 1024); + std::string currentMessage; + bool currentBinary = false; + bool hasCurrentMessage = false; + std::string closePayload; + Luau::VecDeque pendingSends; + std::atomic isClosed{false}; + std::weak_ptr owner; + std::shared_ptr keepAliveHandle; + + void setOwner(const std::shared_ptr& handle) + { + owner = handle; + keepAliveHandle = handle; + } + + void releaseKeepAliveHandle() + { + keepAliveHandle.reset(); + } + + bool closed() const + { + return isClosed.load(); + } + + bool startPolling(Runtime* runtime, curl_socket_t socket) + { + if (!runtime || socket == CURL_SOCKET_BAD) + return false; + + auto* state = new WebSocketPollState(); + state->owner = shared_from_this(); + state->handle.data = state; + + int initResult = uv_poll_init_socket(runtime->getEventLoop(), &state->handle, socket); + if (initResult != 0) + { + delete state; + return false; + } + + pollState = state; + + if (!updatePollingInterest()) + { + closeTransport(false); + return false; + } + + return true; + } + + bool applyPollingEvents(int events) + { + if (!pollState) + return false; + + if (activePollEvents == events) + return true; + + int startResult = uv_poll_start( + &pollState->handle, + events, + [](uv_poll_t* handle, int status, int events) + { + auto* state = static_cast(handle->data); + if (!state || !state->owner) + return; + + state->owner->handlePollEvent(status, events); + } + ); + + if (startResult != 0) + return false; + + activePollEvents = events; + return true; + } + + bool updatePollingInterest() + { + int events = UV_READABLE; + if (!pendingSends.empty()) + events |= UV_WRITABLE; + + return applyPollingEvents(events); + } + + void handlePollEvent(int status, int events) + { + if (status < 0) + { + closeWithError(uv_strerror(status)); + return; + } + + if (events & UV_READABLE) + processIncoming(); + + if (!closed() && (events & UV_WRITABLE)) + flushOutgoing(); + } + + void enqueueSend(std::string payload, bool binary) + { + if (closed()) + return; + + pendingSends.push_back({std::move(payload), binary, 0}); + flushOutgoing(); + } + + bool closeTransport(bool sendCloseFrame) + { + bool expected = false; + if (!isClosed.compare_exchange_strong(expected, true)) + return false; + + clearWebSocketPollState(pollState, activePollEvents); + pendingSends.clear(); + currentMessage.clear(); + hasCurrentMessage = false; + closePayload.clear(); + + { + std::lock_guard lock(curlMutex); + if (curl) + { + if (sendCloseFrame) + { + size_t sent = 0; + (void)curl_ws_send(curl, "", 0, &sent, 0, CURLWS_CLOSE); + } + + curl_easy_cleanup(curl); + curl = nullptr; + } + + if (headerList) + { + curl_slist_free_all(headerList); + headerList = nullptr; + } + } + + return true; + } + + void closeWithCode(int closeCode = 1000, std::string closeReason = "", bool sendCloseFrame = true) + { + if (!closeTransport(sendCloseFrame)) + return; + + notifyClose(closeCode, std::move(closeReason)); + } + + void closeWithError(std::string error) + { + if (!closeTransport(false)) + return; + + notifyError(std::move(error)); + notifyClose(1006, ""); + } + + void processIncoming() + { + while (!closed()) + { + size_t receivedLength = 0; + const curl_ws_frame* meta = nullptr; + CURLcode result = CURLE_OK; + + { + std::lock_guard lock(curlMutex); + if (!curl) + return; + + result = curl_ws_recv(curl, recvBuffer.data(), recvBuffer.size(), &receivedLength, &meta); + } + + if (closed()) + return; + + if (result == CURLE_AGAIN) + return; + + if (result != CURLE_OK) + { + closeWithError(curl_easy_strerror(result)); + return; + } + + if (!meta) + continue; + + if (meta->flags & CURLWS_CLOSE) + { + closePayload.append(recvBuffer.data(), receivedLength); + + if (meta->bytesleft != 0) + continue; + + auto [closeCode, closeReason] = parseClosePayload(closePayload); + closeWithCode(closeCode, std::move(closeReason)); + return; + } + + if (meta->flags & CURLWS_PING) + { + size_t sent = 0; + std::lock_guard lock(curlMutex); + if (curl) + (void)curl_ws_send(curl, recvBuffer.data(), receivedLength, &sent, 0, CURLWS_PONG); + continue; + } + + if (meta->flags & (CURLWS_TEXT | CURLWS_BINARY | CURLWS_CONT)) + { + if (meta->flags & (CURLWS_TEXT | CURLWS_BINARY)) + { + bool binary = (meta->flags & CURLWS_BINARY) != 0; + if (!hasCurrentMessage) + { + currentMessage.clear(); + currentBinary = binary; + hasCurrentMessage = true; + } + else if (currentBinary != binary) + { + closeWithError("websocket received mixed message types"); + return; + } + } + else if (!hasCurrentMessage) + { + currentMessage.clear(); + currentBinary = false; + hasCurrentMessage = true; + } + + currentMessage.append(recvBuffer.data(), receivedLength); + + if (meta->bytesleft == 0 && !(meta->flags & CURLWS_CONT)) + { + std::string message = std::move(currentMessage); + bool binary = currentBinary; + hasCurrentMessage = false; + + notifyMessage(std::move(message), binary); + } + } + } + } + + void flushOutgoing() + { + while (!pendingSends.empty() && !closed()) + { + PendingSend& pending = pendingSends.front(); + size_t sent = 0; + CURLcode result = CURLE_OK; + + { + std::lock_guard lock(curlMutex); + if (!curl) + { + pendingSends.clear(); + return; + } + + size_t remaining = pending.payload.size() - pending.offset; + const char* data = remaining > 0 ? pending.payload.data() + pending.offset : pending.payload.data(); + + result = curl_ws_send(curl, data, remaining, &sent, 0, pending.binary ? CURLWS_BINARY : CURLWS_TEXT); + } + + if (result == CURLE_AGAIN) + break; + + if (result != CURLE_OK) + { + closeWithError("websocket send failed: " + std::string(curl_easy_strerror(result))); + return; + } + + pending.offset += sent; + + if (pending.payload.empty() || pending.offset >= pending.payload.size()) + { + pendingSends.pop_front(); + continue; + } + + if (sent == 0) + break; + } + + if (!closed() && !updatePollingInterest()) + closeWithError("failed to update websocket polling"); + } + + ~WebSocketConnection() + { + closeTransport(false); + } + + void notifyMessage(std::string message, bool binary); + void notifyClose(int closeCode, std::string closeReason); + void notifyError(std::string error); +}; + +struct WebSocketHandle : std::enable_shared_from_this +{ + Runtime* runtime = nullptr; + std::shared_ptr onOpenRef; + std::shared_ptr onMessageRef; + std::shared_ptr onCloseRef; + std::shared_ptr onErrorRef; + std::weak_ptr connection; + std::atomic hasScheduledClose{false}; + bool isActive = false; + + std::shared_ptr lockConnection() const + { + return connection.lock(); + } + + void attachConnection(const std::shared_ptr& newConnection) + { + connection = newConnection; + newConnection->setOwner(shared_from_this()); + } + + void activate() + { + isActive = true; + } + + bool closed() const + { + auto lockedConnection = lockConnection(); + return !lockedConnection || lockedConnection->closed(); + } + + void scheduleCallback(const std::shared_ptr& callback, std::function argPusher) + { + if (!isActive || !callback || !runtime) + return; + + runtime->scheduleLuauCallback(callback, std::move(argPusher)); + } + + void scheduleCloseCallback(int closeCode = 1000, std::string closeReason = "") + { + bool expected = false; + if (!isActive || !hasScheduledClose.compare_exchange_strong(expected, true)) + return; + + scheduleCallback( + onCloseRef, + [closeCode, closeReason = std::move(closeReason)](lua_State* L) + { + lua_pushinteger(L, closeCode); + lua_pushlstring(L, closeReason.data(), closeReason.size()); + return 2; + } + ); + } + + void handleMessage(std::string message, bool binary) + { + scheduleCallback( + onMessageRef, + [message = std::move(message), binary](lua_State* L) + { + pushWebSocketMessageToLua(L, message, binary); + return 1; + } + ); + } + + void handleClose(int closeCode, std::string closeReason) + { + scheduleCloseCallback(closeCode, std::move(closeReason)); + releaseConnectionKeepAlive(); + } + + void releaseConnectionKeepAlive() + { + if (auto lockedConnection = lockConnection()) + lockedConnection->releaseKeepAliveHandle(); + } + + void handleError(std::string error) + { + scheduleErrorCallback(std::move(error)); + scheduleCloseCallback(1006); + releaseConnectionKeepAlive(); + } + + void scheduleErrorCallback(std::string error) + { + scheduleCallback( + onErrorRef, + [error = std::move(error)](lua_State* L) + { + lua_pushlstring(L, error.data(), error.size()); + return 1; + } + ); + } + + void notifyOpen() + { + scheduleCallback( + onOpenRef, + [](lua_State*) + { + return 0; + } + ); + } + + void send(std::string payload, bool binary) + { + if (auto lockedConnection = lockConnection()) + lockedConnection->enqueueSend(std::move(payload), binary); + } + + void close() + { + closeWithCode(); + } + + void closeWithCode(int closeCode = 1000, std::string closeReason = "", bool sendCloseFrame = true) + { + if (!isActive) + { + if (auto lockedConnection = lockConnection()) + { + lockedConnection->closeTransport(false); + lockedConnection->releaseKeepAliveHandle(); + } + return; + } + + scheduleCloseCallback(closeCode, std::move(closeReason)); + + if (auto lockedConnection = lockConnection()) + { + lockedConnection->closeTransport(sendCloseFrame); + lockedConnection->releaseKeepAliveHandle(); + } + + releaseConnectionKeepAlive(); + } + + ~WebSocketHandle() + { + close(); + + onOpenRef.reset(); + onMessageRef.reset(); + onCloseRef.reset(); + onErrorRef.reset(); + } +}; + +void WebSocketConnection::notifyMessage(std::string message, bool binary) +{ + if (auto handle = owner.lock()) + handle->handleMessage(std::move(message), binary); +} + +void WebSocketConnection::notifyClose(int closeCode, std::string closeReason) +{ + if (auto handle = owner.lock()) + handle->handleClose(closeCode, std::move(closeReason)); +} + +void WebSocketConnection::notifyError(std::string error) +{ + if (auto handle = owner.lock()) + handle->handleError(std::move(error)); +} + +static std::shared_ptr* getWebSocketHandle(lua_State* L, int index) +{ + return static_cast*>(lua_touserdatatagged(L, index, kWebSocketHandleTag)); +} + +int ws_send(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TUSERDATA); + auto* handleStorage = getWebSocketHandle(L, 1); + if (!handleStorage || !(*handleStorage) || (*handleStorage)->closed()) + luaL_errorL(L, "Invalid or closed websocket"); + + WebSocketPayload payload = checkWebSocketPayload(L, 2); + (*handleStorage)->send(std::string(payload.data, payload.length), payload.binary); + return 0; +} + +int ws_close(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TUSERDATA); + auto* handleStorage = getWebSocketHandle(L, 1); + if (!handleStorage || !(*handleStorage)) + luaL_errorL(L, "Invalid websocket"); + + (*handleStorage)->close(); + return 0; +} + +int websocket(lua_State* L) +{ + std::string url = luaL_checkstring(L, 1); + std::vector> headers; + std::shared_ptr onOpenRef; + std::shared_ptr onMessageRef; + std::shared_ptr onCloseRef; + std::shared_ptr onErrorRef; + + if (lua_istable(L, 2)) + { + lua_getfield(L, 2, "headers"); + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + if (lua_isstring(L, -2) && lua_isstring(L, -1)) + headers.emplace_back(lua_tostring(L, -2), lua_tostring(L, -1)); + + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_getfield(L, 2, "onopen"); + if (lua_isfunction(L, -1)) + onOpenRef = std::make_shared(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 2, "onmessage"); + if (lua_isfunction(L, -1)) + onMessageRef = std::make_shared(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 2, "onclose"); + if (lua_isfunction(L, -1)) + onCloseRef = std::make_shared(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 2, "onerror"); + if (lua_isfunction(L, -1)) + onErrorRef = std::make_shared(L, -1); + lua_pop(L, 1); + } + + auto token = getResumeToken(L); + Runtime* runtime = token->runtime; + + runtime->runInWorkQueue( + [token, + runtime, + url = std::move(url), + headers = std::move(headers), + onOpenRef = std::move(onOpenRef), + onMessageRef = std::move(onMessageRef), + onCloseRef = std::move(onCloseRef), + onErrorRef = std::move(onErrorRef)]() mutable + { + CURL* curl = curl_easy_init(); + if (!curl) + { + token->fail("failed to initialize websocket"); + return; + } + + curl_slist* headerList = nullptr; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + net::applySystemCA(curl); + curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 2L); + + for (const auto& headerPair : headers) + { + std::string headerString = headerPair.first + ": " + headerPair.second; + headerList = curl_slist_append(headerList, headerString.c_str()); + } + + if (headerList) + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLE_OK) + { + std::string error = curl_easy_strerror(result); + if (headerList) + curl_slist_free_all(headerList); + curl_easy_cleanup(curl); + token->fail("websocket connect failed: " + error); + return; + } + + curl_socket_t socket = CURL_SOCKET_BAD; + if (curl_easy_getinfo(curl, CURLINFO_ACTIVESOCKET, &socket) != CURLE_OK || socket == CURL_SOCKET_BAD) + { + if (headerList) + curl_slist_free_all(headerList); + curl_easy_cleanup(curl); + token->fail("failed to get websocket socket"); + return; + } + + token->complete( + [runtime, + curl, + socket, + headerList, + onOpenRef = std::move(onOpenRef), + onMessageRef = std::move(onMessageRef), + onCloseRef = std::move(onCloseRef), + onErrorRef = std::move(onErrorRef)](lua_State* L) mutable + { + auto connection = std::make_shared(); + connection->curl = curl; + connection->headerList = headerList; + + auto handle = std::make_shared(); + handle->runtime = runtime; + handle->onOpenRef = std::move(onOpenRef); + handle->onMessageRef = std::move(onMessageRef); + handle->onCloseRef = std::move(onCloseRef); + handle->onErrorRef = std::move(onErrorRef); + handle->attachConnection(connection); + + if (!connection->startPolling(runtime, socket)) + { + connection->closeTransport(false); + connection->releaseKeepAliveHandle(); + luaL_errorL(L, "failed to initialize websocket polling"); + } + + handle->activate(); + + auto* storage = new (static_cast*>( + lua_newuserdatataggedwithmetatable(L, sizeof(std::shared_ptr), kWebSocketHandleTag) + )) std::shared_ptr(handle); + (void)storage; + + handle->notifyOpen(); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} +} // namespace net::client diff --git a/lute/net/src/wscommon.cpp b/lute/net/src/wscommon.cpp new file mode 100644 index 000000000..9d0e77a36 --- /dev/null +++ b/lute/net/src/wscommon.cpp @@ -0,0 +1,27 @@ +#include "wscommon.h" + +#include "lualib.h" + +namespace net +{ + +WebSocketPayload checkWebSocketPayload(lua_State* L, int index) +{ + if (lua_isstring(L, index)) + { + size_t length = 0; + const char* data = lua_tolstring(L, index, &length); + return {data, length, false}; + } + + if (lua_isbuffer(L, index)) + { + size_t length = 0; + void* data = lua_tobuffer(L, index, &length); + return {static_cast(data), length, true}; + } + + luaL_typeerrorL(L, index, "string or buffer"); +} + +} // namespace net diff --git a/lute/net/src/wscommon.h b/lute/net/src/wscommon.h new file mode 100644 index 000000000..71bb3bee4 --- /dev/null +++ b/lute/net/src/wscommon.h @@ -0,0 +1,19 @@ +#pragma once + +#include "lua.h" + +#include + +namespace net +{ + +struct WebSocketPayload +{ + const char* data = nullptr; + size_t length = 0; + bool binary = false; +}; + +WebSocketPayload checkWebSocketPayload(lua_State* L, int index); + +} // namespace net diff --git a/lute/process/CMakeLists.txt b/lute/process/CMakeLists.txt index 449ec1ccf..720b6437e 100644 --- a/lute/process/CMakeLists.txt +++ b/lute/process/CMakeLists.txt @@ -2,11 +2,15 @@ add_library(Lute.Process STATIC) target_sources(Lute.Process PRIVATE include/lute/process.h + include/lute/processhandle.h + include/lute/os_signal.h src/process.cpp + src/processhandle.cpp + src/os_signal.cpp ) target_compile_features(Lute.Process PUBLIC cxx_std_17) target_include_directories(Lute.Process PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Process PRIVATE Lute.Runtime Luau.VM uv_a) +target_link_libraries(Lute.Process PRIVATE Lute.Common Lute.Runtime Luau.VM uv_a) target_compile_options(Lute.Process PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/process/include/lute/os_signal.h b/lute/process/include/lute/os_signal.h new file mode 100644 index 000000000..f06d55e32 --- /dev/null +++ b/lute/process/include/lute/os_signal.h @@ -0,0 +1,34 @@ +#pragma once + +#include "lua.h" + +#include "uv.h" + +#include +#include + +struct Runtime; + +namespace process +{ + +struct SignalHandle +{ + SignalHandle(uv_loop_t* loop, int signum, std::function callback); + + + ~SignalHandle(); + + void close(); + +private: + std::function callback; + bool isClosed = false; + std::unique_ptr uvHandle; +}; + +int signalFunc(lua_State* L); + +} // namespace process + +void registerSignalHandle(lua_State* L); diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index 88c888fd7..f0858eea8 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -1,30 +1,24 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" +#include +#include + // open the library as a standard global luau library -int luaopen_process(lua_State* L); +LUTE_API int luaopen_process(lua_State* L); // open the library as a table on top of the stack -int luteopen_process(lua_State* L); +LUTE_API int luteopen_process(lua_State* L); -namespace process +struct Process : LuteLibrary { + static constexpr const char kName[] = "process"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; -int run(lua_State* L); - -int homedir(lua_State* L); -int cwd(lua_State* L); - -int exitFunc(lua_State* L); - -static const luaL_Reg lib[] = { - {"run", run}, - {"homedir", homedir}, - {"cwd", cwd}, - {"exit", exitFunc}, - - {nullptr, nullptr} + static std::optional getExecPath(std::string* error); }; - -} // namespace process diff --git a/lute/process/include/lute/processhandle.h b/lute/process/include/lute/processhandle.h new file mode 100644 index 000000000..650185ec4 --- /dev/null +++ b/lute/process/include/lute/processhandle.h @@ -0,0 +1,138 @@ +#pragma once + +#include "lute/runtime.h" +#include "lute/uvstream.h" + +#include "lua.h" + +#include "uv.h" + +#include +#include +#include +#include +#include +#include + +namespace process +{ + +extern const std::string kStdioKindDefault; +extern const std::string kStdioKindInherit; +extern const std::string kStdioKindNone; + +struct ProcessOptions +{ + std::string cwd; + std::string stdioKind = kStdioKindDefault; + std::map env; + std::string customShell; // only used by system() +}; + +void convertCRLFtoLF(std::string& str); + +struct ProcessExecutionState +{ + + // Your process is ready to finish: + // a) stdout stream has signalled error or EOF + // b) ditto for stderr stream + // c) Process has finished + static constexpr int kWaitOnNForProcessCompletion = 3; + ProcessExecutionState(int maxCompletions = kWaitOnNForProcessCompletion); + + void signalProcessCompletion(int64_t exitCode, int termSignal); + void signalPipeClose(); + + int64_t getExitCode() const; + int getTermSignal() const; + bool isReadyToComplete() const; + + void signalProcessError(std::string err); + bool hasErrors() const; + std::vector& getErrors(); + + std::string stdoutData; + std::string stderrData; + bool completed; + +private: + // Requires onStreamEnd for stderr/stdout/processExit + std::atomic processCanComplete; + int64_t exitCode; + int termSignal; + std::vector errors; +}; + +/** +The process handle is a tricky class that coordinates a processes' life cycle. +This does not get exposed as a user data. Instead the current thread yields +while the process runs and on completion the current coroutine resumes. This +comment will try to document how process spawning and completion currently +works. + +While running, a state of a running process is tracked by the struct +ProcessExecutionState. This tracks a variable called `processCanComplete`. A +process is allowed to 'complete' when a) the stdout and stderr stream signals +EOF, and b) the processExit callback gets called. Each of the callbacks +decrements `processCanComplete` and then calls `tryComplete`. + +tryComplete now attempts to schedule close callbacks for each of the pipes we +opened - input, output, error pipes. We increment a pending closes variable and +decrement it when the close finishes execution. The process handle stores a self +reference so that it stays alive long enough across all the various libuv +callbacks. We need to ensure this self reference is freed when appropriate. + +Some callouts: + +- Stdiokind makes this tricky! If you spin up a process in the default mode, we + actively create a pipe and initialize it with certain settings. However, child + processes inherit the file descriptors of the parent. This means you have to + be very careful here about calling onRead on the uvutils::PipeStream. A child + process that is Inherit or None will never receive any data as the pipe object + doesn't get connected to the file descriptor (only the parent gets this). + However, the actual pipe object sending EOF is a pre-requisite to closing the + process + +- We need to close stdin/stdout/stderr in order for the process to close + successfully. If you spawn a process in default mode, and then it spawns a + process in inherit mode, there is no api to send data to the child process (we + don't have process interception). The child process will wait for input + indefinitely (which will never come). For that reason, we can pre-emptively + close the pipe in the parent process after spawning, which lets the child see + EOF and avoid hanging indefinitely. When we have an actual process.spawn that + lets you intercept file descriptors, we can revisit this. + */ +struct ProcessHandle +{ + uv_process_t process; + uv_loop_t* loop = nullptr; + + ResumeToken resumeToken; + std::shared_ptr self; + std::atomic pendingCloses{0}; + + std::unique_ptr stdinPipe; + std::unique_ptr stdoutPipe; + std::unique_ptr stderrPipe; + ProcessExecutionState state; + std::string stdioKind; + + uv_process_options_t options; + uv_stdio_container_t stdio[3]; + std::vector processArguments; + std::vector environmentStrings; + std::vector environmentVarString; + + ProcessHandle(lua_State* L, ProcessOptions& opts, std::vector& args, std::string context = "Process Spawn"); + + void spawn(lua_State* L); + void onHandleClose(); + void closePipe(std::unique_ptr& pipe); + void closeHandles(); + static void onProcessExit(uv_process_t* process, int64_t exitStatus, int termSignal); + void tryComplete(std::optional errorMessage = std::nullopt); + void completeProcessExecution(); +}; + +} // namespace process diff --git a/lute/process/src/os_signal.cpp b/lute/process/src/os_signal.cpp new file mode 100644 index 000000000..4fb514485 --- /dev/null +++ b/lute/process/src/os_signal.cpp @@ -0,0 +1,249 @@ +#include "lute/os_signal.h" + +#include "lute/common.h" +#include "lute/runtime.h" +#include "lute/userdatas.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +#include +#include +#include +#include +#include + +namespace process +{ + +// These signals cannot be caught or ignored at the OS level. +static const std::unordered_set kUncatchableSignals = {"SIGKILL", "SIGSTOP"}; + +// These signals are generated synchronously by hardware exceptions, and thus cannot be safely handled from Luau code. +static const std::unordered_set kHardwareSynchronousSignals = {"SIGBUS", "SIGFPE", "SIGSEGV", "SIGILL"}; + +// Returns the signal number for a name. +// Returns -1 for signals that are known but not available on this platform (no-op handle). +// Calls luaL_error for signals that are always invalid (uncatchable, reserved, or unsafe). +static int resolveSignal(lua_State* L, const char* name) +{ + // Uncatchable signals — always an error regardless of platform + if (kUncatchableSignals.count(name)) + luaL_errorL(L, "%s cannot be handled", name); + + // Reserved by libuv for child process tracking + if (strcmp(name, "SIGCHLD") == 0) + luaL_errorL(L, "%s is reserved by the runtime", name); + + // Hardware synchronous signals — unsafe to handle from Luau + if (kHardwareSynchronousSignals.count(name)) + luaL_errorL(L, "%s is a synchronous hardware signal and cannot be safely handled", name); + + // Always-available signals + if (strcmp(name, "SIGINT") == 0) + return SIGINT; + if (strcmp(name, "SIGTERM") == 0) + return SIGTERM; + + // Platform-conditional signals: return the signal number if available, + // or -1 (no-op handle) if not defined on this platform. + if (strcmp(name, "SIGHUP") == 0) +#ifdef SIGHUP + return SIGHUP; +#else + return -1; +#endif + + if (strcmp(name, "SIGQUIT") == 0) +#ifdef SIGQUIT + return SIGQUIT; +#else + return -1; +#endif + + if (strcmp(name, "SIGUSR1") == 0) +#ifdef SIGUSR1 + return SIGUSR1; +#else + return -1; +#endif + + if (strcmp(name, "SIGUSR2") == 0) +#ifdef SIGUSR2 + return SIGUSR2; +#else + return -1; +#endif + + if (strcmp(name, "SIGWINCH") == 0) +#ifdef SIGWINCH + return SIGWINCH; +#else + return -1; +#endif + + if (strcmp(name, "SIGPIPE") == 0) +#ifdef SIGPIPE + return SIGPIPE; +#else + return -1; +#endif + + if (strcmp(name, "SIGBREAK") == 0) +#ifdef SIGBREAK + return SIGBREAK; +#else + return -1; +#endif + + if (strcmp(name, "SIGALRM") == 0) +#ifdef SIGALRM + return SIGALRM; +#else + return -1; +#endif + + luaL_errorL(L, "unknown signal: %s", name); +} + +SignalHandle::SignalHandle(uv_loop_t* loop, int signum, std::function callback) + : callback(std::move(callback)) +{ + if (signum < 0) + return; + + uvHandle = std::make_unique(); + uvHandle->data = this; + + int err = uv_signal_init(loop, uvHandle.get()); + if (err != 0) + { + uvHandle.reset(); + return; + } + + err = uv_signal_start( + uvHandle.get(), + [](uv_signal_t* h, int) + { + auto* self = static_cast(h->data); + self->callback(); + }, + signum + ); + + if (err != 0) + { + uv_signal_stop(uvHandle.get()); + this->callback = nullptr; + auto raw = uvHandle.release(); + + uv_close(reinterpret_cast(raw), [](uv_handle_t* h) { delete reinterpret_cast(h); }); + } +} + +void SignalHandle::close() +{ + if (isClosed) + return; + + isClosed = true; + + if (!uvHandle) + return; + + uv_signal_stop(uvHandle.get()); + callback = nullptr; + auto raw = uvHandle.release(); + + uv_close( + reinterpret_cast(raw), + [](uv_handle_t* h) + { + delete reinterpret_cast(h); + } + ); +} + +SignalHandle::~SignalHandle() +{ + close(); +} + +static int closeSignalHandle(lua_State* L) +{ + auto* handle = static_cast(lua_touserdatatagged(L, 1, kSignalHandleTag)); + if (!handle) + luaL_errorL(L, "invalid signal handle"); + + handle->close(); + return 0; +} + +int signalFunc(lua_State* L) +{ + const char* name = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + int signum = resolveSignal(L, name); + + auto* runtime = getRuntime(L); + auto callbackReference = std::make_shared(L, 2); + + void* storage = lua_newuserdatataggedwithmetatable(L, sizeof(SignalHandle), kSignalHandleTag); + new (storage) SignalHandle( + getRuntimeLoop(L), + signum, + [runtime, callbackReference]() + { + runtime->scheduleLuauCallback( + callbackReference, + [](lua_State*) -> int + { + return 0; + } + ); + } + ); + + return 1; +} + +} // namespace process + +void registerSignalHandle(lua_State* L) +{ + luaL_newmetatable(L, "SignalHandle"); + + lua_pushcfunction( + L, + [](lua_State* L) -> int + { + const char* key = luaL_checkstring(L, 2); + if (strcmp(key, "close") == 0) + { + lua_pushcfunction(L, process::closeSignalHandle, "SignalHandle.close"); + return 1; + } + return 0; + }, + "SignalHandle.__index" + ); + lua_setfield(L, -2, "__index"); + + lua_pushstring(L, "SignalHandle"); + lua_setfield(L, -2, "__type"); + + lua_setuserdatadtor( + L, + kSignalHandleTag, + [](lua_State*, void* ud) + { + std::destroy_at(static_cast(ud)); + } + ); + + lua_setuserdatametatable(L, kSignalHandleTag); +} diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 7ec3cdcec..a0a02be2b 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -1,438 +1,185 @@ #include "lute/process.h" + +#include "lute/common.h" +#include "lute/processhandle.h" #include "lute/runtime.h" -#include -#include -#include -#include -#include -#include +#include "lute/uvutils.h" +#include "lute/os_signal.h" + #include "Luau/Common.h" #include "lua.h" #include "lualib.h" +#include "uv.h" + +#include // IWYU pragma: keep +#include +#include +#include +#ifdef PATH_MAX +#define LUTE_PATH_MAX PATH_MAX +#else +#define LUTE_PATH_MAX 8192 +#endif + namespace process { -struct ProcessHandle +static int pidFunc(lua_State* L) { - uv_process_t process; - uv_pipe_t stdoutPipe; - uv_pipe_t stderrPipe; - uv_loop_t* loop = nullptr; - std::string stdoutData; - std::string stderrData; - int64_t exitCode = -1; - int termSignal = 0; - bool completed = false; - ResumeToken resumeToken; - std::shared_ptr self; - std::atomic pendingCloses{0}; - - ~ProcessHandle() {} - - void closeHandles() - { - auto closeCb = [](uv_handle_t* handle) - { - ProcessHandle* ph = static_cast(handle->data); - if (--ph->pendingCloses == 0) - { - ph->self.reset(); - } - }; - - if (stdoutPipe.loop && uv_is_active((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)) - { - pendingCloses++; - uv_read_stop((uv_stream_t*)&stderrPipe); - uv_close((uv_handle_t*)&stderrPipe, closeCb); - } - if (process.loop) - { - pendingCloses++; - uv_close((uv_handle_t*)&process, closeCb); - } - - if (pendingCloses == 0) - { - self.reset(); - } - } - - void triggerCompletion(bool success, const std::string& error_msg = "") - { - if (completed) - return; - completed = true; - - closeHandles(); - - if (!resumeToken) - { - return; - } - - if (success) - { - int64_t finalExitCode = exitCode; - int finalTermSignal = termSignal; - std::string finalStdout = stdoutData; - std::string finalStderr = stderrData; - std::string finalSignalStr = finalTermSignal ? std::to_string(finalTermSignal) : ""; - - resumeToken->complete( - [=](lua_State* L) - { - lua_createtable(L, 0, 5); // ok, exitCode, stdout, stderr, signal - - bool ok = (finalExitCode == 0 && finalTermSignal == 0); - - lua_pushboolean(L, ok); - lua_setfield(L, -2, "ok"); - - lua_pushinteger(L, finalExitCode); - lua_setfield(L, -2, "exitcode"); - - lua_pushlstring(L, finalStdout.c_str(), finalStdout.length()); - lua_setfield(L, -2, "stdout"); - - lua_pushlstring(L, finalStderr.c_str(), finalStderr.length()); - lua_setfield(L, -2, "stderr"); - - if (!finalSignalStr.empty()) - { - lua_pushstring(L, finalSignalStr.c_str()); - } - else - { - lua_pushnil(L); - } - lua_setfield(L, -2, "signal"); - - return 1; - } - ); - } - else - { - resumeToken->fail("Process error: " + error_msg); - } - - resumeToken.reset(); - } -}; + lua_pushinteger(L, uv_os_getpid()); + return 1; +} -static void onProcessExit(uv_process_t* process, int64_t exitStatus, int termSignal) +// helper function for run() and system() +int executionHelper(lua_State* L, std::vector args, ProcessOptions opts) { - ProcessHandle* handle = static_cast(process->data); - if (!handle || handle->completed) - return; - - handle->exitCode = exitStatus; - handle->termSignal = termSignal; - - handle->triggerCompletion(true); + auto handle = std::make_shared(L, opts, args, "Process Spawn"); + handle->self = handle; + handle->spawn(L); + return lua_yield(L, 0); } -static void onPipeRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) +ProcessOptions parseOptions(lua_State* L, int index) { - ProcessHandle* handle = static_cast(stream->data); + ProcessOptions opts; - if (!handle || handle->completed) + if (lua_isnoneornil(L, index)) { - if (buf->base) - free(buf->base); - return; + return opts; // use defaults } - if (nread > 0) - { - std::string* targetBuffer = (stream == (uv_stream_t*)&handle->stdoutPipe) ? &handle->stdoutData : &handle->stderrData; - targetBuffer->append(buf->base, nread); - } - else if (nread < 0) + if (!lua_istable(L, index)) { - if (nread != UV_EOF) - { - std::string errorDetails = (stream == (uv_stream_t*)&handle->stdoutPipe) ? "stdout" : "stderr"; - errorDetails += " read error: "; - errorDetails += uv_strerror(nread); - handle->triggerCompletion(false, errorDetails); - } + luaL_error(L, "process options must be a table"); } - if (buf->base) + lua_getfield(L, index, "system"); + if (!lua_isnil(L, -1)) { - free(buf->base); + opts.customShell = luaL_checkstring(L, -1); } -} + lua_pop(L, 1); -static void allocBuffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) -{ - buf->base = (char*)malloc(suggestedSize); - buf->len = buf->base ? suggestedSize : 0; - if (!buf->base) + lua_getfield(L, index, "cwd"); + if (!lua_isnil(L, -1)) { - fprintf(stderr, "Process pipe buffer allocation failed!\n"); + opts.cwd = luaL_checkstring(L, -1); } -} + lua_pop(L, 1); -const std::string kStdioKindDefault = "default"; -const std::string kStdioKindInherit = "inherit"; -const std::string kStdioKindNone = "none"; -// TODO: add forwarding -// const std::string kStdioKindForward = "forward"; - -int run(lua_State* L) -{ - std::vector args; - if (lua_istable(L, 1)) + lua_getfield(L, index, "stdio"); + if (!lua_isnil(L, -1)) { - int len = lua_objlen(L, 1); - for (int i = 1; i <= len; i++) + opts.stdioKind = luaL_checkstring(L, -1); + if (opts.stdioKind != kStdioKindNone && opts.stdioKind != kStdioKindDefault && opts.stdioKind != kStdioKindInherit) { - lua_rawgeti(L, 1, i); - args.push_back(lua_tostring(L, -1)); - lua_pop(L, 1); + luaL_error(L, "Invalid stdio kind: %s", opts.stdioKind.c_str()); } } 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; + opts.stdioKind = kStdioKindDefault; } - bool useShell = false; - std::string customShell; - std::string cwd; - std::string stdioKind; - std::map env; + lua_pop(L, 1); - if (lua_istable(L, 2)) + lua_getfield(L, index, "env"); + if (!lua_isnil(L, -1)) { - 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); + opts.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) + else { - char shellBuffer[1024]; - size_t shellSize = sizeof(shellBuffer); - int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); - shell = result == 0 ? shellBuffer : shellFallback; + luaL_error(L, "process option 'env' must be a table"); } - - args = {shell, shellArg, commandStr}; } + lua_pop(L, 1); - auto handle = std::make_shared(); - handle->loop = uv_default_loop(); - handle->self = handle; - - uv_process_options_t options = {}; - options.exit_cb = onProcessExit; - options.file = args[0].c_str(); + return opts; +} - std::vector processArgsPtr; - for (const auto& arg : args) +int run(lua_State* L) +{ + if (!lua_istable(L, 1)) { - processArgsPtr.push_back(const_cast(arg.c_str())); + luaL_error(L, "process.run expects a table of arguments as the first parameter"); } - processArgsPtr.push_back(nullptr); - options.args = processArgsPtr.data(); - std::vector envStrings; - std::vector envPtr; - if (!env.empty()) - { - // Copy current environment into the new environment - uv_env_item_t* currentEnvItems; - int currentEnvCount; - int err = uv_os_environ(¤tEnvItems, ¤tEnvCount); - if (err != 0) - { - 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()) - { - env[currentEnvItems[i].name] = currentEnvItems[i].value; - } - } - // Turn the new environment into a char** array - envStrings.reserve(env.size()); - envPtr.reserve(env.size() + 1); - for (const auto& pair : env) - { - envStrings.push_back(pair.first + "=" + pair.second); - } - for (auto& str : envStrings) - { - envPtr.push_back(&str[0]); - } - envPtr.push_back(nullptr); - options.env = envPtr.data(); - } - - if (!cwd.empty()) + std::vector args; + int len = lua_objlen(L, 1); + for (int i = 1; i <= len; i++) { - options.cwd = cwd.c_str(); + lua_rawgeti(L, 1, i); + args.push_back(luaL_checkstring(L, -1)); + lua_pop(L, 1); } - uv_pipe_init(handle->loop, &handle->stdoutPipe, 0); - uv_pipe_init(handle->loop, &handle->stderrPipe, 0); - - options.stdio_count = 3; - uv_stdio_container_t stdio[3]; - stdio[0].flags = UV_IGNORE; - if (stdioKind == kStdioKindNone) - { - stdio[1].flags = UV_IGNORE; - stdio[2].flags = UV_IGNORE; - } - else if (stdioKind == kStdioKindInherit) + if (args.empty()) { - stdio[1].flags = UV_INHERIT_FD; - stdio[1].data.fd = fileno(stdout); - stdio[2].flags = UV_INHERIT_FD; - stdio[2].data.fd = fileno(stderr); + luaL_error(L, "process.run requires a non-empty table of arguments"); } - else if (stdioKind == kStdioKindDefault || stdioKind.empty()) + if (args[0].empty()) { - stdio[1].flags = static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); - stdio[1].data.stream = (uv_stream_t*)&handle->stdoutPipe; - stdio[2].flags = static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); - stdio[2].data.stream = (uv_stream_t*)&handle->stderrPipe; + luaL_error(L, "process.run requires a non-empty command as the first argument"); } - else - { - luaL_error(L, "Invalid stdio kind: %s", stdioKind.c_str()); - return 0; - } - options.stdio = stdio; - - handle->process.data = handle.get(); - handle->stdoutPipe.data = handle.get(); - handle->stderrPipe.data = handle.get(); - - handle->resumeToken = getResumeToken(L); - int spawnResult = uv_spawn(handle->loop, &handle->process, &options); + ProcessOptions opts = parseOptions(L, 2); + return executionHelper(L, args, opts); +} - if (spawnResult != 0) +int system(lua_State* L) +{ + std::string command = luaL_checkstring(L, 1); + if (command.empty()) { - if (handle->resumeToken) - { - handle->resumeToken->runtime->releasePendingToken(); - handle->resumeToken.reset(); - } - handle->closeHandles(); - - luaL_error(L, "Failed to spawn process: %s", uv_strerror(spawnResult)); - return 0; + luaL_error(L, "process.system requires a non-empty string as the command"); } - uv_read_start((uv_stream_t*)&handle->stdoutPipe, allocBuffer, onPipeRead); - uv_read_start((uv_stream_t*)&handle->stderrPipe, allocBuffer, onPipeRead); - - return lua_yield(L, 0); -} + ProcessOptions opts = parseOptions(L, 2); -int homedir(lua_State* L) -{ - std::string buffer; - - size_t homedir_size = 255; - buffer.reserve(homedir_size); +#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 - int status = uv_os_homedir(buffer.data(), &homedir_size); - if (status == UV_ENOBUFS) + std::string resolvedShell; + if (opts.customShell.empty()) { - // libuv gives us the new size if it's under sized - buffer.reserve(homedir_size); - - status = uv_os_homedir(buffer.data(), &homedir_size); + char shellBuffer[1024]; + size_t shellSize = sizeof(shellBuffer); + int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); + resolvedShell = result == 0 ? shellBuffer : shellFallback; } - - if (status != 0) + else { - luaL_error(L, "failed to get home directory"); - return 1; + resolvedShell = opts.customShell; } - lua_pushstring(L, buffer.c_str()); + 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,39 +187,33 @@ int exitFunc(lua_State* L) { int exitCode = luaL_optinteger(L, 1, 0); - // Exit with the provided code std::exit(exitCode); - LUAU_UNREACHABLE(); + LUTE_UNREACHABLE(); } int cwd(lua_State* L) { - std::string buffer; - - size_t cwd_size = 255; - buffer.reserve(cwd_size); + 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()); - 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); - - status = uv_cwd(buffer.data(), &cwd_size); - } - - if (status != 0) - { - luaL_error(L, "failed to get current working directory"); - return 1; - } + std::string* cwd = result.get_if(); + lua_pushlstring(L, cwd->c_str(), cwd->size()); + return 1; +}; - lua_pushstring(L, buffer.c_str()); +int execPath(lua_State* L) +{ + std::string error; + std::optional execPath = Process::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) { @@ -484,6 +225,9 @@ static int envIndex(lua_State* L) if (err == UV_ENOBUFS) { char* buffer = (char*)malloc(size); + if (!buffer) + luaL_error(L, "out of memory"); + err = uv_os_getenv(key, buffer, &size); if (err == 0) { @@ -557,8 +301,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 +325,7 @@ static int envIter(lua_State* L) sizeof(EnvIter), [](void* ptr) { - static_cast(ptr)->~EnvIter(); + std::destroy_at(static_cast(ptr)); } ); @@ -599,17 +344,47 @@ static int envIter(lua_State* L) static const luaL_Reg processEnvMeta[] = {{"__index", process::envIndex}, {"__newindex", process::envNewindex}, {"__iter", process::envIter}, {nullptr, nullptr}}; -int luaopen_process(lua_State* L) +std::optional Process::getExecPath(std::string* error) { - luaL_register(L, "process", process::lib); - return 1; + // 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; + + char buf[LUTE_PATH_MAX]; + size_t len = sizeof(buf); + + if (int status = uv_exepath(buf, &len); status < 0) + { + if (error) + *error = uv_strerror(status); + return std::nullopt; + } + + cachedPath = std::string(buf, len); + return *cachedPath; } -int luteopen_process(lua_State* L) +const char* const Process::properties[] = {"env", "args"}; + +const luaL_Reg Process::lib[] = { + {"run", process::run}, + {"system", process::system}, + {"homedir", process::homedir}, + {"cwd", process::cwd}, + {"exit", process::exitFunc}, + {"execPath", process::execPath}, + {"onSignal", process::signalFunc}, + {"pid", process::pidFunc}, + {nullptr, nullptr}, +}; + +int Process::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(process::lib)); + lua_createtable(L, 0, std::size(Process::lib) + std::size(Process::properties)); - for (auto& [name, func] : process::lib) + for (auto& [name, func] : Process::lib) { if (!name || !func) break; @@ -624,7 +399,37 @@ int luteopen_process(lua_State* L) lua_setmetatable(L, -2); lua_setfield(L, -2, "env"); - lua_setreadonly(L, -1, 1); + // Create process.args table + Runtime* runtime = getRuntime(L); + if (runtime) + { + lua_createtable(L, static_cast(runtime->args.size()), 0); + for (int i = 0; i < static_cast(runtime->args.size()); ++i) + { + lua_pushlstring(L, runtime->args[i].c_str(), runtime->args[i].size()); + lua_rawseti(L, -2, i + 1); + } + } + else + { + lua_createtable(L, 0, 0); + } + lua_setreadonly(L, -1, 1); // args table + lua_setfield(L, -2, "args"); + + lua_setreadonly(L, -1, 1); // process table + + registerSignalHandle(L); return 1; } + +LUTE_API int luaopen_process(lua_State* L) +{ + return Process::openAsGlobal(L); +} + +LUTE_API int luteopen_process(lua_State* L) +{ + return Process::pushLibrary(L); +} diff --git a/lute/process/src/processhandle.cpp b/lute/process/src/processhandle.cpp new file mode 100644 index 000000000..73349a89a --- /dev/null +++ b/lute/process/src/processhandle.cpp @@ -0,0 +1,324 @@ +#include "lute/processhandle.h" + +#include "lute/uvutils.h" + +#include "lua.h" +#include "lualib.h" + +#include + +#ifdef _WIN32 +#define crossPlatformFileno(stream) _fileno(stream) +#else +#define crossPlatformFileno(stream) fileno(stream) +#endif + +namespace process +{ + +const std::string kStdioKindDefault = "default"; +const std::string kStdioKindInherit = "inherit"; +const std::string kStdioKindNone = "none"; + +ProcessExecutionState::ProcessExecutionState(int maxCompletions) + : completed(false) + , processCanComplete(maxCompletions) + , exitCode(-1) + , termSignal(0) +{ +} + +void ProcessExecutionState::signalProcessCompletion(int64_t exitCode, int termSignal) +{ + this->exitCode = exitCode; + this->termSignal = termSignal; + processCanComplete--; +} + +void ProcessExecutionState::signalPipeClose() +{ + processCanComplete--; +} + +int64_t ProcessExecutionState::getExitCode() const +{ + return exitCode; +} + +int ProcessExecutionState::getTermSignal() const +{ + return termSignal; +} + +bool ProcessExecutionState::isReadyToComplete() const +{ + return processCanComplete == 0; +} + +void ProcessExecutionState::signalProcessError(std::string err) +{ + errors.emplace_back(err); +} + +bool ProcessExecutionState::hasErrors() const +{ + return !errors.empty(); +} + +std::vector& ProcessExecutionState::getErrors() +{ + return errors; +} + +ProcessHandle::ProcessHandle(lua_State* L, ProcessOptions& opts, std::vector& args, std::string context) + : loop(getRuntimeLoop(L)) + , resumeToken(getResumeToken(L)) + , state(opts.stdioKind == kStdioKindDefault ? ProcessExecutionState::kWaitOnNForProcessCompletion : 1) + , stdioKind(opts.stdioKind) + , options({}) +{ + // When the process finishes, what do we do? + options.exit_cb = ProcessHandle::onProcessExit; + // Program to execute + options.file = args[0].c_str(); + + // Set up all the arguments to the process (args[0] must be passed) + for (const auto& arg : args) + { + processArguments.push_back(const_cast(arg.c_str())); + } + processArguments.push_back(nullptr); + options.args = processArguments.data(); + + // Pass a current working directory, if it exists + // Note - in a separate pass, make these optional + if (!opts.cwd.empty()) + { + options.cwd = opts.cwd.c_str(); + } + + // Set up the processes environment variables + if (!opts.env.empty()) + { + if (auto err = uvutils::getEnvironmentVariables(opts.env)) + { + luaL_error(L, "Failed to get current environment: %s", uv_strerror(*err)); + } + for (const auto& [key, value] : opts.env) + environmentStrings.push_back(key + "=" + value); + for (auto& str : environmentStrings) + environmentVarString.push_back(str.data()); + environmentVarString.push_back(nullptr); + options.env = environmentVarString.data(); + } + + // Setup input output for the process + options.stdio_count = 3; + + if (opts.stdioKind == kStdioKindNone) + { + stdio[0].flags = UV_IGNORE; + stdio[1].flags = UV_IGNORE; + stdio[2].flags = UV_IGNORE; + } + else if (opts.stdioKind == kStdioKindInherit) + { + stdio[0].flags = UV_INHERIT_FD; + stdio[0].data.fd = crossPlatformFileno(stdin); + stdio[1].flags = UV_INHERIT_FD; + stdio[1].data.fd = crossPlatformFileno(stdout); + stdio[2].flags = UV_INHERIT_FD; + stdio[2].data.fd = crossPlatformFileno(stderr); + } + else if (opts.stdioKind == kStdioKindDefault) + { + stdinPipe = std::make_unique(loop, false, context + " stdin pipe"); + stdoutPipe = std::make_unique(loop, false, context + " stdout pipe"); + stderrPipe = std::make_unique(loop, false, context + " stderr pipe"); + + stdio[0].flags = static_cast(UV_CREATE_PIPE | UV_READABLE_PIPE); + stdio[0].data.stream = stdinPipe->getStream(); + stdio[1].flags = static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); + stdio[1].data.stream = stdoutPipe->getStream(); + stdio[2].flags = static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); + stdio[2].data.stream = stderrPipe->getStream(); + } + + options.stdio = stdio; + + process.data = this; +} + +void ProcessHandle::spawn(lua_State* L) +{ + int spawnResult = uv_spawn(loop, &process, &options); + if (spawnResult != 0) + { + resumeToken->runtime->releasePendingToken(); + state.completed = true; // Allow close callbacks to release self + closeHandles(); + luaL_error(L, "Failed to spawn process: %s", uv_strerror(spawnResult)); + } + + if (stdioKind == kStdioKindDefault) + { + // Close our write end of stdin so the child sees EOF instead of + // blocking forever on a read. + closePipe(stdinPipe); + + // If we have created a pipe, then this process must be responsible for closing this pipe + // If we haven't, then these pipes should be considered closed? + stdoutPipe->read( + [this](std::string_view input) + { + state.stdoutData.append(input); + }, + [this](std::optional err) + { + state.signalPipeClose(); + tryComplete(err); + } + ); + + stderrPipe->read( + [this](std::string_view input) + { + state.stderrData.append(input); + }, + [this](std::optional err) + { + state.signalPipeClose(); + tryComplete(err); + } + ); + } +} + +void ProcessHandle::onHandleClose() +{ + --pendingCloses; + if (pendingCloses == 0 && state.completed) + self.reset(); +} + +void ProcessHandle::closePipe(std::unique_ptr& pipe) +{ + if (!pipe) + return; + if (pipe->close( + [this]() + { + onHandleClose(); + } + )) + pendingCloses++; +} + +void ProcessHandle::closeHandles() +{ + closePipe(stdinPipe); + closePipe(stdoutPipe); + closePipe(stderrPipe); + + if (!uv_is_closing((uv_handle_t*)&process)) + { + pendingCloses++; + uv_close( + (uv_handle_t*)&process, + [](uv_handle_t* handle) + { + auto ph = static_cast(handle->data); + ph->onHandleClose(); + } + ); + } + + if (pendingCloses == 0 && state.completed) + self.reset(); +} + +void ProcessHandle::onProcessExit(uv_process_t* process, int64_t exitStatus, int termSignal) +{ + ProcessHandle* handle = static_cast(process->data); + if (!handle || handle->state.completed) + return; + + handle->state.signalProcessCompletion(exitStatus, termSignal); + handle->tryComplete(); +} + +void ProcessHandle::tryComplete(std::optional errorMessage) +{ + if (errorMessage) + { + state.signalProcessError(*errorMessage); + } + if (state.completed) + return; + + if (!state.isReadyToComplete()) + return; + + completeProcessExecution(); +} + +void ProcessHandle::completeProcessExecution() +{ + state.completed = true; + + if (!state.hasErrors()) + { + int64_t finalExitCode = state.getExitCode(); + int finalTermSignal = state.getTermSignal(); + // TODO: should we put any leftover stdin data into the output here? + std::string finalStdout = state.stdoutData; + std::string finalStderr = state.stderrData; + std::string finalSignalStr = finalTermSignal ? std::to_string(finalTermSignal) : ""; + resumeToken->complete( + [=](lua_State* L) + { + lua_createtable(L, 0, 5); // ok, exitCode, stdout, stderr, signal + + bool ok = (finalExitCode == 0 && finalTermSignal == 0); + + lua_pushboolean(L, ok); + lua_setfield(L, -2, "ok"); + + lua_pushinteger(L, finalExitCode); + lua_setfield(L, -2, "exitcode"); + + lua_pushlstring(L, finalStdout.c_str(), finalStdout.length()); + lua_setfield(L, -2, "stdout"); + + lua_pushlstring(L, finalStderr.c_str(), finalStderr.length()); + lua_setfield(L, -2, "stderr"); + + if (!finalSignalStr.empty()) + { + lua_pushlstring(L, finalSignalStr.c_str(), finalSignalStr.size()); + } + else + { + lua_pushnil(L); + } + lua_setfield(L, -2, "signal"); + + return 1; + } + ); + } + else + { + std::string msg = "Process failed because: \n"; + for (const auto& err : state.getErrors()) + { + msg += err + "\n"; + } + resumeToken->fail(msg); + } + + resumeToken.reset(); + closeHandles(); +} + +} // namespace process diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 0059b866c..e629342ee 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -1,24 +1,34 @@ add_library(Lute.Require) target_sources(Lute.Require PRIVATE + include/lute/batteriesvfs.h + 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/batteriesvfs.cpp + 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.Common Lute.Std Lute.CLI.Commands Lute.Batteries Lute.Std Lute.Lute Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Syntax 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/batteriesvfs.h b/lute/require/include/lute/batteriesvfs.h new file mode 100644 index 000000000..510a72f3f --- /dev/null +++ b/lute/require/include/lute/batteriesvfs.h @@ -0,0 +1,24 @@ +#pragma once + +#include "lute/modulepath.h" + +#include + +class BatteriesVfs +{ +public: + NavigationStatus resetToPath(const std::string& path); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& path); + + 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/bundlevfs.h b/lute/require/include/lute/bundlevfs.h new file mode 100644 index 000000000..c2ee2be41 --- /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 luauConfigFiles, 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 luauConfigFiles; + std::optional modulePath; +}; diff --git a/lute/require/include/lute/clivfs.h b/lute/require/include/lute/clivfs.h index 4541a5680..f2f470ee8 100644 --- a/lute/require/include/lute/clivfs.h +++ b/lute/require/include/lute/clivfs.h @@ -16,8 +16,8 @@ class CliVfs std::string getIdentifier() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; - std::optional getConfig() const; + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; private: std::optional modulePath; diff --git a/lute/require/include/lute/filevfs.h b/lute/require/include/lute/filevfs.h index 687a46bbe..76ac87e4b 100644 --- a/lute/require/include/lute/filevfs.h +++ b/lute/require/include/lute/filevfs.h @@ -9,6 +9,7 @@ class FileVfs public: NavigationStatus resetToStdIn(); NavigationStatus resetToPath(const std::string& path); + NavigationStatus jumpToAlias(const std::string& path); NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); @@ -18,9 +19,11 @@ 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: std::optional modulePath; + + NavigationStatus doReset(const std::string& path, std::function handleRelativePath); }; 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..41fc327a1 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -1,5 +1,8 @@ #pragma once +#include "Luau/RequireNavigator.h" + +#include #include #include @@ -10,11 +13,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 +47,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,15 +62,73 @@ 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; std::optional relativePathToTrack; }; + +class ModulePathNavigationContext : public Luau::Require::NavigationContext +{ +public: + ModulePathNavigationContext(ModulePath modulePath) + : current(std::move(modulePath)) + , start(current) + { + } + + ModulePath getCurrentModulePath() const + { + return current; + } + + NavigateResult resetToRequirer() override + { + current = start; + return NavigateResult::Success; + } + + NavigateResult jumpToAlias(const std::string& path) override + { + return NavigateResult::NotFound; + } + + NavigateResult toParent() override + { + return convert(current.toParent()); + } + + NavigateResult toChild(const std::string& component) override + { + return convert(current.toChild(component)); + } + +private: + ModulePath current; + ModulePath start; + + NavigateResult convert(NavigationStatus status) const + { + NavigateResult result = NavigateResult::NotFound; + switch (status) + { + case NavigationStatus::Success: + result = NavigateResult::Success; + break; + case NavigationStatus::Ambiguous: + result = NavigateResult::Ambiguous; + break; + case NavigationStatus::NotFound: + result = NavigateResult::NotFound; + break; + } + return result; + } +}; diff --git a/lute/require/include/lute/options.h b/lute/require/include/lute/options.h index 36c314794..2a9c00e95 100644 --- a/lute/require/include/lute/options.h +++ b/lute/require/include/lute/options.h @@ -3,5 +3,3 @@ #include "Luau/Compiler.h" Luau::CompileOptions copts(); - -bool getCodegenEnabled(); 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..2eeb142d4 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -1,8 +1,12 @@ #pragma once +#include "lute/batteriesvfs.h" +#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 +14,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,15 +52,17 @@ class RequireVfs Disk, Std, Cli, + Bundle, Lute, + Batteries, // Only for internal use }; VFSType vfsType = VFSType::Disk; FileVfs fileVfs; StdLibVfs stdLibVfs; + LuteVfs luteVfs; + BatteriesVfs batteriesVfs; 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..b54c2cad9 --- /dev/null +++ b/lute/require/include/lute/userlandvfs.h @@ -0,0 +1,112 @@ +#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 jumpToAlias(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/batteriesvfs.cpp b/lute/require/src/batteriesvfs.cpp new file mode 100644 index 000000000..662e69c61 --- /dev/null +++ b/lute/require/src/batteriesvfs.cpp @@ -0,0 +1,86 @@ +#include "lute/batteriesvfs.h" + +#include "lute/clibatteries.h" +#include "lute/common.h" +#include "lute/modulepath.h" + +#include "Luau/Common.h" + +#include + +constexpr std::string_view kBatteriesAliasPrefix = "@batteries"; + +static bool isBatteryModule(const std::string& path) +{ + BatteryModuleResult result = getBatteryModule(path); + return result.type == BatteryModuleType::Module; +} + +static std::optional readBatteryModule(const std::string& path) +{ + BatteryModuleResult result = getBatteryModule(path); + if (result.type == BatteryModuleType::Module) + return std::string(result.contents); + + return std::nullopt; +} + +static bool isBatteryDirectory(const std::string& path) +{ + if (path == kBatteriesAliasPrefix) + return true; + + BatteryModuleResult result = getBatteryModule(path); + return result.type == BatteryModuleType::Directory; +} + +NavigationStatus BatteriesVfs::resetToPath(const std::string& path) +{ + std::string filePath = path == kBatteriesAliasPrefix ? "" : path.substr(kBatteriesAliasPrefix.size() + 1); + + if (path.rfind(kBatteriesAliasPrefix, 0) != 0) + return NavigationStatus::NotFound; + + modulePath = ModulePath::create(std::string(kBatteriesAliasPrefix), filePath, isBatteryModule, isBatteryDirectory); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; +} + +NavigationStatus BatteriesVfs::toParent() +{ + LUTE_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus BatteriesVfs::toChild(const std::string& name) +{ + LUTE_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool BatteriesVfs::isModulePresent() const +{ + return getBatteryModule(getIdentifier()).type == BatteryModuleType::Module; +} + +std::string BatteriesVfs::getIdentifier() const +{ + LUTE_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUTE_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional BatteriesVfs::getContents(const std::string& path) const +{ + return readBatteryModule(path); +} + +ConfigStatus BatteriesVfs::getConfigStatus() const +{ + return ConfigStatus::Absent; +} + +std::optional BatteriesVfs::getConfig() const +{ + return std::nullopt; +} diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp new file mode 100644 index 000000000..cb5c9cd05 --- /dev/null +++ b/lute/require/src/bundlevfs.cpp @@ -0,0 +1,215 @@ +#include "lute/bundlevfs.h" + +#include "lute/common.h" + +#include "Luau/Common.h" +#include "Luau/DenseHash.h" + +#include +#include + +constexpr std::string_view kBundlePrefix = "@bundle"; +constexpr std::string_view kBundlePrefixPath = "@bundle/"; + +namespace +{ + +std::string getConfigPathFromBundlePath(const std::string& path) +{ + if (path.rfind(kBundlePrefixPath, 0) == 0) + return path.substr(kBundlePrefixPath.size()); + return path; +} + +} // namespace + +BundleVfs::BundleVfs(Luau::DenseHashMap luauConfigFiles, Luau::DenseHashMap bundleMap) + : filePathToBytecode(std::move(bundleMap)) + , luauConfigFiles(std::move(luauConfigFiles)) +{ +} + +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 == kBundlePrefix) + { + 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; + } + + std::string filePath; + + // Handle "@bundle/path/to/file" + if (path.rfind(kBundlePrefixPath, 0) == 0) + { + filePath = path.substr(kBundlePrefixPath.size()); + } + // Handle "~/path/to/file" + else if (path.size() > 1 && path[0] == '~' && (path[1] == '/' || path[1] == '\\')) + { + filePath = path.substr(2); + } + else + { + return NavigationStatus::NotFound; + } + + 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() +{ + LUTE_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus BundleVfs::toChild(const std::string& name) +{ + LUTE_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool BundleVfs::isModulePresent() const +{ + return isBundleModule(filePathToBytecode, getIdentifier()); +} + +std::string BundleVfs::getIdentifier() const +{ + LUTE_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUTE_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 +{ + LUTE_ASSERT(modulePath); + + const std::string luaurcPath = getConfigPathFromBundlePath(modulePath->getPotentialConfigPath(".luaurc")); + const std::string luauConfigPath = getConfigPathFromBundlePath(modulePath->getPotentialConfigPath(".config.luau")); + + const bool luaurcExists = luauConfigFiles.find(luaurcPath) != nullptr; + const bool luauConfigExists = luauConfigFiles.find(luauConfigPath) != nullptr; + + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) + return ConfigStatus::PresentJson; + + return ConfigStatus::Absent; +} + +std::optional BundleVfs::getConfig() const +{ + LUTE_ASSERT(modulePath); + + const ConfigStatus status = getConfigStatus(); + + std::string configName; + if (status == ConfigStatus::PresentJson) + configName = ".luaurc"; + else if (status == ConfigStatus::PresentLuau) + configName = ".config.luau"; + else + return std::nullopt; + + const std::string configPath = getConfigPathFromBundlePath(modulePath->getPotentialConfigPath(configName)); + + const std::string* configContent = luauConfigFiles.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..8fa6ea683 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -1,11 +1,21 @@ #include "lute/clivfs.h" #include "lute/clicommands.h" - -#include "Luau/Common.h" +#include "lute/common.h" +#include "lute/modulepath.h" #include +constexpr std::string_view kCliAliasPrefix = "@cli"; +constexpr std::string_view kCliConfig = R"( +{ + "aliases": { + "lint": "@std/commands/lint/types", + "transform": "@std/commands/transform/types" + } +} +)"; + static bool isCliModule(const std::string& path) { CliModuleResult result = getCliModule(path); @@ -23,36 +33,34 @@ static std::optional readCliModule(const std::string& path) static bool isCliDirectory(const std::string& path) { + if (path == kCliAliasPrefix) + return true; + CliModuleResult result = getCliModule(path); return result.type == CliModuleType::Directory; } NavigationStatus CliVfs::resetToPath(const std::string& path) { - if (path == "@cli") + if (path.rfind(kCliAliasPrefix, 0) == 0) { - modulePath = ModulePath::create("@cli", "", isCliModule, isCliDirectory); + std::string filePath = path == kCliAliasPrefix ? "" : path.substr(kCliAliasPrefix.size() + 1); + modulePath = ModulePath::create(std::string(kCliAliasPrefix), filePath, isCliModule, isCliDirectory); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } - std::string cliPrefix = "@cli/"; - - if (path.find_first_of(cliPrefix) != 0) - return NavigationStatus::NotFound; - - modulePath = ModulePath::create("@cli", path.substr(cliPrefix.size()), isCliModule, isCliDirectory); - return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + return NavigationStatus::NotFound; } NavigationStatus CliVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus CliVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -63,9 +71,9 @@ bool CliVfs::isModulePresent() const std::string CliVfs::getIdentifier() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } @@ -74,14 +82,12 @@ 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::PresentJson; } -std::optional CliVfs::getConfig() const +std::optional CliVfs::getConfig() const { - // Currently, we do not support .luaurc files in CLI commands. - return std::nullopt; + return kCliConfig; } diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index b9ae194f2..91bafaad0 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -1,13 +1,16 @@ #include "lute/filevfs.h" +#include "lute/common.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 -#include NavigationStatus FileVfs::resetToStdIn() { @@ -16,49 +19,100 @@ NavigationStatus FileVfs::resetToStdIn() return NavigationStatus::NotFound; size_t firstSlash = cwd->find_first_of("\\/"); - LUAU_ASSERT(firstSlash != std::string::npos); + LUTE_ASSERT(firstSlash != std::string::npos); modulePath = ModulePath::create(cwd->substr(0, firstSlash), cwd->substr(firstSlash + 1), isFile, isDirectory, "./"); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } -NavigationStatus FileVfs::resetToPath(const std::string& path) +NavigationStatus FileVfs::doReset(const std::string& path, std::function handleRelativePath) { - 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)) { size_t firstSlash = normalizedPath.find_first_of('/'); - LUAU_ASSERT(firstSlash != std::string::npos); + LUTE_ASSERT(firstSlash != std::string::npos); modulePath = ModulePath::create(normalizedPath.substr(0, firstSlash), normalizedPath.substr(firstSlash + 1), isFile, isDirectory); } else { - std::optional cwd = getCurrentWorkingDirectory(); - if (!cwd) + if (!handleRelativePath(normalizedPath)) return NavigationStatus::NotFound; - - std::string joinedPath = normalizePath(*cwd + "/" + normalizedPath); - - size_t firstSlash = joinedPath.find_first_of("\\/"); - LUAU_ASSERT(firstSlash != std::string::npos); - - modulePath = ModulePath::create(joinedPath.substr(0, firstSlash), joinedPath.substr(firstSlash + 1), isFile, isDirectory, normalizedPath); } return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } +NavigationStatus FileVfs::resetToPath(const std::string& path) +{ + return doReset( + path, + [this](const std::string& normalizedPath) + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + return false; + + std::string joinedPath = normalizePath(*cwd + "/" + normalizedPath); + + size_t firstSlash = joinedPath.find_first_of("\\/"); + LUTE_ASSERT(firstSlash != std::string::npos); + + modulePath = ModulePath::create(joinedPath.substr(0, firstSlash), joinedPath.substr(firstSlash + 1), isFile, isDirectory, normalizedPath); + return true; + } + ); +} + +NavigationStatus FileVfs::jumpToAlias(const std::string& path) +{ + return doReset( + path, + [this](const std::string& normalizedPath) + { + ModulePathNavigationContext nc = ModulePathNavigationContext(*modulePath); + Luau::Require::ErrorHandler er; + Luau::Require::Navigator navigator(nc, er); + Luau::Require::Navigator::Status status = navigator.navigate("@self/" + normalizedPath); + + if (status == Luau::Require::Navigator::Status::ErrorReported) + return false; + + modulePath = nc.getCurrentModulePath(); + return true; + } + ); +} + NavigationStatus FileVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus FileVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -69,17 +123,17 @@ bool FileVfs::isModulePresent() const std::string FileVfs::getFilePath() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.relativePath ? *result.relativePath : result.realPath; } std::string FileVfs::getAbsoluteFilePath() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } @@ -88,14 +142,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()); + LUTE_ASSERT(modulePath); + + 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()); + LUTE_ASSERT(modulePath); + + ConfigStatus status = getConfigStatus(); + LUTE_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)); + + LUTE_UNREACHABLE(); } diff --git a/lute/require/src/lutevfs.cpp b/lute/require/src/lutevfs.cpp new file mode 100644 index 000000000..ec695f572 --- /dev/null +++ b/lute/require/src/lutevfs.cpp @@ -0,0 +1,128 @@ +#include "lute/lutevfs.h" + +#include "lute/common.h" +#include "lute/crypto.h" +#include "lute/fs.h" +#include "lute/io.h" +#include "lute/luau.h" +#include "lute/lutemodules.h" +#include "lute/modulepath.h" +#include "lute/net.h" +#include "lute/process.h" +#include "lute/syntax.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/io.luau"] = luteopen_io; + map["@lute/luau.luau"] = luteopen_luau; + map["@lute/net/init.luau"] = luteopen_net; + map["@lute/net/client.luau"] = luteopen_net_client; + map["@lute/net/server.luau"] = luteopen_net_server; + map["@lute/process.luau"] = luteopen_process; + map["@lute/syntax/cst.luau"] = luteopen_syntax; + map["@lute/syntax/parser.luau"] = luteopen_syntax_parser; + map["@lute/system.luau"] = luteopen_system; + map["@lute/task.luau"] = luteopen_task; + map["@lute/time.luau"] = luteopen_time; + map["@lute/vm.luau"] = luteopen_vm; + return map; +}(); + +static bool isLuteModule(const std::string& path) +{ + return kLuteModules.contains(path); +} + +static bool isLuteDirectory(const std::string& path) +{ + if (path == "@lute") + return true; + + std::string prefix = path + "/"; + for (const auto& [modulePath, open] : kLuteModules) + { + if (modulePath.empty() || !open) + continue; + + if (modulePath.rfind(prefix, 0) == 0) + return true; + } + + return false; +} + +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() +{ + LUTE_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus LuteVfs::toChild(const std::string& name) +{ + LUTE_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool LuteVfs::isModulePresent() const +{ + LUTE_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUTE_ASSERT(result.status == NavigationStatus::Success); + return result.type == ResolvedRealPath::PathType::File; +} + +std::string LuteVfs::getIdentifier() const +{ + LUTE_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUTE_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional LuteVfs::getContents(const std::string& path) const +{ + LuteModuleResult result = getLuteModule(path); + if (result.type == LuteModuleType::Module) + return std::string(result.contents); + return std::nullopt; +} + +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/options.cpp b/lute/require/src/options.cpp index e8c278a34..244023cde 100644 --- a/lute/require/src/options.cpp +++ b/lute/require/src/options.cpp @@ -1,20 +1,12 @@ #include "lute/options.h" -// TODO: this is never set to true today -static bool codegen = false; - Luau::CompileOptions copts() { Luau::CompileOptions result = {}; result.optimizationLevel = 2; - result.debugLevel = 2; - result.typeInfoLevel = 1; + result.debugLevel = 1; + result.typeInfoLevel = 0; result.coverageLevel = 0; return result; } - -bool getCodegenEnabled() -{ - return codegen; -} diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp new file mode 100644 index 000000000..d879f327b --- /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.jumpToAlias(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..3b350f775 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -1,16 +1,17 @@ #include "lute/require.h" -#include "lute/clivfs.h" +#include "lute/common.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 +34,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); + LUTE_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,28 +171,27 @@ 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()) - { - Luau::CodeGen::CompilationOptions nativeOptions; - Luau::CodeGen::compile(ML, -1, nativeOptions); - } + Luau::CodeGen::CompilationOptions nativeOptions; + nativeOptions.flags = Luau::CodeGen::CodeGen_OnlyNativeModules; + Luau::CodeGen::compile(ML, -1, nativeOptions); int status = lua_resume(ML, L, 0); 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 +205,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 +227,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..0642e9c16 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -1,14 +1,23 @@ #include "lute/requirevfs.h" +#include "lute/bundlevfs.h" +#include "lute/common.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)) + : vfsType(VFSType::Cli) + , cliVfs(std::move(cliVfs)) +{ +} + +RequireVfs::RequireVfs(BundleVfs bundleVfs) + : vfsType(VFSType::Bundle) + , bundleVfs(std::move(bundleVfs)) { } @@ -18,13 +27,12 @@ bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkna 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; @@ -34,10 +42,23 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@cli/")) { vfsType = VFSType::Cli; - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); return cliVfs->resetToPath(std::string(requirerChunkname.substr(1))); } + if ((requirerChunkname.size() >= 9 && requirerChunkname.substr(0, 9) == "@@bundle/")) + { + vfsType = VFSType::Bundle; + LUTE_ASSERT(bundleVfs); + return bundleVfs->resetToPath(std::string(requirerChunkname.substr(1))); + } + + if ((requirerChunkname.size() >= 12 && requirerChunkname.substr(0, 12) == "@@batteries/")) + { + vfsType = VFSType::Batteries; + return batteriesVfs.resetToPath(std::string(requirerChunkname.substr(1))); + } + vfsType = VFSType::Disk; if (requirerChunkname == "=stdin") return fileVfs.resetToStdIn(); @@ -50,37 +71,62 @@ 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.jumpToAlias(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: + LUTE_ASSERT(cliVfs); + status = cliVfs->resetToPath(std::string(path)); + break; + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + status = bundleVfs->resetToPath(std::string(path)); + break; + case VFSType::Batteries: + status = batteriesVfs.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) + else if (aliasUnprefixed == "batteries" && (vfsType == VFSType::Cli || vfsType == VFSType::Std || vfsType == VFSType::Batteries)) { - 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; + vfsType = VFSType::Batteries; + return batteriesVfs.resetToPath("@batteries"); } + + 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 +135,43 @@ 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); + LUTE_ASSERT(cliVfs); status = cliVfs->toParent(); break; - case VFSType::Lute: - luaL_error(L, "cannot get the parent of @lute"); + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + status = bundleVfs->toParent(); + break; + case VFSType::Batteries: + status = batteriesVfs.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); + LUTE_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: + LUTE_ASSERT(bundleVfs); + return bundleVfs->toChild(std::string(name)); + case VFSType::Batteries: + return batteriesVfs.toChild(std::string(name)); } - return NavigationStatus::NotFound; } @@ -143,14 +183,17 @@ bool RequireVfs::isModulePresent(lua_State* L) const return fileVfs.isModulePresent(); case VFSType::Std: return stdLibVfs.isModulePresent(); - case VFSType::Cli: - LUAU_ASSERT(cliVfs); - return cliVfs->isModulePresent(); case VFSType::Lute: - luaL_error(L, "@lute is not requirable"); - break; - default: + return luteVfs.isModulePresent(); break; + case VFSType::Cli: + LUTE_ASSERT(cliVfs); + return cliVfs->isModulePresent(); + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + return bundleVfs->isModulePresent(); + case VFSType::Batteries: + return batteriesVfs.isModulePresent(); } return false; @@ -168,11 +211,19 @@ 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); + LUTE_ASSERT(cliVfs); contents = cliVfs->getContents(loadname); break; - default: + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + contents = bundleVfs->getContents(loadname); + break; + case VFSType::Batteries: + contents = batteriesVfs.getContents(loadname); break; } @@ -181,86 +232,123 @@ 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 ""; + LUTE_ASSERT(cliVfs); + chunkname = "@" + cliVfs->getIdentifier(); + break; + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + chunkname = "@" + bundleVfs->getIdentifier(); + break; + case VFSType::Batteries: + chunkname = "@" + batteriesVfs.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 ""; + LUTE_ASSERT(cliVfs); + loadname = cliVfs->getIdentifier(); + break; + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + loadname = bundleVfs->getIdentifier(); + break; + case VFSType::Batteries: + loadname = batteriesVfs.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 ""; + LUTE_ASSERT(cliVfs); + cacheKey = cliVfs->getIdentifier(); + break; + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + cacheKey = bundleVfs->getIdentifier(); + break; + case VFSType::Batteries: + cacheKey = batteriesVfs.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; + LUTE_ASSERT(cliVfs); + status = cliVfs->getConfigStatus(); + break; + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + status = bundleVfs->getConfigStatus(); + break; + case VFSType::Batteries: + status = batteriesVfs.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 +357,20 @@ 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); + LUTE_ASSERT(cliVfs); configContents = cliVfs->getConfig(); break; - default: + case VFSType::Bundle: + LUTE_ASSERT(bundleVfs); + configContents = bundleVfs->getConfig(); + break; + case VFSType::Batteries: + configContents = batteriesVfs.getConfig(); break; } - return configContents ? *configContents : ""; } diff --git a/lute/require/src/stdlibvfs.cpp b/lute/require/src/stdlibvfs.cpp index 6b0e0041d..ff262b647 100644 --- a/lute/require/src/stdlibvfs.cpp +++ b/lute/require/src/stdlibvfs.cpp @@ -1,5 +1,6 @@ #include "lute/stdlibvfs.h" +#include "lute/common.h" #include "lute/modulepath.h" #include "lute/stdlib.h" @@ -49,13 +50,13 @@ NavigationStatus StdLibVfs::resetToPath(const std::string& path) NavigationStatus StdLibVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus StdLibVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -66,9 +67,9 @@ bool StdLibVfs::isModulePresent() const std::string StdLibVfs::getIdentifier() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } @@ -77,10 +78,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..703f216fa --- /dev/null +++ b/lute/require/src/userlandvfs.cpp @@ -0,0 +1,290 @@ +#include "lute/userlandvfs.h" + +#include "lute/common.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(); + LUTE_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)); + + LUTE_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::jumpToAlias(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.jumpToAlias(path); +} + +NavigationStatus UserlandVfs::toAliasFallback(std::string_view aliasUnprefixed) +{ + std::vector availableDependencies; + switch (vfsType) + { + case VFSType::Disk: + availableDependencies = directDependencies; + break; + case VFSType::Subtree: + LUTE_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: + LUTE_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: + LUTE_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: + LUTE_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: + LUTE_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: + LUTE_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: + LUTE_ASSERT(currentSubtree); + path = currentSubtree->getCurrentPath(); + break; + } + return path; +} + +} // namespace Package diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 632d89ffd..9815e977e 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -4,12 +4,18 @@ target_sources(Lute.Runtime PRIVATE include/lute/ref.h include/lute/runtime.h include/lute/userdatas.h + include/lute/uvrequest.h + include/lute/uvstream.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 PUBLIC Lute.Common) +target_link_libraries(Lute.Runtime PRIVATE Luau.Require Luau.Compiler Luau.VM uv_a) target_compile_options(Lute.Runtime PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 8b8d94e29..fd76f3ce9 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -1,7 +1,12 @@ #pragma once -#include "Luau/Variant.h" #include "lute/ref.h" +#include "lute/reporter.h" + +#include "Luau/Variant.h" +#include "Luau/VecDeque.h" + +#include "uv.h" #include #include @@ -9,11 +14,16 @@ #include #include #include -#include +#include #include struct lua_State; +namespace Luau +{ +struct CompileOptions; +} + struct ThreadToContinue { bool success = false; @@ -22,6 +32,13 @@ struct ThreadToContinue std::function cont; }; +// Optional completion hook for threads that need native follow-up work once +// they finish or error after one or more yields. +struct ThreadCompletionHandler +{ + std::function onFinish; +}; + struct StepErr { lua_State* L; @@ -40,7 +57,7 @@ using RuntimeStep = Luau::Variant; struct Runtime { - Runtime(); + Runtime(LuteReporter& reporter); ~Runtime(); bool runToCompletion(); @@ -64,12 +81,44 @@ struct Runtime // Resume thread with the results computed by the continuation void scheduleLuauResume(std::shared_ptr ref, std::function cont); + // Invoke a Luau callback function on a fresh thread. + // argPusher should push arguments onto the stack and return the count. + void scheduleLuauCallback(std::shared_ptr callbackRef, std::function argPusher); + + // Attach native follow-up work to a specific Luau thread. The hook will run + // once that thread eventually returns or errors after any number of yields. + void addThreadCompletionHandler(lua_State* L, ThreadCompletionHandler completion); + // Run 'f' in a libuv work queue void runInWorkQueue(std::function f); void addPendingToken(); void releasePendingToken(); + uv_loop_t* getEventLoop(); + + // Load and run bytecode. + // If provided, onLoaded is called after bytecode is loaded + // with the loaded function at the top of the stack. + bool runBytecode( + const std::string& bytecode, + const std::string& chunkname, + int argc = 0, + char** argv = nullptr, + std::function onLoaded = nullptr + ); + + // Compile Luau source and run it. + bool runSource( + const std::string& source, + const Luau::CompileOptions& compileOptions, + const std::string& chunkname = "=stdin", + int argc = 0, + char** argv = nullptr + ); + + LuteReporter& reporter; + // VM for this runtime std::unique_ptr globalState; @@ -79,21 +128,34 @@ struct Runtime std::mutex dataCopyMutex; std::unique_ptr dataCopy; - std::vector runningThreads; + Luau::VecDeque runningThreads; + + // CLI arguments passed after the script filename + std::vector args; + + // Factory function for creating an identical require context in child + // Runtimes. Set during parent Runtime's setup. + std::function requireContextFactory; private: + bool runThreadCompletionHandler(lua_State* L, int status); + void clearThreadCompletionHandler(lua_State* L); + std::mutex continuationMutex; std::vector> continuations; + std::unordered_map threadCompletionHandlers; - // TODO: can this be handled by libuv? std::atomic stop; std::condition_variable runLoopCv; - std::thread runLoopThread; + uv_thread_t runLoopThread = {}; + bool runLoopThreadStarted = false; std::atomic activeTokens; + uv_loop_t eventLoop; }; Runtime* getRuntime(lua_State* L); +uv_loop_t* getRuntimeLoop(lua_State* L); struct ResumeTokenData; using ResumeToken = std::shared_ptr; @@ -112,4 +174,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..10cfdcda6 100644 --- a/lute/runtime/include/lute/userdatas.h +++ b/lute/runtime/include/lute/userdatas.h @@ -1,7 +1,11 @@ #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 kWebSocketHandleTag = 123; +constexpr int kServerWebSocketHandleTag = 122; +constexpr int kUVFileTag = 121; +constexpr int kSignalHandleTag = 120; diff --git a/lute/runtime/include/lute/uvrequest.h b/lute/runtime/include/lute/uvrequest.h new file mode 100644 index 000000000..5898fe027 --- /dev/null +++ b/lute/runtime/include/lute/uvrequest.h @@ -0,0 +1,137 @@ +#pragma once + +#include "lute/runtime.h" + +#include "uv.h" + +#include +#include +#include + +namespace uvutils +{ + +template +std::string formatUVError(const char* fmt, Args... args) +{ + int size = snprintf(nullptr, 0, fmt, args...); + if (size < 0) + return "Format error"; + std::vector buffer(size + 1); + snprintf(buffer.data(), buffer.size(), fmt, args...); + return std::string(buffer.data()); +} + +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)) + , loop(getRuntimeLoop(L)) + { + req.data = this; + } + + UVRequest(const UVRequest&) = delete; + UVRequest& operator=(const UVRequest&) = delete; + UVRequest(UVRequest&&) = delete; + UVRequest& operator=(UVRequest&&) = delete; + + template + void fail(const char* fmt, Args&&... args) + { + token->fail(formatUVError(fmt, std::forward(args)...)); + } + + template + void succeed(F&& cont) + { + token->complete(std::forward(cont)); + } + + void succeedTrivially() + { + succeed( + [](lua_State* L) + { + return 0; + } + ); + } + + ~UVRequest() + { + cleanup_uv_req(&req); + } + + uv_loop_t* getLoop() + { + return loop; + } + + ResumeToken token; + ReqT req; + uv_loop_t* loop; +}; + + +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 libuv request struct retains a raw pointer to this object via req->data. + // Releasing here intentionally leaks so the object stays alive until the callback fires. + // The callback must call retake() to reclaim ownership; the destructor then runs after + // the Luau coroutine has been scheduled to resume. + 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/uvstream.h b/lute/runtime/include/lute/uvstream.h new file mode 100644 index 000000000..f9f1435ad --- /dev/null +++ b/lute/runtime/include/lute/uvstream.h @@ -0,0 +1,147 @@ +#pragma once + +#include "lute/common.h" + +#include "uv.h" + +#include +#include +#include +#include +#include + +namespace uvutils +{ + +using OnRead = std::function; +using OnStreamEnd = std::function)>; +using OnClose = std::function; + +struct StreamCallbacks +{ + OnRead onRead; + OnStreamEnd onStreamEnd; + OnClose onClose; +}; + +template +struct UVStream +{ + + UVStream(uv_loop_t* loop, std::string handleContext = "") + : stream(std::make_unique()) + , loop(loop) + + , handleContext(std::move(handleContext)) + , closed(false) + { + + stream->data = this; + } + + UVStream(const UVStream&) = delete; + UVStream& operator=(const UVStream&) = delete; + UVStream(UVStream&&) = delete; + UVStream& operator=(UVStream&&) = delete; + + uv_stream_t* getStream() + { + return (uv_stream_t*)stream.get(); + } + + void read(OnRead&& onRead, OnStreamEnd&& onStreamEnd) + { + callbacks.onRead = std::move(onRead); + callbacks.onStreamEnd = std::move(onStreamEnd); + uv_read_start(getStream(), allocBuffer, readStream); + } + + bool isClosed() const + { + return closed; + } + + // Returns true if the close callback was registered successfully, false if the handle is already closed or closing. + bool close(OnClose&& onClose) + { + if (isClosed()) + return false; + if (uv_is_closing((uv_handle_t*)getStream())) + return false; + callbacks.onClose = std::move(onClose); + uv_read_stop(getStream()); + uv_close( + (uv_handle_t*)getStream(), + [](uv_handle_t* handle) + { + auto stream = static_cast*>(handle->data); + LUTE_ASSERT(stream); + stream->closed = true; + stream->callbacks.onClose(); + } + ); + return true; + } + + static void allocBuffer(uv_handle_t* handle, size_t newSize, uv_buf_t* buf) + { + buf->base = (char*)malloc(newSize); + buf->len = buf->base ? newSize : 0; + auto uvStream = static_cast*>(handle->data); + LUTE_ASSERT(uvStream); + if (!buf->base) + { + uvStream->callbacks.onStreamEnd("Failed to allocate memory"); + } + } + + static void readStream(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) + { + auto handle = static_cast*>(stream->data); + if (!handle) + { + if (buf->base) + free(buf->base); + return; + } + + if (nread > 0) + { + handle->callbacks.onRead(std::string_view(buf->base, nread)); + } + else if (nread < 0) + { + std::optional error = std::nullopt; + if (nread != UV_EOF) + { + std::string msg = handle->handleContext + ": " + uv_strerror(nread); + error.emplace(msg); + } + handle->callbacks.onStreamEnd(error); + } + + if (buf->base) + { + free(buf->base); + } + } + + std::unique_ptr stream; + uv_loop_t* loop; + StreamCallbacks callbacks; + std::string handleContext; + bool closed = false; +}; + + +struct PipeStream : UVStream +{ + PipeStream(uv_loop_t* loop, bool ipc, std::string handleContext) + : UVStream(loop, handleContext) + { + uv_pipe_init(loop, stream.get(), ipc); + } +}; + + +} // namespace uvutils diff --git a/lute/runtime/include/lute/uvutils.h b/lute/runtime/include/lute/uvutils.h new file mode 100644 index 000000000..3b774cfda --- /dev/null +++ b/lute/runtime/include/lute/uvutils.h @@ -0,0 +1,31 @@ +#pragma once + +#include "Luau/Variant.h" + +#include "uv.h" + +#include +#include +#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); + +// Grabs environment variables +std::optional getEnvironmentVariables(std::map& map); + +} // 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..a35d11012 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -1,24 +1,18 @@ #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 "lute/common.h" +#include "lute/ref.h" + +#include "Luau/Compiler.h" #include "lua.h" +#include "luacode.h" #include "lualib.h" #include "uv.h" -#include #include +#include static void lua_close_checked(lua_State* L) { @@ -26,12 +20,19 @@ static void lua_close_checked(lua_State* L) lua_close(L); } -Runtime::Runtime() - : globalState(nullptr, lua_close_checked) +Runtime::Runtime(LuteReporter& reporter) + : reporter(reporter) + , globalState(nullptr, lua_close_checked) , dataCopy(nullptr, lua_close_checked) { + stop.store(false); activeTokens.store(0); + + if (uv_loop_init(&eventLoop) < 0) + { + LUTE_ASSERT("Couldn't initialize runtime event loop"); + } } Runtime::~Runtime() @@ -44,18 +45,29 @@ Runtime::~Runtime() runLoopCv.notify_one(); } - if (runLoopThread.joinable()) - runLoopThread.join(); + if (runLoopThreadStarted) + { + uv_thread_join(&runLoopThread); + runLoopThreadStarted = false; + } + // At this point, Runtime::hasWork will have returned false (i.e uv_loop_alive is false) + // This means there are no outstanding handles, or file descriptors or work, to do, and we can exit + uv_loop_close(&eventLoop); } 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(getEventLoop()); } RuntimeStep Runtime::runOnce() { - uv_run(uv_default_loop(), UV_RUN_DEFAULT); + uv_run_mode mode = (hasContinuations() || hasThreads()) ? UV_RUN_NOWAIT : UV_RUN_ONCE; + uv_run(getEventLoop(), mode); // Complete all C++ continuations std::vector> copy; @@ -73,14 +85,14 @@ 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); if (L == nullptr) { - fprintf(stderr, "Cannot resume a non-thread reference"); + reporter.reportError("Cannot resume a non-thread reference"); return StepErr{L}; } @@ -89,6 +101,26 @@ RuntimeStep Runtime::runOnce() int status = LUA_OK; + // It's possible for a spawned task to be killed by a coroutine.close() + // before it gets processed in the runningThreads queue. This leads to situations where a thread was scheduled to resume + // but has already been killed. + + // One example: + // 1) Main thread executes task.defer on a coroutine.create thread + // 2) Code is queued up on the thread + // 3) coroutine.cancel is invoked + // 4) runtime evaluates callbacks + // 5) runtime evaluates running threads <- UH OH, found a thread that was scheduled to resume but has already been killed + // 6) We can just step over it, because + // a) if it scheduled a resume, the corresponding pending token will have been cleared + // b) the corresponding ref for the lua state will be freed at the end of Runtime::runOnce() + int co_status = lua_costatus(GL, L); + if (co_status == LUA_COFIN) + { + clearThreadCompletionHandler(L); + return StepSuccess{L}; + } + if (!next.success) status = lua_resumeerror(L, nullptr); else @@ -99,6 +131,12 @@ RuntimeStep Runtime::runOnce() return StepSuccess{L}; } + bool ranCompletionHandler = runThreadCompletionHandler(L, status); + // Completion handlers are responsible for consuming/reporting terminal errors + // for threads they own (for example, turning a failed HTTP handler into a 500). + if (ranCompletionHandler && status != LUA_OK) + return StepSuccess{L}; + if (status != LUA_OK) { return StepErr{L}; @@ -120,7 +158,7 @@ bool Runtime::runToCompletion() { if (err->L == nullptr) { - fprintf(stderr, "lua_State* L is nullptr"); + reporter.reportError("lua_State* L is nullptr"); return false; } @@ -149,34 +187,38 @@ void Runtime::reportError(lua_State* L) error += "\nstacktrace:\n"; error += lua_debugtrace(L); - fprintf(stderr, "%s", error.c_str()); + reporter.reportError(error); } void Runtime::runContinuously() { - // TODO: another place for libuv - runLoopThread = std::thread( - [this] + runLoopThreadStarted = uv_thread_create( + &runLoopThread, + [](void* arg) { - while (!stop) + Runtime* self = static_cast(arg); + while (!self->stop) { - // Block to wait on event { - std::unique_lock lock(continuationMutex); + std::unique_lock lock(self->continuationMutex); - runLoopCv.wait( + self->runLoopCv.wait( lock, - [this] + [self] { - return !continuations.empty() || stop; + return !self->continuations.empty() || self->stop; } ); } - runToCompletion(); + self->runToCompletion(); } - } - ); + }, + this + ) == 0; + + if (!runLoopThreadStarted) + LUTE_ASSERT("Failed to create runtime runloop thread"); } bool Runtime::hasContinuations() @@ -190,6 +232,31 @@ bool Runtime::hasThreads() return !runningThreads.empty(); } +void Runtime::addThreadCompletionHandler(lua_State* L, ThreadCompletionHandler completion) +{ + threadCompletionHandlers[L] = std::move(completion); +} + +bool Runtime::runThreadCompletionHandler(lua_State* L, int status) +{ + auto it = threadCompletionHandlers.find(L); + if (it == threadCompletionHandlers.end()) + return false; + + ThreadCompletionHandler completion = std::move(it->second); + clearThreadCompletionHandler(L); + + if (completion.onFinish) + completion.onFinish(L, status); + + return true; +} + +void Runtime::clearThreadCompletionHandler(lua_State* L) +{ + threadCompletionHandlers.erase(L); +} + void Runtime::schedule(std::function f) { std::unique_lock lock(continuationMutex); @@ -229,18 +296,42 @@ void Runtime::scheduleLuauResume(std::shared_ptr ref, std::function f) +void Runtime::scheduleLuauCallback(std::shared_ptr callbackRef, std::function argPusher) { - auto loop = uv_default_loop(); + std::unique_lock lock(continuationMutex); + + continuations.push_back( + [this, callbackRef = std::move(callbackRef), argPusher = std::move(argPusher)]() mutable + { + lua_State* newThread = lua_newthread(GL); + std::shared_ptr threadRef = getRefForThread(newThread); + lua_pop(GL, 1); + + callbackRef->push(newThread); + int nargs = argPusher(newThread); + + runningThreads.push_back({true, threadRef, nargs}); + } + ); + runLoopCv.notify_one(); +} + +void Runtime::runInWorkQueue(std::function f) +{ + auto loop = getEventLoop(); uv_work_t* work = new uv_work_t(); work->data = new decltype(f)(std::move(f)); @@ -272,9 +363,19 @@ void Runtime::releasePendingToken() assert(before > 0); } +uv_loop_t* Runtime::getEventLoop() +{ + return &eventLoop; +} + Runtime* getRuntime(lua_State* L) { - return reinterpret_cast(lua_getthreaddata(lua_mainthread(L))); + return static_cast(lua_getthreaddata(lua_mainthread(L))); +} + +uv_loop_t* getRuntimeLoop(lua_State* L) +{ + return getRuntime(L)->getEventLoop(); } void ResumeTokenData::fail(std::string error) @@ -298,39 +399,60 @@ void ResumeTokenData::complete(std::function cont) ResumeToken getResumeToken(lua_State* L) { ResumeToken token = std::make_shared(); - token->runtime = getRuntime(L); token->ref = getRefForThread(L); - token->runtime->addPendingToken(); return token; } -static void luteopen_libs(lua_State* L) +bool Runtime::runBytecode(const std::string& bytecode, const std::string& chunkname, int argc, char** argv, std::function onLoaded) { - 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_State* L = lua_newthread(GL); + + luaL_sandboxthread(L); + + if (luau_load(L, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) != 0) + { + reportError(L); + lua_pop(GL, 1); + return false; + } + + if (onLoaded) + onLoaded(L); + + if (argc > 0 && argv != nullptr) { - lua_pushcfunction(L, luarequire_registermodule, nullptr); - lua_pushstring(L, name); - func(L); - lua_call(L, 2, 0); + if (!lua_checkstack(L, argc)) + { + fprintf(stderr, "Failed to pass arguments to Luau\n"); + lua_pop(GL, 1); + return false; + } + + for (int i = 0; i < argc; ++i) + lua_pushstring(L, argv[i]); } + + args.clear(); + for (int i = 0; i < argc; ++i) + args.emplace_back(argv[i]); + + runningThreads.push_back({true, getRefForThread(L), argc}); + + lua_pop(GL, 1); + + return runToCompletion(); } -lua_State* setupState(Runtime& runtime, void (*doBeforeSandbox)(lua_State*)) +bool Runtime::runSource(const std::string& source, const Luau::CompileOptions& compileOptions, const std::string& chunkname, int argc, char** argv) +{ + std::string bytecode = Luau::compile(source, compileOptions); + return runBytecode(bytecode, chunkname, argc, argv); +} + +lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox) { // Separate VM for data copies runtime.dataCopy.reset(luaL_newstate()); @@ -346,8 +468,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/uvrequest.cpp b/lute/runtime/src/uvrequest.cpp new file mode 100644 index 000000000..7ab4e94be --- /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/uvutils.cpp b/lute/runtime/src/uvutils.cpp new file mode 100644 index 000000000..815027b3b --- /dev/null +++ b/lute/runtime/src/uvutils.cpp @@ -0,0 +1,61 @@ +#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; +} + +std::optional getEnvironmentVariables(std::map& map) +{ + uv_env_item_t* currentEnvItems; + int currentEnvCount; + int err = uv_os_environ(¤tEnvItems, ¤tEnvCount); + if (err != 0) + { + uv_os_free_environ(currentEnvItems, currentEnvCount); + return err; + } + + for (int i = 0; i < currentEnvCount; i++) + { + auto var = currentEnvItems[i]; + if (var.name && var.value && map.find(var.name) == map.end()) + map[var.name] = var.value; + } + uv_os_free_environ(currentEnvItems, currentEnvCount); + + return std::nullopt; +} + +} // namespace uvutils diff --git a/lute/std/CMakeLists.txt b/lute/std/CMakeLists.txt index fcbdc909b..0c240ddc9 100644 --- a/lute/std/CMakeLists.txt +++ b/lute/std/CMakeLists.txt @@ -1,12 +1,18 @@ add_library(Lute.Std STATIC) -target_sources(Lute.Std PRIVATE - include/lute/stdlib.h - - src/stdlib.cpp - src/generated/modules.h - src/generated/modules.cpp -) +if (LUTE_STDLESS) + target_sources(Lute.Std PRIVATE + include/lute/stdlib.h + src/stdlib_stub.cpp + ) +else() + target_sources(Lute.Std PRIVATE + include/lute/stdlib.h + src/stdlib.cpp + src/generated/modules.h + src/generated/modules.cpp + ) +endif() target_compile_features(Lute.Std PUBLIC cxx_std_17) target_include_directories(Lute.Std PUBLIC "include") diff --git a/lute/std/libs/.luaurc b/lute/std/libs/.luaurc new file mode 100644 index 000000000..62e27d4e6 --- /dev/null +++ b/lute/std/libs/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../../../batteries" + } +} diff --git a/lute/std/libs/commands/lint/types.luau b/lute/std/libs/commands/lint/types.luau new file mode 100644 index 000000000..263ee08c2 --- /dev/null +++ b/lute/std/libs/commands/lint/types.luau @@ -0,0 +1,34 @@ +local path = require("@std/path") +local syntax = require("@std/syntax") + +export type severity = "error" | "warning" | "info" | "hint" +export type tag = "unnecessary" | "deprecated" + +export type LintViolation = { + lintname: string, + location: syntax.Span, + message: string, + severity: severity, + sourcepath: path.Path?, + suggestedfix: { + read location: syntax.Span?, -- if nil, applies to whole violation location + read fix: string, + }?, + target: string?, -- Additional information on the violation (e.g link to docs) + tags: { tag }?, +} + +export type GlobalsConfig = { [string]: unknown } +export type RuleOptions = { [string]: unknown } + +export type RuleContext = { + globals: GlobalsConfig, + options: RuleOptions, +} + +export type LintRule = { + read lint: (cst: syntax.CstStatBlock, sourcepath: path.Path?, context: RuleContext) -> { LintViolation }, + read name: string, +} + +return {} diff --git a/lute/std/libs/commands/transform/types.luau b/lute/std/libs/commands/transform/types.luau new file mode 100644 index 000000000..50a5273d3 --- /dev/null +++ b/lute/std/libs/commands/transform/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.CstParseResult, + 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/std/libs/fs.luau b/lute/std/libs/fs.luau new file mode 100644 index 000000000..e162fd11b --- /dev/null +++ b/lute/std/libs/fs.luau @@ -0,0 +1,256 @@ +--!strict +-- @std/fs +-- stdlib for `@lute/fs` +-- Provides file system utility functions that handles file paths as path types instead of strings + +local deque = require("@batteries/collections/deque") +local fs = require("@lute/fs") +local pathlib = require("@std/path") +local sys = require("@std/system") +local win32paths = require("@std/path/win32") + +local fslib = {} + +--- How a file should be opened: +--- - `"r"`: Open for reading. Fails if the file does not exist. +--- - `"w"`: Open for writing; creates the file if absent, truncates it if present. +--- - `"x"`: Open for exclusive creation. Fails if the file already exists. +--- - `"a"`: Open for appending; creates the file if absent, writes go to the end. +--- - `"r+"`: Open for reading and writing. Fails if the file does not exist. +--- - `"w+"`: Open for reading and writing; creates the file if absent, truncates it if present. +--- - `"x+"`: Open for reading and exclusive creation. Fails if the file already exists. +--- - `"a+"`: Open for reading and appending; creates the file if absent, writes go to the end. +export type HandleMode = fs.HandleMode +--- An open file handle. +export type File = fs.FileHandle +--- The type of a filesystem entry: +--- - `"file"`: A regular file. +--- - `"dir"`: A directory. +--- - `"link"`: A symbolic link. +--- - `"fifo"`: A named pipe (FIFO). +--- - `"socket"`: A Unix domain socket. +--- - `"char"`: A character device. +--- - `"block"`: A block device. +--- - `"unknown"`: An entry whose type could not be determined. +export type FileType = fs.FileType +--- Metadata about a filesystem entry, such as size, type, and timestamps. +export type FileMetadata = fs.FileMetadata +--- A single entry returned by `listDirectory`, containing the entry's name and type. +export type DirectoryEntry = fs.DirectoryEntry +--- A handle to an active filesystem watcher, returned by `watch`. +export type WatchHandle = fs.WatchHandle +--- A filesystem change event, yielded by a `Watcher`. +export type WatchEvent = fs.WatchEvent + +type Pathlike = pathlib.Pathlike +type Path = pathlib.Path +type win32path = win32paths.Path + +--- Options for `createDirectory`: +--- - `makeParents`: If `true`, creates any missing parent directories. Defaults to `false`. +export type CreateDirectoryOptions = { + makeParents: boolean?, +} + +--- Options for `removeDirectory`: +--- - `recursive`: If `true`, removes all contents of the directory before deleting it. Defaults to `false`. +export type RemoveDirectoryOptions = { + recursive: boolean?, +} + +--- Options for `walk`: +--- - `recursive`: If `true`, descends into subdirectories. Defaults to `false`. +export type WalkOptions = { + recursive: boolean?, +} + +type Watcher = { + next: (Watcher) -> WatchEvent?, + close: (self: Watcher) -> (), +} + +--- Opens the file at `path` in the given `mode` (defaults to `"r"`). Returns a file handle. +function fslib.open(path: Pathlike, mode: HandleMode?): File + return fs.open(pathlib.format(path), mode) +end + +--- Reads the full contents of `file` and returns them as a string. +function fslib.read(file: File): string + return fs.read(file) +end + +--- Writes `contents` to `file`. +function fslib.write(file: File, contents: string): () + return fs.write(file, contents) +end + +--- Closes `file`, flushing any pending writes. +function fslib.close(file: File): () + return fs.close(file) +end + +--- Removes the file at `path`. +function fslib.remove(path: Pathlike): () + return fs.remove(pathlib.format(path)) +end + +--- Returns metadata for the file or directory at `path`. +function fslib.metadata(path: Pathlike): FileMetadata + return fs.stat(pathlib.format(path)) +end + +--- Returns the `FileType` of the entry at `path` (e.g. `"file"`, `"dir"`, `"symlink"`). +function fslib.type(path: Pathlike): FileType + return fs.type(pathlib.format(path)) +end + +--- Creates a hard link at `dest` pointing to `src`. +function fslib.link(src: Pathlike, dest: Pathlike): () + return fs.link(pathlib.format(src), pathlib.format(dest)) +end + +--- Creates a symbolic link at `dest` pointing to `src`. +function fslib.symbolicLink(src: Pathlike, dest: Pathlike): () + return fs.symlink(pathlib.format(src), pathlib.format(dest)) +end + +--- Watches `path` for filesystem changes. Returns a `Watcher` with a `next` method that returns the next +--- `WatchEvent`, or `nil` if none is available, and a `close` method to stop watching. +--- Note: a while loop must be used rather than a for loop. See example/watch_directory.luau for usage. +function fslib.watch(path: Pathlike): Watcher + -- In the future this could be written with explicit instantiation syntax + -- as in: + -- + -- deque.new<<{ filename: path, event: watchevent }>>() + local deq = deque.new(nil :: { filename: Path, event: WatchEvent }?) + local handle = fs.watch(pathlib.format(path), function(filename, event) + deq:pushback({ filename = pathlib.parse(filename), event = event }) + end) + + return { + next = function(_self: Watcher): WatchEvent? + if not deq:peekfront() then + return nil + end + local item = deq:popfront() + assert(item) + return item.event + end, + close = function(_self: Watcher): () + if handle then + handle:close() + end + end, + } +end + +--- Returns `true` if a file or directory exists at `path`. +function fslib.exists(path: Pathlike): boolean + return fs.exists(pathlib.format(path)) +end + +--- Copies the file at `src` to `dest`. +function fslib.copy(src: Pathlike, dest: Pathlike): () + return fs.copy(pathlib.format(src), pathlib.format(dest)) +end + +--- Moves the file or directory at `src` to `dest`. +--- Falls back to a copy-then-remove if `src` and `dest` are on different filesystems. +function fslib.move(src: Pathlike, dest: Pathlike): () + return fs.move(pathlib.format(src), pathlib.format(dest)) +end + +--- Returns an array of `DirectoryEntry` values for the immediate children of the directory at `path`. +function fslib.listDirectory(path: Pathlike): { DirectoryEntry } + return fs.listdir(pathlib.format(path)) +end + +--- Returns the entire contents of the file at `filepath` as a string. +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 + +--- Writes `contents` to the file at `filepath`, replacing any existing contents. +function fslib.writeStringToFile(filepath: Pathlike, contents: string): () + local f = fs.open(pathlib.format(filepath), "w+") + fs.write(f, contents) + fs.close(f) +end + +--- Creates a directory at `path`. If `options.makeParents` is `true`, creates any missing parent directories as well. +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 :: win32path) + 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 + +--- Removes the directory at `path`. If `options.recursive` is `true`, removes all contents first. +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 + +--- Returns an iterator over all paths under `path`. If `options.recursive` is `true`, descends into subdirectories. +--- See example/walk_directory.luau for usage. +function fslib.walk(path: Pathlike, options: WalkOptions?): () -> Path? + local queue = deque.new(pathlib.parse(path)) + local recursive = if options then options.recursive else false + + return function() + if #queue == 0 then + return nil + end + + local current = queue:popfront() + + if fslib.type(current) == "dir" and recursive then + local entries = fslib.listDirectory(current) + for _, entry in entries do + queue:pushback(pathlib.join(current, entry.name)) + end + end + + return current + 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..d89366263 --- /dev/null +++ b/lute/std/libs/io.luau @@ -0,0 +1,35 @@ +--!strict +-- @std/io +-- stdlib for `@lute/io` +-- Provides standard input, output, and other I/O utility functions + +local io = require("@lute/io") + +local iolib = {} + +--- Writes one or more strings to standard output without a trailing newline. +function iolib.write(...: string): () + io.write(...) +end + +--- Reads a line from standard input. If `prompt` is provided, it is written to stdout before waiting for input. +--- 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 + io.write(prompt) + end + + local line = io.read() + + -- Strip a single trailing line terminator ("\r\n", "\n", or "\r"), leaving any + -- interior or leading whitespace intact. + if line:sub(-2) == "\r\n" then + return line:sub(1, -3) + elseif line:sub(-1) == "\n" or line:sub(-1) == "\r" then + return line:sub(1, -2) + end + + return line +end + +return table.freeze(iolib) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau new file mode 100644 index 000000000..9666b3b64 --- /dev/null +++ b/lute/std/libs/json.luau @@ -0,0 +1,678 @@ +--!strict +--!native +-- @std/json +-- JSON serialization and deserialization library + +local json = { + --- A sentinel value representing JSON `null`. Not actually `nil` — use this when you need to serialize a null field. + null = newproxy() :: nil, +} + +--- A JSON object: a table with string keys. +export type Object = { [string]: Value } +--- A JSON array: a table with integer keys. +export type Array = { [number]: Value } +--- Any JSON value: `null`, a number, string, boolean, array, or object. +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 = 0 +local keyCacheLimit = 128 +local keyCacheKeySizeLimit = 64 + +type SerializerState = { + buf: buffer, + cursor: number, + prettyPrint: boolean, + depth: number, + -- Per-serialize-call cache of escaped object keys, including their colon separator. + keyCache: { [string]: string }, + keyCacheCount: number, +} + +local function growBuffer(state: SerializerState, curLen: number): () + local newBuffer = buffer.create(curLen) + + buffer.copy(newBuffer, 0, state.buf) + + state.buf = newBuffer +end + +local function checkState(state: SerializerState, len: number): () + local cursor = state.cursor + local requiredLen = cursor + len + local bits = bit32.countlz(requiredLen) + + if bit32.countlz(cursor) == bits then + return + end + + growBuffer(state, 2 ^ (32 - bits)) +end + +local function writeByte(state: SerializerState, byte: number): () + checkState(state, 1) + + buffer.writeu8(state.buf, state.cursor, byte) + + state.cursor += 1 +end + +local function writeSpaces(state: SerializerState): () + if not state.prettyPrint or state.depth == 0 then + return + end + + checkState(state, state.depth * 4) + + for _ = 1, state.depth do + buffer.writeu32(state.buf, state.cursor, 0x_20_20_20_20) + state.cursor += 4 + end +end + +local function writeString(state: SerializerState, str: string): () + local sl = #str + checkState(state, sl) + + local cursor = state.cursor + buffer.writestring(state.buf, cursor, str) + + state.cursor = cursor + sl +end + +local serializeAny: (SerializerState, (Array | Object | boolean | number | string)?) -> () + +local STRING_ESCAPES: { [string]: string } = { + ["\\"] = "\\\\", + ['"'] = '\\"', + ["\b"] = "\\b", + ["\f"] = "\\f", + ["\n"] = "\\n", + ["\r"] = "\\r", + ["\t"] = "\\t", +} +for i = 0, 31 do + local c = string.char(i) + if not STRING_ESCAPES[c] then + STRING_ESCAPES[c] = string.format("\\u%04x", i) + end +end + +local STRING_NEEDS_ESCAPE = '[%z\1-\31"\\]' + +local function serializeString(state: SerializerState, str: string): () + local escaped = if string.find(str, STRING_NEEDS_ESCAPE) + then string.gsub(str, STRING_NEEDS_ESCAPE, STRING_ESCAPES) + else str + + local sl = #escaped + checkState(state, sl + 2) + local buf = state.buf + local cursor = state.cursor + buffer.writeu8(buf, cursor, 0x22) + buffer.writestring(buf, cursor + 1, escaped) + buffer.writeu8(buf, cursor + 1 + sl, 0x22) + state.cursor = cursor + sl + 2 +end + +local function serializeKeyString(state: SerializerState, str: string): () + local cached = state.keyCache[str] + if cached ~= nil then + writeString(state, cached) + return + end + + local escaped: string + if string.find(str, STRING_NEEDS_ESCAPE) then + escaped = string.gsub(str, STRING_NEEDS_ESCAPE, STRING_ESCAPES) + else + escaped = str + end + cached = '"' .. escaped .. (if state.prettyPrint then '": ' else '":') + if state.keyCacheCount < keyCacheLimit and #str <= keyCacheKeySizeLimit then + state.keyCache[str] = cached + state.keyCacheCount += 1 + end + + writeString(state, cached) +end + +local function serializeArray(state: SerializerState, array: Array): () + state.depth += 1 + + writeByte(state, string.byte("[")) + + if state.prettyPrint and #array ~= 0 then + writeByte(state, string.byte("\n")) + end + + for i, value in array do + if i ~= 1 then + writeByte(state, string.byte(",")) + + if state.prettyPrint then + writeByte(state, string.byte("\n")) + end + end + + writeSpaces(state) + + serializeAny(state, value) + end + + state.depth -= 1 + + if state.prettyPrint and #array ~= 0 then + writeByte(state, string.byte("\n")) + writeSpaces(state) + end + + writeByte(state, string.byte("]")) +end + +local function serializeTable(state: SerializerState, object: Object): () + writeByte(state, string.byte("{")) + + if state.prettyPrint then + writeByte(state, string.byte("\n")) + end + + state.depth += 1 + + local first = true + for key, value in object do + if key == object_key then + continue + end + + if not first then + writeByte(state, string.byte(",")) + + if state.prettyPrint then + writeByte(state, string.byte("\n")) + end + end + + first = false + + writeSpaces(state) + + serializeKeyString(state, key) + + serializeAny(state, value) + end + + if state.prettyPrint then + writeByte(state, string.byte("\n")) + end + + state.depth -= 1 + + writeSpaces(state) + + writeByte(state, string.byte("}")) +end + +serializeAny = function(state: SerializerState, value: Value): () + if value == json.null then + writeString(state, "null") + elseif typeof(value) == "boolean" then + writeString(state, if value then "true" else "false") + elseif typeof(value) == "number" then + if not math.isfinite(value) then + error("Cannot serialize non-finite number", 2) + end + + writeString(state, tostring(value)) + elseif typeof(value) == "string" then + serializeString(state, value) + elseif typeof(value) == "table" then + if #value == 0 and next(value :: { [unknown]: unknown }) ~= nil then + serializeTable(state, value :: Object) + else + serializeArray(state, value :: Array) + end + else + error("Unknown value", 2) + end +end + +-- deserialization + +type DeserializerState = { + src: string, + cursor: number, +} + +local function deserializerError(state: DeserializerState, msg: string): never + return error(`JSON error - {msg} around {state.cursor}`) +end + +local function skipWhitespace(state: DeserializerState): boolean + local b = string.byte(state.src, state.cursor) + if b == nil then + return false + end + if b ~= 0x20 and b ~= 0x09 and b ~= 0x0A and b ~= 0x0D then + return true + end + + state.cursor = string.find(state.src, "[^ \n\r\t]", state.cursor) :: number + if not state.cursor then + return false + end + return true +end + +local function currentByte(state: DeserializerState): number + return (string.byte(state.src, state.cursor)) +end + +local function deserializeNumber(state: DeserializerState): number? + local src = state.src + local nStart = state.cursor + local cursor = nStart + local b = string.byte(src, cursor) + local sign = 1 + local intValue = 0 + local digitCount = 0 + local isInteger = true + + if b == 0x2D then -- '-' + sign = -1 + cursor += 1 + b = string.byte(src, cursor) + end + + if b == 0x30 then -- '0' + digitCount = 1 + cursor += 1 + b = string.byte(src, cursor) + if b ~= nil and b >= 0x30 and b <= 0x39 then + deserializerError(state, "Malformed number (leading zeros not allowed)") + end + elseif b ~= nil and b >= 0x31 and b <= 0x39 then + repeat + digitCount += 1 + if digitCount <= 15 then + intValue = intValue * 10 + ((b :: number) - 0x30) + end + cursor += 1 + b = string.byte(src, cursor) + until b == nil or b < 0x30 or b > 0x39 + else + deserializerError(state, "Malformed number (bad integer part)") + end + + if b == 0x2E then -- '.' + isInteger = false + cursor += 1 + b = string.byte(src, cursor) + if b == nil or b < 0x30 or b > 0x39 then + deserializerError(state, "Malformed number (bad decimal part)") + end + repeat + cursor += 1 + b = string.byte(src, cursor) + until b == nil or b < 0x30 or b > 0x39 + end + + if b == 0x65 or b == 0x45 then -- 'e' or 'E' + isInteger = false + cursor += 1 + b = string.byte(src, cursor) + if b == 0x2B or b == 0x2D then -- '+' or '-' + cursor += 1 + b = string.byte(src, cursor) + end + if b == nil or b < 0x30 or b > 0x39 then + deserializerError(state, "Malformed number (bad exponent part)") + end + repeat + cursor += 1 + b = string.byte(src, cursor) + until b == nil or b < 0x30 or b > 0x39 + end + + if isInteger and digitCount <= 15 then + state.cursor = cursor + return sign * intValue + end + + local num = tonumber(string.sub(src, nStart, cursor - 1)) + if not num then + deserializerError(state, "Malformed number value") + end + + state.cursor = cursor + return num +end + +local UNESCAPE: { [string]: string } = { + ['"'] = '"', + ["\\"] = "\\", + ["/"] = "/", + b = "\b", + f = "\f", + n = "\n", + r = "\r", + t = "\t", +} + +local function decodeUnicodeEscape(hex: string, cursor: number): string + local cp = tonumber(hex, 16) :: number + if cp >= 0xD800 and cp <= 0xDBFF then + error(`JSON error - Incomplete surrogate pair around {cursor}`) + end + if cp >= 0xDC00 and cp <= 0xDFFF then + error(`JSON error - Lone low surrogate around {cursor}`) + end + return utf8.char(cp) +end + +local function decodeSurrogatePair(highHex: string, lowHex: string, cursor: number): string + local high = tonumber(highHex, 16) :: number + local low = tonumber(lowHex, 16) :: number + if low < 0xDC00 or low > 0xDFFF then + error(`JSON error - Invalid unicode surrogate pair around {cursor}`) + end + local codepoint = 0x10000 + ((high - 0xD800) * 0x400) + (low - 0xDC00) + return utf8.char(codepoint) +end + +local function deserializeString(state: DeserializerState): string + if currentByte(state) ~= string.byte('"') then + return deserializerError(state, "Expected string opening quote") + end + + local startPos = state.cursor + 1 + local src = state.src + + local p = string.find(src, '[\\"%z\1-\31]', startPos) + if not p then + return deserializerError(state, "Unterminated string") + end + local b = string.byte(src, p) + if b == 0x22 then + state.cursor = p + 1 + return string.sub(src, startPos, p - 1) + end + if b ~= 0x5C then + state.cursor = p + return deserializerError(state, "Unescaped control character in string") + end + + local len = #src + while p <= len do + local c = string.byte(src, p) + if c == 0x5C then + p += 2 + elseif c == 0x22 then + break + elseif c < 0x20 then + state.cursor = p + return deserializerError(state, "Unescaped control character in string") + else + p += 1 + end + end + if p > len then + return deserializerError(state, "Unterminated string") + end + + local raw = string.sub(src, startPos, p - 1) + local cursor = state.cursor + + -- Freeze raw `\\` as 0x01 so a `\\u0000` source isn't mis-parsed as + -- backslash + `\u0000`. The scan above guarantees no 0x01 in input. + raw = string.gsub(raw, "\\\\", "\1") + raw = string.gsub( + raw, + "\\u([dD][89aAbB]%x%x)\\u([dD][c-fC-F]%x%x)", + function(h, l) + return decodeSurrogatePair(h, l, cursor) + end :: any + ) + raw = string.gsub(raw, "\\u(%x%x%x%x)", function(code) + return decodeUnicodeEscape(code, cursor) + end) + raw = string.gsub(raw, "\\(.)", function(esc) + local v = UNESCAPE[esc] + if not v then + error(`JSON error - Invalid escape sequence around {cursor}`) + end + return v + end) + raw = string.gsub(raw, "\1", "\\") + + state.cursor = p + 1 + return raw +end + +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 + + state.cursor += 1 + skipWhitespace(state) + + local current: Array = {} + local index = 1 + + -- empty array + if currentByte(state) == string.byte("]") then + state.cursor += 1 + return current + end + + local expectingValue = true + + 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 + + table.insert(current, deserialize(state)) + index += 1 + expectingValue = false + end + + if not skipWhitespace(state) then + return deserializerError(state, "Unterminated array") + end + end + + return deserializerError(state, "Unterminated array") +end + +local function deserializeObject(state: DeserializerState): Object + state.cursor += 1 + + local current = {} + + local expectingValue = false + while state.cursor < #state.src do + skipWhitespace(state) + + if currentByte(state) == string.byte("}") then + break + end + + if currentByte(state) ~= string.byte('"') then + return deserializerError(state, "Expected a string key") + end + + local key = deserializeString(state) + + skipWhitespace(state) + + if currentByte(state) ~= string.byte(":") then + return deserializerError(state, "Expected ':' for key value pair") + end + + state.cursor += 1 + + local value = deserialize(state) + + current[key] = value + + if not skipWhitespace(state) then + deserializerError(state, "Unterminated object") + end + + if currentByte(state) == string.byte(",") then + expectingValue = true + state.cursor += 1 + else + expectingValue = false + end + end + + if expectingValue then + return deserializerError(state, "Trailing comma") + end + + if not skipWhitespace(state) or currentByte(state) ~= string.byte("}") then + deserializerError(state, "Unterminated object") + end + + state.cursor += 1 + + return current +end + +deserialize = function(state: DeserializerState): Value + skipWhitespace(state) + + local src = state.src + local cursor = state.cursor + local b = string.byte(src, cursor) + + if b == 0x22 then -- '"' + return deserializeString(state) + elseif b == 0x7B then -- '{' + local obj = deserializeObject(state) + obj[object_key] = true + return obj + elseif b == 0x5B then -- '[' + return deserializeArray(state) + elseif b == 0x74 then -- 't' -> "true" + local b1, b2, b3 = string.byte(src, cursor + 1, cursor + 3) + if b1 == 0x72 and b2 == 0x75 and b3 == 0x65 then + state.cursor = cursor + 4 + return true + end + elseif b == 0x66 then -- 'f' -> "false" + local b1, b2, b3, b4 = string.byte(src, cursor + 1, cursor + 4) + if b1 == 0x61 and b2 == 0x6C and b3 == 0x73 and b4 == 0x65 then + state.cursor = cursor + 5 + return false + end + elseif b == 0x6E then -- 'n' -> "null" + local b1, b2, b3 = string.byte(src, cursor + 1, cursor + 3) + if b1 == 0x75 and b2 == 0x6C and b3 == 0x6C then + state.cursor = cursor + 4 + return json.null + end + elseif b == 0x2D or (b ~= nil and b >= 0x30 and b <= 0x39) then + return deserializeNumber(state) + end + + return deserializerError(state, `Unexpected token '{string.sub(src, cursor, cursor)}'`) +end + +-- user-facing + +--- Serializes `value` to a JSON string. If `prettyPrint` is `true`, the output is indented for readability. +function json.serialize(value: Value, prettyPrint: boolean?): string + local state: SerializerState = { + buf = buffer.create(bufferSize), + cursor = 0, + prettyPrint = prettyPrint or false, + depth = 0, + keyCache = {}, + keyCacheCount = 0, + } + + serializeAny(state, value) + + return buffer.readstring(state.buf, 0, state.cursor) +end + +--- Parses a JSON string and returns the corresponding Luau value. Returns `json.null` for a JSON `null` literal; compare with `json.null` rather than `nil` to check for null. +function json.deserialize(src: string): (Array | Object | boolean | number | string)? + local state = { + src = src, + cursor = 1, + } + + 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 + +--- Creates a JSON `Object` from `props`. Use this to distinguish an empty object `{}` from an empty array when serializing. +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 + +--- Returns `value` as an `Object` if it is a JSON object, or `nil` otherwise. +function json.asObject(value: Value): Object? + if typeof(value) == "table" and value[object_key] then + return value :: Object + end + + return nil +end + +--- Returns `value` as an `Array` if it is a JSON array, or `nil` otherwise. +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..ea30bfc8c --- /dev/null +++ b/lute/std/libs/luau.luau @@ -0,0 +1,112 @@ +--!strict +-- @std/luau +-- stdlib for `@lute/luau` +-- Provides utilities for parsing and compiling Luau source code + +local cst = require("@lute/syntax/cst") +local luteLuau = require("@lute/luau") +local parser = require("@lute/syntax/parser") + +local fs = require("@lute/fs") +local path = require("@std/path") +local query = require("@std/syntax/query") +local utils = require("@std/syntax/utils") +local printer = require("@std/syntax/printer") + +local luau = {} + +--- Compiled Luau bytecode produced by `luau.compile`. +export type Bytecode = luteLuau.Bytecode + +--- A `require(...)` call found in a source file, with its resolved absolute path and the raw require string. +export type RequireNode = { + resolvedPath: string?, + requirePath: string?, + requireExpr: cst.CstExprCall, +} + +--- Compiles Luau source code into bytecode. +function luau.compile(source: string): Bytecode + return luteLuau.compile(source) +end + +--- Loads `bytecode` into a callable function. `chunkname` names the chunk for error messages. +--- An optional `env` table can be provided to override the global environment for the loaded chunk. +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 + +--- Loads and executes the Luau file at `requirePath`, returning its result. +--- An optional `env` table can be provided to override the global environment. +function luau.loadModule(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 + +--- A Luau type pack, representing a sequence of types. +export type TypePack = luteLuau.TypePack +--- A Luau type, representing the type of a value or expression. +export type Type = luteLuau.Type + +--- Returns the type pack representing the return types of the module at `modulepath`, or `nil` if unavailable. +function luau.typeofModule(modulepath: path.Pathlike): TypePack? + local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) + + return luteLuau.typeofModule(modulePathStr) +end + +--- Resolves the require path `modulepath` relative to `frompath`, returning the absolute path. +function luau.resolveModule(modulepath: string, frompath: path.Pathlike): path.Pathlike + local frompathString = if typeof(frompath) == "string" then frompath else path.format(frompath) + return luteLuau.resolveModule(modulepath, `@{frompathString}`) +end + +--- Returns all `require(...)` calls found in the file at `filePath`, with resolved absolute paths where available. +function luau.requires(filePath: path.Pathlike): { RequireNode } + local filePathStr = if typeof(filePath) == "string" then filePath else path.format(filePath) + + local handle = fs.open(filePathStr, "r") + local source = fs.read(handle) + fs.close(handle) + + local cst = parser.parse(source) + + local requireCalls = query.findAllFromRoot(cst, utils.isRequireCall) + + return requireCalls:mapToArray(function(call: cst.CstExprCall): RequireNode? + local argNode = call.arguments[1] + if argNode == nil then + return nil + end + + local content: string? = nil + if argNode.tag == "string" then + content = (argNode :: cst.CstExprConstantString).value.text + else + content = printer.printNode(argNode) + end + + local resolved: string? = nil + if content ~= nil then + local ok, result = pcall(luteLuau.resolveModule, content, `@{filePathStr}`) + if ok then + resolved = result + end + end + + return { + resolvedPath = resolved, + requirePath = content, + requireExpr = call, + } + end) +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..f91754b53 --- /dev/null +++ b/lute/std/libs/net.luau @@ -0,0 +1,22 @@ +--!strict +-- @std/net +-- stdlib for `@lute/net` + +local client = require("@lute/net/client") + +local netlib = {} + +--- Options for an HTTP request: +--- - `method`: The HTTP method (e.g. `"GET"`, `"POST"`). Defaults to `"GET"`. +--- - `body`: The request body as a string. If omitted, no body is sent. +--- - `headers`: A table of HTTP headers to include in the request. +export type Metadata = client.Metadata +--- An HTTP response, containing status, headers, and body. +export type Response = client.Response + +--- Makes an HTTP request to `url` with optional request metadata. Returns the response. +function netlib.request(url: string, metadata: Metadata?): Response + return client.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..729722623 --- /dev/null +++ b/lute/std/libs/path/init.luau @@ -0,0 +1,110 @@ +local platform = require("@std/system/platform") + +local posix = require("@self/posix") +local win32 = require("@self/win32") + +local pathtypes = require("@self/types") + +local pathlib = {} + +--- A structured, platform-aware file system path. +export type Path = pathtypes.Path +--- Anything that can be used as a path: a string, a `Path`, or raw path data. +export type Pathlike = pathtypes.Pathlike + +local onWindows = platform.win32 + +--- Returns the last component of `path` (the filename or directory name), or `nil` if the path has no components. +function pathlib.basename(path: Pathlike): string? + if onWindows then + return win32.basename(path :: win32.Pathlike) + else + return posix.basename(path :: posix.Pathlike) + end +end + +--- Returns the directory portion of `path` as a string (everything except the last component). +function pathlib.dirname(path: Pathlike): string + if onWindows then + return win32.dirname(path :: win32.Pathlike) + else + return posix.dirname(path :: posix.Pathlike) + end +end + +--- Returns the file extension of `path` (including the leading dot), or `""` if there is none. +function pathlib.extname(path: Pathlike): string + if onWindows then + return win32.extname(path :: win32.Pathlike) + else + return posix.extname(path :: posix.Pathlike) + end +end + +--- Converts `path` to its string representation using the platform's separator. +function pathlib.format(path: Pathlike): string + if onWindows then + return win32.format(path :: win32.Pathlike) + else + return posix.format(path :: posix.Pathlike) + end +end + +--- Returns `true` if `path` is absolute. +function pathlib.isAbsolute(path: Pathlike): boolean + if onWindows then + return win32.isAbsolute(path :: win32.Pathlike) + else + return posix.isAbsolute(path :: posix.Pathlike) + end +end + +--- Joins one or more path segments together into a single `Path`. Each segment after the first must be relative. +function pathlib.join(...: Pathlike): Path + if onWindows then + return win32.join(...) + else + return posix.join(...) + end +end + +--- Returns a normalized form of `path`, resolving `.` and `..` components and removing redundant separators. +function pathlib.normalize(path: Pathlike): Path + if onWindows then + return win32.normalize(path :: win32.Pathlike) + else + return posix.normalize(path :: posix.Pathlike) + end +end + +--- Parses `path` into a structured `Path` value. +function pathlib.parse(path: Pathlike): Path + if onWindows then + return win32.parse(path :: win32.Pathlike) + else + return posix.parse(path :: posix.Pathlike) + end +end + +--- Returns the relative path from `from` to `to`. Both paths must be of the same kind (both absolute or both relative). +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 + +--- Resolves a sequence of paths into an absolute `Path`, processing right-to-left and falling back to the current working directory. +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..90401a9ad --- /dev/null +++ b/lute/std/libs/path/pathinterface.luau @@ -0,0 +1,5 @@ +export type PathInterface = { + __tostring: (self: PathInterface) -> 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..60ddcc2ac --- /dev/null +++ b/lute/std/libs/path/posix/init.luau @@ -0,0 +1,255 @@ +local process = require("@lute/process") +local posixtypes = require("@self/types") +local pathinterface = require("./pathinterface") + +export type Path = posixtypes.Path +export type Pathlike = posixtypes.Pathlike + +local posix = {} +posix.pathmt = {} :: pathinterface.PathInterface + +--- Returns the last component of `path`, or `nil` if the path has no components. +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 + +--- Returns the directory portion of `path` as a string (everything except the last component). +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 + +--- Returns the file extension of `path` (including the leading dot), or `""` if there is none. +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 + +--- Converts `path` to its POSIX string representation. +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 + +--- Returns `true` if `path` is absolute (begins with `/`). +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 + +--- Joins one or more path segments into a single `Path`. Each segment after the first must be relative. +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 + +--- Returns a normalized form of `path`, resolving `.` and `..` components and removing redundant separators. +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 + +--- Parses `path` into a structured `Path` value. +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 + +--- Resolves a sequence of paths into an absolute `Path`, processing right-to-left and falling back to the current working directory. +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 + +--- Returns the relative path from `from` to `to`. Both paths must be of the same kind (both absolute or both relative). +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 _ = 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..b19e567f9 --- /dev/null +++ b/lute/std/libs/path/posix/types.luau @@ -0,0 +1,15 @@ +local pathinterface = require("../pathinterface") + +--- The raw data of a POSIX path: an array of components and a flag indicating whether the path is absolute. +export type PathData = { + parts: { string }, + absolute: boolean, +} + +--- A structured POSIX path. +export type Path = setmetatable + +--- Anything that can be used as a POSIX path: a string, a `Path`, or raw `PathData`. +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..90cecfa16 --- /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 = posixtypes.Pathlike | win32types.Pathlike + +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..77bead06f --- /dev/null +++ b/lute/std/libs/path/win32/init.luau @@ -0,0 +1,326 @@ +local process = require("@lute/process") +local win32types = require("@self/types") +local pathinterface = require("./pathinterface") + +export type PathKind = win32types.PathKind +export type Path = win32types.Path +export type Pathlike = win32types.Pathlike + +local win32 = {} +win32.pathmt = {} :: pathinterface.PathInterface + +--- Returns the last component of `path`, or `nil` if the path has no components. +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 + +--- Returns the directory portion of `path` as a string (everything except the last component). +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 + +--- Returns the file extension of `path` (including the leading dot), or `""` if there is none. +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 + +--- Returns a `Path` representing just the drive root of `path` (e.g. `C:\`). Errors if `path` has no drive letter. +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 + +--- Converts `path` to its Windows string representation using backslash separators. +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 + +--- Returns `true` if `path` is absolute (drive-rooted or UNC). +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 + +--- Joins one or more path segments into a single `Path`. Each segment after the first must be relative. +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 + +--- Returns a normalized form of `path`, resolving `.` and `..` components and removing redundant separators. +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 + +--- Parses `path` into a structured `Path` value. +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 + separator (C:\ or C:/) + local sep = path:sub(3, 3) + kind = if sep == "\\" or sep == "/" 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 + +--- Returns the relative path from `from` to `to`. Both paths must have the same kind and drive. +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 _ = 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 + +--- Resolves a sequence of paths into an absolute `Path`, processing right-to-left and falling back to the current working directory. +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..b8a590b0d --- /dev/null +++ b/lute/std/libs/path/win32/types.luau @@ -0,0 +1,22 @@ +local pathinterface = require("../pathinterface") + +--- Describes how a Windows path is rooted: +--- - `"absolute"`: Drive-rooted path, e.g. `C:\foo`. +--- - `"unc"`: UNC path, e.g. `\\server\share`. +--- - `"relative"`: A path with no drive or UNC root. +export type PathKind = "unc" | "absolute" | "relative" + +--- The raw data of a Windows path: an array of components, the path kind, and an optional drive letter. +export type PathData = { + parts: { string }, + kind: PathKind, + drive: string?, +} + +--- A structured Windows path. +export type Path = setmetatable + +--- Anything that can be used as a Windows path: a string, a `Path`, or raw `PathData`. +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..53cedfdc4 --- /dev/null +++ b/lute/std/libs/process.luau @@ -0,0 +1,83 @@ +--!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 = {} + +--- How a child process's standard streams should be handled: +--- - `"default"`: Capture stdout and stderr to pipes, accessible via `ProcessResult`. +--- - `"inherit"`: Pass the parent process's stdio streams through to the child. +--- - `"none"`: Discard the child's stdio streams. +export type SignalHandle = process.SignalHandle +export type Signal = process.Signal +export type StdioKind = process.StdioKind +--- Options for `process.run`: +--- - `cwd`: The working directory for the child process. Defaults to the current working directory. +--- - `stdio`: How to handle the child's stdio streams. Defaults to `"default"`. +--- - `env`: Environment variables for the child process. If omitted, inherits the parent's environment. +export type ProcessRunOptions = process.ProcessRunOptions +--- Options for `process.system`: +--- - `system`: The shell executable to use. If omitted, uses the platform default (`$SHELL` on Unix, `%COMSPEC%` on Windows). +--- - `cwd`: The working directory for the child process. Defaults to the current working directory. +--- - `stdio`: How to handle the child's stdio streams. Defaults to `"default"`. +--- - `env`: Environment variables for the child process. If omitted, inherits the parent's environment. +export type ProcessSystemOptions = process.ProcessSystemOptions +--- The result of a completed process, including exit code and captured stdout/stderr. +export type ProcessResult = process.ProcessResult +--- A structured, platform-aware file system path. +export type Path = pathlib.Path +--- Anything that can be used as a path: a string, a `Path`, or raw path data. +export type Pathlike = pathlib.Pathlike + +--- Returns the current user's home directory as a `Path`. +function processlib.homedir(): Path + return pathlib.parse(process.homedir()) +end + +--- Returns the current working directory as a `Path`. +function processlib.cwd(): Path + return pathlib.parse(process.cwd()) +end + +--- Runs the program given by `args[1]` with the remaining entries as arguments. +--- Returns the process result including exit code and any captured output. +function processlib.run(args: { string }, options: ProcessRunOptions?): ProcessResult + return process.run(args, options) +end + +--- Runs `command` through the system shell. Returns the process result. +function processlib.system(command: string, options: ProcessSystemOptions?): ProcessResult + return process.system(command, options) +end + +--- Exits the current process with the given `exitcode`. +function processlib.exit(exitcode: number): never + return process.exit(exitcode) +end + +--- Returns the path of the currently running lute executable as a `Path`. +function processlib.execPath(): Path + return pathlib.parse(process.execPath()) +end + +--- Registers `callback` to be called whenever `signal` is delivered to this process. +--- Suppresses the default OS behavior (e.g. `"SIGINT"` will no longer terminate the process). +--- Returns a handle; call `handle:close()` to deregister. +function processlib.onSignal(signal: Signal, callback: () -> ()): SignalHandle + return process.onSignal(signal, callback) +end + +--- Returns the PID of the current process. +function processlib.pid(): number + return process.pid() +end + +-- re-exports +processlib.args = process.args +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..e1ed6863e --- /dev/null +++ b/lute/std/libs/stringext.luau @@ -0,0 +1,64 @@ +--!strict +-- @std/stringext +-- stdlib for an extension of the built-in string library in Luau +-- Provides additional utility functions for string operations + +local stringext = {} + +--- Returns `true` if `str` begins with `prefix`. +function stringext.hasPrefix(str: string, prefix: string): boolean + if #prefix > #str then + return false + end + + return str:sub(1, #prefix) == prefix +end + +--- Returns `str` with `prefix` removed from the front. If `str` does not start with `prefix`, returns `str` unchanged. +function stringext.removePrefix(str: string, prefix: string): string + if not stringext.hasPrefix(str, prefix) then + return str + end + + return str:sub(#prefix + 1) +end + +--- Returns `true` if `str` ends with `suffix`. +function stringext.hasSuffix(str: string, suffix: string): boolean + return str:sub(-#suffix) == suffix +end + +--- Returns `str` with `suffix` removed from the end. If `str` does not end with `suffix`, returns `str` unchanged. +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 + +--- Returns `str` with leading and trailing whitespace removed. +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`, optionally restricted to the range `[startPos, endPos]`. +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..b39dd7abb --- /dev/null +++ b/lute/std/libs/syntax/init.luau @@ -0,0 +1,187 @@ +local parser = require("@self/parser") + +local cst = require("@lute/syntax/cst") + +export type Span = cst.Span + +local span = { + --- Creates a `Span` from begin/end line and column values. + create = cst.span.create, +} + +--- Returns `true` if `haystack` fully contains `needle`. +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 = cst.Whitespace +export type SingleLineComment = cst.SingleLineComment +export type MultiLineComment = cst.MultiLineComment + +export type Trivia = cst.Trivia + +export type CstToken = cst.CstToken + +export type CstEof = cst.CstEof + +export type CstPunctuated = cst.CstPunctuated + +export type CstLocal = cst.CstLocal + +export type CstExprGroup = cst.CstExprGroup + +export type CstExprConstantNil = cst.CstExprConstantNil + +export type CstExprConstantBool = cst.CstExprConstantBool + +export type CstExprConstantNumber = cst.CstExprConstantNumber + +export type CstExprConstantInteger = cst.CstExprConstantInteger + +export type CstExprConstantString = cst.CstExprConstantString + +export type CstExprLocal = cst.CstExprLocal + +export type CstExprGlobal = cst.CstExprGlobal + +export type CstExprVarargs = cst.CstExprVarargs + +export type CstExprCall = cst.CstExprCall + +export type CstExprInstantiate = cst.CstExprInstantiate + +export type CstExprIndexName = cst.CstExprIndexName + +export type CstExprIndexExpr = cst.CstExprIndexExpr + +export type CstExprFunction = cst.CstExprFunction + +export type CstTableExprListItem = cst.CstTableExprListItem + +export type CstTableExprRecordItem = cst.CstTableExprRecordItem + +export type CstTableExprGeneralItem = cst.CstTableExprGeneralItem + +export type CstTableExprItem = cst.CstTableExprItem + +export type CstExprTable = cst.CstExprTable + +export type CstExprUnary = cst.CstExprUnary + +export type CstExprBinary = cst.CstExprBinary + +export type CstExprInterpString = cst.CstExprInterpString + +export type CstExprTypeAssertion = cst.CstExprTypeAssertion + +export type CstElseIfExpr = cst.CstElseIfExpr + +export type CstExprIfElse = cst.CstExprIfElse + +export type CstExpr = cst.CstExpr + +export type CstStatBlock = cst.CstStatBlock + +export type CstStatDo = cst.CstStatDo + +export type CstElseIfStat = cst.CstElseIfStat + +export type CstStatIf = cst.CstStatIf + +export type CstStatWhile = cst.CstStatWhile + +export type CstStatRepeat = cst.CstStatRepeat + +export type CstStatBreak = cst.CstStatBreak + +export type CstStatContinue = cst.CstStatContinue + +export type CstStatReturn = cst.CstStatReturn + +export type CstStatExpr = cst.CstStatExpr + +export type CstStatLocal = cst.CstStatLocal + +export type CstStatFor = cst.CstStatFor + +export type CstStatForIn = cst.CstStatForIn + +export type CstStatAssign = cst.CstStatAssign + +export type CstStatCompoundAssign = cst.CstStatCompoundAssign + +export type CstAttribute = cst.CstAttribute + +export type CstStatFunction = cst.CstStatFunction + +export type CstStatLocalFunction = cst.CstStatLocalFunction + +export type CstStatTypeAlias = cst.CstStatTypeAlias + +export type CstStatTypeFunction = cst.CstStatTypeFunction + +export type CstStat = cst.CstStat + +export type CstGenericType = cst.CstGenericType + +export type CstGenericTypePack = cst.CstGenericTypePack + +export type CstTypeReference = cst.CstTypeReference + +export type CstTypeSingletonBool = cst.CstTypeSingletonBool + +export type CstTypeSingletonString = cst.CstTypeSingletonString + +export type CstTypeTypeof = cst.CstTypeTypeof + +export type CstTypeGroup = cst.CstTypeGroup + +export type CstTypeOptional = cst.CstTypeOptional + +export type CstTypeUnion = cst.CstTypeUnion + +export type CstTypeIntersection = cst.CstTypeIntersection + +export type CstTypeArray = cst.CstTypeArray + +export type CstTableTypeItemIndexer = cst.CstTableTypeItemIndexer + +export type CstTableTypeItemProperty = cst.CstTableTypeItemProperty + +export type CstTableTypeItem = cst.CstTableTypeItem + +export type CstTypeTable = cst.CstTypeTable + +export type CstFunctionTypeParameter = cst.CstFunctionTypeParameter + +export type CstTypeFunction = cst.CstTypeFunction + +export type CstType = cst.CstType + +export type CstTypePackExplicit = cst.CstTypePackExplicit + +export type CstTypePackGeneric = cst.CstTypePackGeneric + +export type CstTypePackVariadic = cst.CstTypePackVariadic + +export type CstTypePack = cst.CstTypePack + +export type CstNode = cst.CstNode + +export type CstParseResult = cst.CstParseResult + +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..1cad5f363 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -1,28 +1,25 @@ --!strict +-- @std/syntax/parser +-- Parser for Luau source code into Luau CST nodes -local luau = require("@lute/luau") +local luteParser = require("@lute/syntax/parser") +local types = require("./types") ---- Parses Luau source code into an AstStatBlock -local function parse(source: string): luau.AstStatBlock - return luau.parse(source).root -end +local parser = {} -local function parseexpr(source: string): luau.AstExpr - return luau.parseexpr(source) +--- Parses Luau source code into a CstStatBlock +function parser.parseBlock(source: string): types.CstStatBlock + return luteParser.parse(source).root end -export type ParseResult = { - root: luau.AstStatBlock, - eof: luau.Eof, -} +--- Parses `source` as a single Luau expression and returns the resulting `CstExpr`. +function parser.parseExpr(source: string): types.CstExpr + return luteParser.parseExpr(source) +end -local function parsefile(source: string): ParseResult - local result = luau.parse(source) - return { root = result.root, eof = result.eof } +--- Parses `source` as a complete Luau file and returns the full `CstParseResult`, including the root block and EOF token. +function parser.parse(source: string): types.CstParseResult + return luteParser.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..d0fc329cf 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -1,151 +1,251 @@ --!strict -local luau = require("@lute/luau") +--!native +-- @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.CstToken) -> (), + printString: (self: PrintVisitor, expr: types.CstExprConstantString | types.CstTypeSingletonString) -> (), + printInterpolatedString: (self: PrintVisitor, expr: types.CstExprInterpString) -> (), + printReplacement: (self: PrintVisitor, node: types.CstNode, replacement: types.Replacement) -> (), + printTableTypeItem: (self: PrintVisitor, node: types.CstTableTypeItem) -> (), +} 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.CstToken) + 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) +local function printString(self: PrintVisitor, expr: types.CstExprConstantString | types.CstTypeSingletonString) + if self.replacements[expr] ~= nil then + self:printReplacement(expr, self.replacements[expr]) + return + end + + self:printTriviaList(expr.value.leadingTrivia) if expr.quoteStyle == "single" then - result ..= `'{expr.text}'` + self:write(`'{expr.value.text}'`) elseif expr.quoteStyle == "double" then - result ..= `"{expr.text}"` + self:write(`"{expr.value.text}"`) elseif expr.quoteStyle == "block" then local equals = string.rep("=", expr.blockDepth) - result ..= `[{equals}[{expr.text}]{equals}]` + self:write(`[{equals}[{expr.value.text}]{equals}]`) elseif expr.quoteStyle == "interp" then - result ..= "`" .. expr.text .. "`" + self:write("`" .. expr.value.text .. "`") else - return exhaustiveMatch(expr.quoteStyle) + exhaustiveMatch(expr.quoteStyle) end - result ..= printTriviaList(expr.trailingTrivia) - return result + self:printTriviaList(expr.value.trailingTrivia) end -local function printInterpolatedString(expr: luau.AstExprInterpString): string - local result = "" +local function printInterpolatedString(self: PrintVisitor, expr: types.CstExprInterpString) + 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 - - return result end -type PrintVisitor = visitor.Visitor & { - result: buffer, - cursor: number, -} +local function printTableTypeItem(self: PrintVisitor, node: types.CstTableTypeItem) + if node.access then + printToken(self, node.access) + end + if node.kind == "indexer" then + printToken(self, node.indexerOpen) + visitor.visitType(node.key, self) + printToken(self, node.indexerClose) + elseif node.kind == "property" then + if node.indexerOpen then + printToken(self, node.indexerOpen) + end -local function printVisitor() - local printer = visitor.createVisitor() :: PrintVisitor + if node.quoteStyle then + self:write(if node.quoteStyle == "single" then "'" else '"') + end - printer.result = buffer.create(1024) - printer.cursor = 0 + printToken(self, node.key) - local function write(str: string) - local totalSize = printer.cursor + #str - local bufferSize = buffer.len(printer.result) + if node.quoteStyle then + self:write(if node.quoteStyle == "single" then "'" else '"') + end + + if node.indexerClose then + printToken(self, node.indexerClose) + end + else + exhaustiveMatch(node.kind) + end + printToken(self, node.colon) + visitor.visitType(node.value, self) + if node.separator then + printToken(self, node.separator) + end +end - if totalSize >= bufferSize then - repeat - bufferSize *= 2 - until bufferSize >= totalSize +local function printReplacement(self: PrintVisitor, node: types.CstNode, replacement: types.Replacement) + assert(typeof(replacement) == "string" or typeof(replacement) == "table", "Unsupported replacement type") - local newBuffer = buffer.create(bufferSize) - buffer.copy(newBuffer, 0, printer.result) - printer.result = newBuffer + local replacementContent = if typeof(replacement) == "string" then replacement else replacement.content + local preserveTrivia = if typeof(replacement) == "table" then replacement.preserveTrivia ~= false else true + + if preserveTrivia then + local leftmostTrivia = triviaUtils.leftmostTrivia(node) + if leftmostTrivia ~= nil then + self:printTriviaList(leftmostTrivia) end + end - buffer.writestring(printer.result, printer.cursor, str) - printer.cursor = totalSize + self:write(replacementContent) + + if preserveTrivia then + local rightmostTrivia = triviaUtils.rightmostTrivia(node) + if rightmostTrivia ~= nil then + self:printTriviaList(rightmostTrivia) + end end +end - printer.visitToken = function(node: luau.Token) - write(printToken(node)) - return false +local function write(self: PrintVisitor, str: string) + local totalSize = self.cursor + #str + local bufferSize = buffer.len(self.result) + + if totalSize >= bufferSize then + repeat + bufferSize *= 2 + until bufferSize >= totalSize + + local newBuffer = buffer.create(bufferSize) + buffer.copy(newBuffer, 0, self.result) + self.result = newBuffer end - printer.visitString = function(node: luau.AstExprConstantString) - write(printString(node)) + buffer.writestring(self.result, self.cursor, str) + self.cursor = totalSize +end + +local function printVisitor(replacements: types.Replacements?) + local result = buffer.create(1024) + local cursor = 0 + + local printer: PrintVisitor = {} :: PrintVisitor + + -- The visitor library doesn't use methods, so we access replacements through a closure + local function maybeReplace(node: types.CstNode): boolean + if replacements == nil then + return true + end + local replacement: types.Replacement? = replacements[node] + if replacement == nil then + return true + end + printer:printReplacement(node, replacement) return false end - printer.visitTypeString = function(node: luau.AstTypeSingletonString) - write(printString(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.printTableTypeItem = printTableTypeItem + printer.visitExprInterpString = function(node: types.CstExprInterpString) + printer:printInterpolatedString(node) return false end - - printer.visitInterpolatedString = function(node: luau.AstExprInterpString) - write(printInterpolatedString(node)) + printer.visitTypeSingletonString = function(node: types.CstTypeSingletonString) + printer:printString(node) + return false + end + printer.visitToken = function(node: types.CstToken) + printer:printToken(node) + return false + end + printer.visitExprConstantString = function(node: types.CstExprConstantString) + printer:printString(node) + return false + end + printer.visitTableTypeItem = function(node: types.CstTableTypeItem) + printer:printTableTypeItem(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) - 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) +--- Returns `node` as source text, optionally applying `replacements` to substitute specific nodes with new text. +function printerLib.printNode(node: types.CstNode, replacements: types.Replacements?): string + local printer = printVisitor(replacements) + visitor.visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) end -function printFile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string - local printer = printVisitor() +--- Returns an entire parsed file as source text, including the EOF token, optionally applying `replacements`. +function printerLib.printFile(result: types.CstParseResult, 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 -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..55734d4cd --- /dev/null +++ b/lute/std/libs/syntax/query.luau @@ -0,0 +1,151 @@ +--!strict +-- @std/syntax/query +-- Provides utility functions for querying Luau CST nodes + +local tableext = require("@std/tableext") +local types = require("./types") +local visitor = require("./visitor") + +type Node = types.CstNode + +--- A collection of CST nodes that supports chained filtering, mapping, and replacement operations. +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 = {} + +--- Retains only the nodes for which `pred` returns `true`. Mutates and returns the query. +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 + +--- Calls `repl` on each node and collects the non-nil results into a `Replacements` table for use with the printer. +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 + +--- Replaces the query's nodes with the non-nil values returned by `fn` for each node. Mutates and returns the 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 + +--- Calls `callback` on each node. Returns the query unchanged for chaining. +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) + local selected = fn(n) + if selected ~= nil then + table.insert(nodes, selected) + end + return true + end + + local selectVisitor = visitor.create(visit) + selectVisitor.visitStatBlockEnd = function(_) end + selectVisitor.visitStatLocalDeclarationEnd = function(_) end + selectVisitor.visitExpr = function(_) + return true + end + selectVisitor.visitExprEnd = function(_) end + + return selectVisitor +end + +--- Recursively visits every descendant of each node in the query and collects those for which `fn` returns a non-nil value. Mutates and returns the query. +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 + +--- Replaces each node with the array of values returned by `fn`, flattening one level. Mutates and returns the query. +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 + +--- Returns a plain array of the non-nil values produced by calling `fn` on each node. Does not mutate the query. +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 + +--- Creates a new `Query` by visiting every node in `cst` and collecting those for which `fn` returns a non-nil value. +function queryLib.findAllFromRoot(cst: types.CstParseResult | Node, fn: (Node) -> T?): Query + local nodes: { T } = {} + + local selectVisitor = newSelectVisitor(nodes, fn) + + local root: Node = if (cst :: any).root ~= nil then (cst :: any).root else cst :: any -- LUAUFIX: Replace any cast with cast to node once code too complex no longer emitted + visitor.visit(root, selectVisitor) + + return { + nodes = nodes, + filter = queryLib.filter, + replace = queryLib.replace, + map = queryLib.map :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened + forEach = queryLib.forEach, + findAll = queryLib.findAll :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened + flatMap = queryLib.flatMap, + mapToArray = queryLib.mapToArray :: any, -- Any cast because mapToArray has generics quantified at a different level than the query type expects + } +end + +return table.freeze(queryLib) diff --git a/lute/std/libs/syntax/types/cst.luau b/lute/std/libs/syntax/types/cst.luau new file mode 100644 index 000000000..3c82f43a8 --- /dev/null +++ b/lute/std/libs/syntax/types/cst.luau @@ -0,0 +1,163 @@ +local cst = require("@lute/syntax/cst") + +export type Span = cst.Span + +export type Whitespace = cst.Whitespace +export type SingleLineComment = cst.SingleLineComment +export type MultiLineComment = cst.MultiLineComment + +export type Trivia = cst.Trivia + +export type CstToken = cst.CstToken + +export type CstEof = cst.CstEof + +export type CstPunctuated = cst.CstPunctuated + +export type CstLocal = cst.CstLocal + +export type CstExprGroup = cst.CstExprGroup + +export type CstExprConstantNil = cst.CstExprConstantNil + +export type CstExprConstantBool = cst.CstExprConstantBool + +export type CstExprConstantNumber = cst.CstExprConstantNumber + +export type CstExprConstantInteger = cst.CstExprConstantInteger + +export type CstExprConstantString = cst.CstExprConstantString + +export type CstExprLocal = cst.CstExprLocal + +export type CstExprGlobal = cst.CstExprGlobal + +export type CstExprVarargs = cst.CstExprVarargs + +export type CstExprCall = cst.CstExprCall + +export type CstExprInstantiate = cst.CstExprInstantiate + +export type CstExprIndexName = cst.CstExprIndexName + +export type CstExprIndexExpr = cst.CstExprIndexExpr + +export type CstExprFunction = cst.CstExprFunction + +export type CstTableExprListItem = cst.CstTableExprListItem + +export type CstTableExprRecordItem = cst.CstTableExprRecordItem + +export type CstTableExprGeneralItem = cst.CstTableExprGeneralItem + +export type CstTableExprItem = cst.CstTableExprItem + +export type CstExprTable = cst.CstExprTable + +export type CstExprUnary = cst.CstExprUnary + +export type CstExprBinary = cst.CstExprBinary + +export type CstExprInterpString = cst.CstExprInterpString + +export type CstExprTypeAssertion = cst.CstExprTypeAssertion + +export type CstElseIfExpr = cst.CstElseIfExpr + +export type CstExprIfElse = cst.CstExprIfElse + +export type CstExpr = cst.CstExpr + +export type CstStatBlock = cst.CstStatBlock + +export type CstStatDo = cst.CstStatDo + +export type CstElseIfStat = cst.CstElseIfStat + +export type CstStatIf = cst.CstStatIf + +export type CstStatWhile = cst.CstStatWhile + +export type CstStatRepeat = cst.CstStatRepeat + +export type CstStatBreak = cst.CstStatBreak + +export type CstStatContinue = cst.CstStatContinue + +export type CstStatReturn = cst.CstStatReturn + +export type CstStatExpr = cst.CstStatExpr + +export type CstStatLocal = cst.CstStatLocal + +export type CstStatConst = cst.CstStatConst + +export type CstStatFor = cst.CstStatFor + +export type CstStatForIn = cst.CstStatForIn + +export type CstStatAssign = cst.CstStatAssign + +export type CstStatCompoundAssign = cst.CstStatCompoundAssign + +export type CstAttribute = cst.CstAttribute + +export type CstStatFunction = cst.CstStatFunction + +export type CstStatLocalFunction = cst.CstStatLocalFunction + +export type CstStatTypeAlias = cst.CstStatTypeAlias + +export type CstStatTypeFunction = cst.CstStatTypeFunction + +export type CstStat = cst.CstStat + +export type CstGenericType = cst.CstGenericType + +export type CstGenericTypePack = cst.CstGenericTypePack + +export type CstTypeReference = cst.CstTypeReference + +export type CstTypeSingletonBool = cst.CstTypeSingletonBool + +export type CstTypeSingletonString = cst.CstTypeSingletonString + +export type CstTypeTypeof = cst.CstTypeTypeof + +export type CstTypeGroup = cst.CstTypeGroup + +export type CstTypeOptional = cst.CstTypeOptional + +export type CstTypeUnion = cst.CstTypeUnion + +export type CstTypeIntersection = cst.CstTypeIntersection + +export type CstTypeArray = cst.CstTypeArray + +export type CstTableTypeItemIndexer = cst.CstTableTypeItemIndexer + +export type CstTableTypeItemProperty = cst.CstTableTypeItemProperty + +export type CstTableTypeItem = cst.CstTableTypeItem + +export type CstTypeTable = cst.CstTypeTable + +export type CstFunctionTypeParameter = cst.CstFunctionTypeParameter + +export type CstTypeFunction = cst.CstTypeFunction + +export type CstType = cst.CstType + +export type CstTypePackExplicit = cst.CstTypePackExplicit + +export type CstTypePackGeneric = cst.CstTypePackGeneric + +export type CstTypePackVariadic = cst.CstTypePackVariadic + +export type CstTypePack = cst.CstTypePack + +export type CstNode = cst.CstNode + +export type CstParseResult = cst.CstParseResult + +return {} diff --git a/lute/std/libs/syntax/types/init.luau b/lute/std/libs/syntax/types/init.luau new file mode 100644 index 000000000..aa4ca0d65 --- /dev/null +++ b/lute/std/libs/syntax/types/init.luau @@ -0,0 +1,181 @@ +local cst = require("@lute/syntax/cst") +local luau = require("@lute/luau") + +export type Span = cst.Span + +export type Whitespace = cst.Whitespace +export type SingleLineComment = cst.SingleLineComment +export type MultiLineComment = cst.MultiLineComment + +export type Trivia = cst.Trivia + +export type CstToken = cst.CstToken + +export type CstEof = cst.CstEof + +export type CstPunctuated = cst.CstPunctuated + +export type CstLocal = cst.CstLocal + +export type CstExprGroup = cst.CstExprGroup + +export type CstExprConstantNil = cst.CstExprConstantNil + +export type CstExprConstantBool = cst.CstExprConstantBool + +export type CstExprConstantNumber = cst.CstExprConstantNumber + +export type CstExprConstantInteger = cst.CstExprConstantInteger + +export type CstExprConstantString = cst.CstExprConstantString + +export type CstExprLocal = cst.CstExprLocal + +export type CstExprGlobal = cst.CstExprGlobal + +export type CstExprVarargs = cst.CstExprVarargs + +export type CstExprCall = cst.CstExprCall + +export type CstExprInstantiate = cst.CstExprInstantiate + +export type CstExprIndexName = cst.CstExprIndexName + +export type CstExprIndexExpr = cst.CstExprIndexExpr + +export type CstExprFunction = cst.CstExprFunction + +export type CstTableExprListItem = cst.CstTableExprListItem + +export type CstTableExprRecordItem = cst.CstTableExprRecordItem + +export type CstTableExprGeneralItem = cst.CstTableExprGeneralItem + +export type CstTableExprItem = cst.CstTableExprItem + +export type CstExprTable = cst.CstExprTable + +export type CstExprUnary = cst.CstExprUnary + +export type CstExprBinary = cst.CstExprBinary + +export type CstExprInterpString = cst.CstExprInterpString + +export type CstExprTypeAssertion = cst.CstExprTypeAssertion + +export type CstElseIfExpr = cst.CstElseIfExpr + +export type CstExprIfElse = cst.CstExprIfElse + +export type CstExpr = cst.CstExpr + +export type CstStatBlock = cst.CstStatBlock + +export type CstStatDo = cst.CstStatDo + +export type CstElseIfStat = cst.CstElseIfStat + +export type CstStatIf = cst.CstStatIf + +export type CstStatWhile = cst.CstStatWhile + +export type CstStatRepeat = cst.CstStatRepeat + +export type CstStatBreak = cst.CstStatBreak + +export type CstStatContinue = cst.CstStatContinue + +export type CstStatReturn = cst.CstStatReturn + +export type CstStatExpr = cst.CstStatExpr + +export type CstStatLocal = cst.CstStatLocal + +export type CstStatConst = cst.CstStatConst + +export type CstStatFor = cst.CstStatFor + +export type CstStatForIn = cst.CstStatForIn + +export type CstStatAssign = cst.CstStatAssign + +export type CstStatCompoundAssign = cst.CstStatCompoundAssign + +export type CstAttribute = cst.CstAttribute + +export type CstStatFunction = cst.CstStatFunction + +export type CstStatLocalFunction = cst.CstStatLocalFunction + +export type CstStatTypeAlias = cst.CstStatTypeAlias + +export type CstStatTypeFunction = cst.CstStatTypeFunction + +export type CstStat = cst.CstStat + +export type CstGenericType = cst.CstGenericType + +export type CstGenericTypePack = cst.CstGenericTypePack + +export type CstTypeReference = cst.CstTypeReference + +export type CstTypeSingletonBool = cst.CstTypeSingletonBool + +export type CstTypeSingletonString = cst.CstTypeSingletonString + +export type CstTypeTypeof = cst.CstTypeTypeof + +export type CstTypeGroup = cst.CstTypeGroup + +export type CstTypeOptional = cst.CstTypeOptional + +export type CstTypeUnion = cst.CstTypeUnion + +export type CstTypeIntersection = cst.CstTypeIntersection + +export type CstTypeArray = cst.CstTypeArray + +export type CstTableTypeItemIndexer = cst.CstTableTypeItemIndexer + +export type CstTableTypeItemProperty = cst.CstTableTypeItemProperty + +export type CstTableTypeItem = cst.CstTableTypeItem + +export type CstTypeTable = cst.CstTypeTable + +export type CstFunctionTypeParameter = cst.CstFunctionTypeParameter + +export type CstTypeFunction = cst.CstTypeFunction + +export type CstType = cst.CstType + +export type CstTypePackExplicit = cst.CstTypePackExplicit + +export type CstTypePackGeneric = cst.CstTypePackGeneric + +export type CstTypePackVariadic = cst.CstTypePackVariadic + +export type CstTypePack = cst.CstTypePack + +export type CstNode = cst.CstNode + +export type CstParseResult = cst.CstParseResult + +--- The type of a replacement for a CST node. +--- A replacement can be: +--- 1. A string, which will be used as the replacement text for the node, with any surrounding trivia preserved. +--- 2. An object with a `content` property containing the replacement string, and an optional `preserveTrivia` boolean property. +--- If `preserveTrivia` is true, the replaced CST node's attached trivia (whitespace and comments) will be reserialized with the replacement content. +--- If false, the trivia will be discarded. If omitted, trivia is preserved by default. +--- Note that trailing trivia for a token consists of all whitespace and comments that follow it up to and including a newline. +--- Any trivia after the first seen newline is considered leading trivia for the next token. +export type Replacement = string | { content: string, preserveTrivia: boolean? } + +--- A mapping of CST nodes to their replacements, used with `@std/syntax/printer` and `lute transform` to specify how CSTs should be mutated. +export type Replacements = { [CstNode]: Replacement } + +export type Type = luau.Type + +export type TypePack = luau.TypePack + +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..cce7bf65d --- /dev/null +++ b/lute/std/libs/syntax/utils/init.luau @@ -0,0 +1,322 @@ +local types = require("./types") + +local utils = {} + +--- Returns `n` narrowed to `CstLocal` if it is a local variable node, or `nil` otherwise. +function utils.isLocal(n: types.CstNode): types.CstLocal? + return if n.kind == "local" then n else nil +end + +--- Returns `n` narrowed to `CstExprGroup` if it is a parenthesized expression, or `nil` otherwise. +function utils.isExprGroup(n: types.CstNode): types.CstExprGroup? + return if n.kind == "expr" and n.tag == "group" then n else nil +end + +--- Returns `n` narrowed to `CstExprConstantNil` if it is a nil literal expression, or `nil` otherwise. +function utils.isExprConstantNil(n: types.CstNode): types.CstExprConstantNil? + return if n.kind == "expr" and n.tag == "nil" then n else nil +end + +--- Returns `n` narrowed to `CstExprConstantBool` if it is a boolean literal expression, or `nil` otherwise. +function utils.isExprConstantBool(n: types.CstNode): types.CstExprConstantBool? + return if n.kind == "expr" and n.tag == "boolean" then n else nil +end + +--- Returns `n` narrowed to `CstExprConstantNumber` if it is a number literal expression, or `nil` otherwise. +function utils.isExprConstantNumber(n: types.CstNode): types.CstExprConstantNumber? + return if n.kind == "expr" and n.tag == "number" then n else nil +end + +--- Returns `n` narrowed to `CstExprConstantString` if it is a string literal expression, or `nil` otherwise. +function utils.isExprConstantString(n: types.CstNode): types.CstExprConstantString? + return if n.kind == "expr" and n.tag == "string" then n else nil +end + +--- Returns `n` narrowed to `CstExprLocal` if it is a local variable reference expression, or `nil` otherwise. +function utils.isExprLocal(n: types.CstNode): types.CstExprLocal? + return if n.kind == "expr" and n.tag == "local" then n else nil +end + +--- Returns `n` narrowed to `CstExprGlobal` if it is a global variable reference expression, or `nil` otherwise. +function utils.isExprGlobal(n: types.CstNode): types.CstExprGlobal? + return if n.kind == "expr" and n.tag == "global" then n else nil +end + +--- Returns `n` narrowed to `CstExprVarargs` if it is a varargs expression (`...`), or `nil` otherwise. +function utils.isExprVarargs(n: types.CstNode): types.CstExprVarargs? + return if n.kind == "expr" and n.tag == "vararg" then n else nil +end + +--- Returns `n` narrowed to `CstExprCall` if it is a call expression, or `nil` otherwise. +function utils.isExprCall(n: types.CstNode): types.CstExprCall? + return if n.kind == "expr" and n.tag == "call" then n else nil +end + +--- Returns `n` as an `CstExprCall` if it is a `require(...)` call expression, or `nil` otherwise. +function utils.isRequireCall(n: types.CstNode): types.CstExprCall? + local call = utils.isExprCall(n) + local global = call and utils.isExprGlobal(call.func) + return if global and global.name.text == "require" then call else nil +end + +--- Returns `n` narrowed to `CstExprIndexName` if it is a field access expression (e.g. `a.b`), or `nil` otherwise. +function utils.isExprIndexName(n: types.CstNode): types.CstExprIndexName? + return if n.kind == "expr" and n.tag == "indexname" then n else nil +end + +--- Returns `n` narrowed to `CstExprIndexExpr` if it is an index expression (e.g. `a[b]`), or `nil` otherwise. +function utils.isExprIndexExpr(n: types.CstNode): types.CstExprIndexExpr? + return if n.kind == "expr" and n.tag == "index" then n else nil +end + +--- Returns `n` narrowed to `CstExprFunction` if it is a function expression, or `nil` otherwise. +function utils.isExprFunction(n: types.CstNode): types.CstExprFunction? + return if n.kind == "expr" and n.tag == "function" then n else nil +end + +--- Returns `n` narrowed to `CstExprTable` if it is a table constructor expression, or `nil` otherwise. +function utils.isExprTable(n: types.CstNode): types.CstExprTable? + return if n.kind == "expr" and n.tag == "table" then n else nil +end + +--- Returns `n` narrowed to `CstTableExprItem` if it is a table constructor item, or `nil` otherwise. +function utils.isTableExprItem(n: types.CstNode | types.CstTableExprItem): types.CstTableExprItem? + return if (n :: types.CstTableExprItem).isTableItem then n :: types.CstTableExprItem else nil +end + +--- Returns `n` narrowed to `CstExprUnary` if it is a unary expression, or `nil` otherwise. +function utils.isExprUnary(n: types.CstNode): types.CstExprUnary? + return if n.kind == "expr" and n.tag == "unary" then n else nil +end + +--- Returns `n` narrowed to `CstExprBinary` if it is a binary expression, or `nil` otherwise. +function utils.isExprBinary(n: types.CstNode): types.CstExprBinary? + return if n.kind == "expr" and n.tag == "binary" then n else nil +end + +--- Returns `n` narrowed to `CstExprInterpString` if it is an interpolated string expression, or `nil` otherwise. +function utils.isExprInterpString(n: types.CstNode): types.CstExprInterpString? + return if n.kind == "expr" and n.tag == "interpolatedstring" then n else nil +end + +--- Returns `n` narrowed to `CstExprTypeAssertion` if it is a type assertion expression (`expr :: Type`), or `nil` otherwise. +function utils.isExprTypeAssertion(n: types.CstNode): types.CstExprTypeAssertion? + return if n.kind == "expr" and n.tag == "cast" then n else nil +end + +--- Returns `n` narrowed to `CstExprIfElse` if it is an `if-then-else` expression, or `nil` otherwise. +function utils.isExprIfElse(n: types.CstNode): types.CstExprIfElse? + return if n.kind == "expr" and n.tag == "conditional" then n else nil +end + +--- Returns `n` narrowed to `CstExpr` if it is any expression node, or `nil` otherwise. +function utils.isExpr(n: types.CstNode): types.CstExpr? + return if n.kind == "expr" then n else nil +end + +--- Returns `n` narrowed to `CstStatBlock` if it is a block statement (do...end), or `nil` otherwise. +function utils.isStatBlock(n: types.CstNode): types.CstStatBlock? + return if n.kind == "stat" and n.tag == "block" then n else nil +end + +--- Returns `n` narrowed to `CstStatIf` if it is an if statement, or `nil` otherwise. +function utils.isStatIf(n: types.CstNode): types.CstStatIf? + return if n.kind == "stat" and n.tag == "conditional" then n else nil +end + +--- Returns `n` narrowed to `CstStatWhile` if it is a while statement, or `nil` otherwise. +function utils.isStatWhile(n: types.CstNode): types.CstStatWhile? + return if n.kind == "stat" and n.tag == "while" then n else nil +end + +--- Returns `n` narrowed to `CstStatRepeat` if it is a repeat-until statement, or `nil` otherwise. +function utils.isStatRepeat(n: types.CstNode): types.CstStatRepeat? + return if n.kind == "stat" and n.tag == "repeat" then n else nil +end + +--- Returns `n` narrowed to `CstStatBreak` if it is a break statement, or `nil` otherwise. +function utils.isStatBreak(n: types.CstNode): types.CstStatBreak? + return if n.kind == "stat" and n.tag == "break" then n else nil +end + +--- Returns `n` narrowed to `CstStatContinue` if it is a continue statement, or `nil` otherwise. +function utils.isStatContinue(n: types.CstNode): types.CstStatContinue? + return if n.kind == "stat" and n.tag == "continue" then n else nil +end + +--- Returns `n` narrowed to `CstStatReturn` if it is a return statement, or `nil` otherwise. +function utils.isStatReturn(n: types.CstNode): types.CstStatReturn? + return if n.kind == "stat" and n.tag == "return" then n else nil +end + +--- Returns `n` narrowed to `CstStatExpr` if it is an expression statement, or `nil` otherwise. +function utils.isStatExpr(n: types.CstNode): types.CstStatExpr? + return if n.kind == "stat" and n.tag == "expression" then n else nil +end + +--- Returns `n` narrowed to `CstStatLocal` if it is a local variable declaration, or `nil` otherwise. +function utils.isStatLocal(n: types.CstNode): types.CstStatLocal? + return if n.kind == "stat" and n.tag == "local" then n else nil +end + +--- Returns `n` narrowed to `CstStatConst` if it is a const variable declaration, or `nil` otherwise. +function utils.isStatConst(n: types.CstNode): types.CstStatConst? + return if n.kind == "stat" and n.tag == "const" then n else nil +end + +--- Returns `n` narrowed to `CstStatFor` if it is a numeric for statement, or `nil` otherwise. +function utils.isStatFor(n: types.CstNode): types.CstStatFor? + return if n.kind == "stat" and n.tag == "for" then n else nil +end + +--- Returns `n` narrowed to `CstStatForIn` if it is a generic for-in statement, or `nil` otherwise. +function utils.isStatForIn(n: types.CstNode): types.CstStatForIn? + return if n.kind == "stat" and n.tag == "forin" then n else nil +end + +--- Returns `n` narrowed to `CstStatAssign` if it is an assignment statement, or `nil` otherwise. +function utils.isStatAssign(n: types.CstNode): types.CstStatAssign? + return if n.kind == "stat" and n.tag == "assign" then n else nil +end + +--- Returns `n` narrowed to `CstStatCompoundAssign` if it is a compound assignment statement (e.g. `+=`), or `nil` otherwise. +function utils.isStatCompoundAssign(n: types.CstNode): types.CstStatCompoundAssign? + return if n.kind == "stat" and n.tag == "compoundassign" then n else nil +end + +--- Returns `n` narrowed to `CstStatFunction` if it is a function declaration statement, or `nil` otherwise. +function utils.isStatFunction(n: types.CstNode): types.CstStatFunction? + return if n.kind == "stat" and n.tag == "function" then n else nil +end + +--- Returns `n` narrowed to `CstStatLocalFunction` if it is a local function declaration, or `nil` otherwise. +function utils.isStatLocalFunction(n: types.CstNode): types.CstStatLocalFunction? + return if n.kind == "stat" and n.tag == "localfunction" then n else nil +end + +--- Returns `n` narrowed to `CstStatTypeAlias` if it is a type alias declaration, or `nil` otherwise. +function utils.isStatTypeAlias(n: types.CstNode): types.CstStatTypeAlias? + return if n.kind == "stat" and n.tag == "typealias" then n else nil +end + +--- Returns `n` narrowed to `CstStatTypeFunction` if it is a type function declaration, or `nil` otherwise. +function utils.isStatTypeFunction(n: types.CstNode): types.CstStatTypeFunction? + return if n.kind == "stat" and n.tag == "typefunction" then n else nil +end + +--- Returns `n` narrowed to `CstStat` if it is any statement node, or `nil` otherwise. +function utils.isStat(n: types.CstNode): types.CstStat? + return if n.kind == "stat" then n else nil +end + +--- Returns `n` narrowed to `CstGenericType` if it is a generic type parameter, or `nil` otherwise. +function utils.isGenericType(n: types.CstNode): types.CstGenericType? + return if n.kind == "type" and n.tag == "generic" then n else nil +end + +--- Returns `n` narrowed to `CstGenericTypePack` if it is a generic type pack parameter, or `nil` otherwise. +function utils.isGenericTypePack(n: types.CstNode): types.CstGenericTypePack? + return if n.kind == "typepack" and n.tag == "genericpack" then n else nil +end + +--- Returns `n` narrowed to `CstTypeReference` if it is a type reference, or `nil` otherwise. +function utils.isTypeReference(n: types.CstNode): types.CstTypeReference? + return if n.kind == "type" and n.tag == "reference" then n else nil +end + +--- Returns `n` narrowed to `CstTypeSingletonBool` if it is a boolean singleton type, or `nil` otherwise. +function utils.isTypeSingletonBool(n: types.CstNode): types.CstTypeSingletonBool? + return if n.kind == "type" and n.tag == "boolean" then n else nil +end + +--- Returns `n` narrowed to `CstTypeSingletonString` if it is a string singleton type, or `nil` otherwise. +function utils.isTypeSingletonString(n: types.CstNode): types.CstTypeSingletonString? + return if n.kind == "type" and n.tag == "string" then n else nil +end + +--- Returns `n` narrowed to `CstTypeTypeof` if it is a `typeof(...)` type, or `nil` otherwise. +function utils.isTypeTypeof(n: types.CstNode): types.CstTypeTypeof? + return if n.kind == "type" and n.tag == "typeof" then n else nil +end + +--- Returns `n` narrowed to `CstTypeGroup` if it is a parenthesized type, or `nil` otherwise. +function utils.isTypeGroup(n: types.CstNode): types.CstTypeGroup? + return if n.kind == "type" and n.tag == "group" then n else nil +end + +--- Returns `n` narrowed to `CstTypeOptional` if it is an optional type (`T?`), or `nil` otherwise. +function utils.isTypeOptional(n: types.CstNode): types.CstTypeOptional? + return if n.kind == "type" and n.tag == "optional" then n else nil +end + +--- Returns `n` narrowed to `CstTypeUnion` if it is a type union, or `nil` otherwise. +function utils.isTypeUnion(n: types.CstNode): types.CstTypeUnion? + return if n.kind == "type" and n.tag == "union" then n else nil +end + +--- Returns `n` narrowed to `CstTypeIntersection` if it is a type intersection, or `nil` otherwise. +function utils.isTypeIntersection(n: types.CstNode): types.CstTypeIntersection? + return if n.kind == "type" and n.tag == "intersection" then n else nil +end + +--- Returns `n` narrowed to `CstTypeArray` if it is an array type (`{T}`), or `nil` otherwise. +function utils.isTypeArray(n: types.CstNode): types.CstTypeArray? + return if n.kind == "type" and n.tag == "array" then n else nil +end + +--- Returns `n` narrowed to `CstTypeTable` if it is a table type, or `nil` otherwise. +function utils.isTypeTable(n: types.CstNode): types.CstTypeTable? + return if n.kind == "type" and n.tag == "table" then n else nil +end + +--- Returns `n` narrowed to `CstTypeFunction` if it is a function type, or `nil` otherwise. +function utils.isTypeFunction(n: types.CstNode): types.CstTypeFunction? + return if n.kind == "type" and n.tag == "function" then n else nil +end + +--- Returns `n` narrowed to `CstType` if it is any type annotation node, or `nil` otherwise. +function utils.isType(n: types.CstNode): types.CstType? + return if n.kind == "type" then n else nil +end + +--- Returns `n` narrowed to `CstTypePackExplicit` if it is an explicit type pack, or `nil` otherwise. +function utils.isTypePackExplicit(n: types.CstNode): types.CstTypePackExplicit? + return if n.kind == "typepack" and n.tag == "explicit" then n else nil +end + +--- Returns `n` narrowed to `CstTypePackVariadic` if it is a variadic type pack, or `nil` otherwise. +function utils.isTypePackVariadic(n: types.CstNode): types.CstTypePackVariadic? + return if n.kind == "typepack" and n.tag == "variadic" then n else nil +end + +--- Returns `n` narrowed to `CstTypePackGeneric` if it is a generic type pack, or `nil` otherwise. +function utils.isTypePackGeneric(n: types.CstNode): types.CstTypePackGeneric? + return if n.kind == "typepack" and n.tag == "generic" then n else nil +end + +--- Returns `n` narrowed to `CstTypePack` if it is any type pack node, or `nil` otherwise. +function utils.isTypePack(n: types.CstNode): types.CstTypePack? + return if n.kind == "typepack" then n else nil +end + +--- Returns `n` narrowed to `CstToken` if it is a token, or `nil` otherwise. +function utils.isToken(n: types.CstNode): types.CstToken? + return if n.kind == "token" then n else nil +end + +--- Returns `n` narrowed to `CstAttribute` if it is an attribute node, or `nil` otherwise. +function utils.isAttribute(n: types.CstNode): types.CstAttribute? + return if n.kind == "attribute" then n else nil +end + +--- Returns a `Replacement` that replaces the original node with the given content, preserving trivia. +function utils.replacementWithTrivia(content: string): types.Replacement + return { content = content } +end + +--- Returns a `Replacement` that replaces the original node with the given content, discarding trivia. +function utils.replacementWithoutTrivia(content: string): types.Replacement + return { content = content, preserveTrivia = false } +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..6277436d2 --- /dev/null +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -0,0 +1,60 @@ +local query = require("../query") +local types = require("../types") + +local retrieverLib = {} + +--- Returns the leading trivia of the leftmost token in `n`, or an empty array if there are no tokens. +function retrieverLib.leftmostTrivia(n: types.CstNode): { types.Trivia } + local q = query.findAllFromRoot<>( + n :: any, + function(n: types.CstNode) -- LUAUFIX: Code too complex emitted because CstNode is very large + return if n.kind == "token" then n else nil + end + ) + + local leftmostToken: types.CstToken? = 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 + +--- Returns the trailing trivia of the rightmost token in `n`, or an empty array if there are no tokens. +function retrieverLib.rightmostTrivia(n: types.CstNode): { types.Trivia } + local q = query.findAllFromRoot( + n :: any, + function(n: types.CstNode) -- LUAUFIX: Code too complex emitted because CstNode is very large + return if n.kind == "token" then n else nil + end + ) + + local rightmostToken: types.CstToken? = 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..cd389a38f 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1,147 +1,188 @@ --!strict +--!native +-- @std/syntax/visitor +-- Visitor pattern implementation for traversing Luau CST nodes -local luau = require("@lute/luau") +local types = require("./types") +local visitorlib = {} + +--- A table of callbacks invoked during CST traversal. Each `visit` callback receives the corresponding node and +--- returns `true` to continue visiting its children, or `false` to skip them. "End" callbacks fire after all children +--- of that sort have been visited. Override only the callbacks you care about; unset callbacks default to visiting all children. 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.CstStatBlock) -> boolean, + visitStatBlockEnd: (types.CstStatBlock) -> (), + visitStatDo: (types.CstStatDo) -> boolean, + visitStatIf: (types.CstStatIf) -> boolean, + visitStatWhile: (types.CstStatWhile) -> boolean, + visitStatRepeat: (types.CstStatRepeat) -> boolean, + visitStatBreak: (types.CstStatBreak) -> boolean, + visitStatContinue: (types.CstStatContinue) -> boolean, + visitStatReturn: (types.CstStatReturn) -> boolean, + visitStatLocalDeclaration: (types.CstStatLocal) -> boolean, + visitStatLocalDeclarationEnd: (types.CstStatLocal) -> (), + visitStatConstDeclaration: (types.CstStatConst) -> boolean, + visitStatConstDeclarationEnd: (types.CstStatConst) -> (), + visitStatFor: (types.CstStatFor) -> boolean, + visitStatForEnd: (types.CstStatFor) -> (), + visitStatForIn: (types.CstStatForIn) -> boolean, + visitStatForInEnd: (types.CstStatForIn) -> (), + visitStatAssign: (types.CstStatAssign) -> boolean, + visitStatCompoundAssign: (types.CstStatCompoundAssign) -> boolean, + visitStatFunction: (types.CstStatFunction) -> boolean, + visitStatLocalFunction: (types.CstStatLocalFunction) -> boolean, + visitStatTypeAlias: (types.CstStatTypeAlias) -> boolean, + visitStatTypeFunction: (types.CstStatTypeFunction) -> boolean, + visitStatExpr: (types.CstStatExpr) -> boolean, + + visitExpr: (types.CstExpr) -> boolean, + visitExprEnd: (types.CstExpr) -> (), + visitExprConstantNil: (types.CstExprConstantNil) -> boolean, + visitExprConstantString: (types.CstExprConstantString) -> boolean, + visitExprConstantBool: (types.CstExprConstantBool) -> boolean, + visitExprConstantNumber: (types.CstExprConstantNumber) -> boolean, + visitExprConstantInteger: (types.CstExprConstantInteger) -> boolean, + visitExprLocal: (types.CstExprLocal) -> boolean, + visitExprGlobal: (types.CstExprGlobal) -> boolean, + visitExprCall: (types.CstExprCall) -> boolean, + visitExprUnary: (types.CstExprUnary) -> boolean, + visitExprBinary: (types.CstExprBinary) -> boolean, + visitExprFunction: (types.CstExprFunction) -> boolean, + visitExprFunctionEnd: (types.CstExprFunction) -> (), + visitExprInstantiate: (types.CstExprInstantiate) -> boolean, + visitTableExprItem: (types.CstTableExprItem) -> boolean, + visitExprTable: (types.CstExprTable) -> boolean, + visitExprIndexName: (types.CstExprIndexName) -> boolean, + visitExprIndexExpr: (types.CstExprIndexExpr) -> boolean, + visitExprGroup: (types.CstExprGroup) -> boolean, + visitExprInterpString: (types.CstExprInterpString) -> boolean, + visitExprTypeAssertion: (types.CstExprTypeAssertion) -> boolean, + visitExprIfElse: (types.CstExprIfElse) -> boolean, + visitExprVarargs: (types.CstExprVarargs) -> boolean, + + visitTypeReference: (types.CstTypeReference) -> boolean, + visitTypeSingletonBool: (types.CstTypeSingletonBool) -> boolean, + visitTypeSingletonString: (types.CstTypeSingletonString) -> boolean, + visitTypeTypeof: (types.CstTypeTypeof) -> boolean, + visitTypeGroup: (types.CstTypeGroup) -> boolean, + visitTypeOptional: (types.CstTypeOptional) -> boolean, + visitTypeUnion: (types.CstTypeUnion) -> boolean, + visitTypeIntersection: (types.CstTypeIntersection) -> boolean, + visitTypeArray: (types.CstTypeArray) -> boolean, + visitTableTypeItem: (types.CstTableTypeItem) -> boolean, + visitTypeTable: (types.CstTypeTable) -> boolean, + visitTypeFunction: (types.CstTypeFunction) -> boolean, + + visitTypePackExplicit: (types.CstTypePackExplicit) -> boolean, + visitTypePackGeneric: (types.CstTypePackGeneric) -> boolean, + visitTypePackVariadic: (types.CstTypePackVariadic) -> boolean, + + visitToken: (types.CstToken) -> boolean, + + visitLocal: (types.CstLocal) -> boolean, + + visitAttribute: (types.CstAttribute) -> boolean, } -local function alwaysVisit(...: any) +local function alwaysVisit(...: types.CstNode) return true 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, - 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, - - visitTypeReference = alwaysVisit :: any, - visitTypeBoolean = alwaysVisit :: any, - visitTypeString = alwaysVisit :: any, - visitTypeTypeof = alwaysVisit :: any, - visitTypeGroup = alwaysVisit :: any, - visitTypeUnion = alwaysVisit :: any, - visitTypeIntersection = alwaysVisit :: any, - visitTypeArray = alwaysVisit :: any, - visitTypeTable = alwaysVisit :: any, + visitStatBlock = alwaysVisit, + visitStatBlockEnd = alwaysVisit :: (types.CstNode) -> (), + visitStatDo = alwaysVisit, + visitStatIf = alwaysVisit, + visitStatWhile = alwaysVisit, + visitStatRepeat = alwaysVisit, + visitStatBreak = alwaysVisit, + visitStatContinue = alwaysVisit, + visitStatReturn = alwaysVisit, + visitStatLocalDeclaration = alwaysVisit, + visitStatLocalDeclarationEnd = alwaysVisit :: (types.CstNode) -> (), + visitStatConstDeclaration = alwaysVisit, + visitStatConstDeclarationEnd = alwaysVisit :: (types.CstNode) -> (), + visitStatFor = alwaysVisit, + visitStatForEnd = alwaysVisit :: (types.CstNode) -> (), + visitStatForIn = alwaysVisit, + visitStatForInEnd = alwaysVisit :: (types.CstNode) -> (), + visitStatAssign = alwaysVisit, + visitStatCompoundAssign = alwaysVisit, + visitStatFunction = alwaysVisit, + visitStatLocalFunction = alwaysVisit, + visitStatTypeAlias = alwaysVisit, + visitStatTypeFunction = alwaysVisit, + visitStatExpr = alwaysVisit, + + visitExpr = alwaysVisit, + visitExprConstantNil = alwaysVisit, + visitExprConstantString = alwaysVisit, + visitExprConstantBool = alwaysVisit, + visitExprConstantNumber = alwaysVisit, + visitExprConstantInteger = alwaysVisit, + visitExprEnd = alwaysVisit :: (types.CstNode) -> (), + visitExprLocal = alwaysVisit, + visitExprGlobal = alwaysVisit, + visitExprCall = alwaysVisit, + visitExprUnary = alwaysVisit, + visitExprBinary = alwaysVisit, + visitExprFunction = alwaysVisit, + visitExprFunctionEnd = alwaysVisit :: (types.CstNode) -> (), + visitExprInstantiate = alwaysVisit, + visitTableExprItem = alwaysVisit :: (types.CstTableExprItem) -> boolean, + visitExprTable = alwaysVisit, + visitExprIndexName = alwaysVisit, + visitExprIndexExpr = alwaysVisit, + visitExprGroup = alwaysVisit, + visitExprInterpString = alwaysVisit, + visitExprTypeAssertion = alwaysVisit, + visitExprIfElse = alwaysVisit, + visitExprVarargs = alwaysVisit, + + visitTypeReference = alwaysVisit, + visitTypeSingletonBool = alwaysVisit, + visitTypeSingletonString = alwaysVisit, + visitTypeTypeof = alwaysVisit, + visitTypeGroup = alwaysVisit, + visitTypeOptional = alwaysVisit, + visitTypeUnion = alwaysVisit, + visitTypeIntersection = alwaysVisit, + visitTypeArray = alwaysVisit, + visitTableTypeItem = alwaysVisit :: (types.CstTableTypeItem) -> boolean, + visitTypeTable = alwaysVisit, visitTypeFunction = alwaysVisit, visitTypePackExplicit = alwaysVisit, visitTypePackGeneric = alwaysVisit, visitTypePackVariadic = alwaysVisit, - visitToken = alwaysVisit :: any, - visitNil = alwaysVisit :: any, - visitString = alwaysVisit :: any, - visitBoolean = alwaysVisit :: any, - visitNumber = alwaysVisit :: any, - visitLocal = alwaysVisit :: any, - visitVarargs = alwaysVisit :: any, + visitToken = alwaysVisit, + + visitLocal = alwaysVisit, + + visitAttribute = alwaysVisit, } 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.CstToken, visitor: Visitor) visitor.visitToken(token) end -local function visitPunctuated(list: luau.Punctuated, visitor: Visitor, apply: (T, Visitor) -> ()) - for _, item in list do - apply(item.node, visitor) - if item.separator then - visitToken(item.separator, visitor) +local function visitPunctuated(list: types.CstPunctuated, visitor: Visitor, apply: (T, Visitor) -> ()) + for i, item in list do + apply(item, visitor) + local sep = list.separators[i] + if sep then + visitToken(sep, visitor) end end end -local function visitLocal(node: luau.AstLocal, visitor: Visitor) +local function visitLocal(node: types.CstLocal, visitor: Visitor) if visitor.visitLocal(node) then visitToken(node.name, visitor) if node.colon then @@ -153,126 +194,169 @@ 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 visitTypeOrTypePack(node: types.CstType | types.CstTypePack, visitor: Visitor) + if node.kind == "type" then + visitType(node, visitor) + else + visitTypePack(node, visitor) + end +end + +local function visitStatBlock(block: types.CstStatBlock, 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.CstStatDo, visitor: Visitor) + if visitor.visitStatDo(node) then + visitToken(node.doKeyword, visitor) + visitStatBlock(node.body, visitor) + visitToken(node.endKeyword, visitor) end end -local function visitIf(node: luau.AstStatIf, visitor: Visitor) - if visitor.visitIf(node) then +local function visitStatIf(node: types.CstStatIf, visitor: Visitor) + if visitor.visitStatIf(node) then visitToken(node.ifKeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) visitToken(node.thenKeyword, visitor) - visitBlock(node.consequent, visitor) + visitStatBlock(node.thenBlock, visitor) for _, elseifNode in node.elseifs do - visitToken(elseifNode.elseifKeyword, visitor) - visitExpression(elseifNode.condition, visitor) + visitToken(elseifNode.elseIfKeyword, visitor) + visitExpr(elseifNode.condition, visitor) visitToken(elseifNode.thenKeyword, visitor) - visitBlock(elseifNode.consequent, visitor) + visitStatBlock(elseifNode.thenBlock, visitor) end 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) end end -local function visitWhile(node: luau.AstStatWhile, visitor: Visitor) - if visitor.visitWhile(node) then +local function visitStatWhile(node: types.CstStatWhile, visitor: Visitor) + if visitor.visitStatWhile(node) then visitToken(node.whileKeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) visitToken(node.doKeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.endKeyword, visitor) end end -local function visitRepeat(node: luau.AstStatRepeat, visitor: Visitor) - if visitor.visitRepeat(node) then +local function visitStatRepeat(node: types.CstStatRepeat, visitor: Visitor) + if visitor.visitStatRepeat(node) then visitToken(node.repeatKeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.untilKeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) end end -local function visitReturn(node: luau.AstStatReturn, visitor: Visitor) - if visitor.visitReturn(node) then +local function visitStatBreak(node: types.CstStatBreak, visitor: Visitor) + if visitor.visitStatBreak(node) then + visitToken(node.token, visitor) + end +end + +local function visitStatContinue(node: types.CstStatContinue, visitor: Visitor) + if visitor.visitStatContinue(node) then + visitToken(node.token, visitor) + end +end + +local function visitStatReturn(node: types.CstStatReturn, visitor: Visitor) + if visitor.visitStatReturn(node) then visitToken(node.returnKeyword, visitor) - visitPunctuated(node.expressions, visitor, visitExpression) + visitPunctuated(node.expressions, visitor, visitExpr) end end -local function visitLocalStatement(node: luau.AstStatLocal, visitor: Visitor) - if visitor.visitLocalDeclaration(node) then +local function visitLocalStatement(node: types.CstStatLocal, 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 +local function visitConstStatement(node: types.CstStatConst, visitor: Visitor) + if visitor.visitStatConstDeclaration(node) then + visitToken(node.constKeyword, visitor) + visitPunctuated(node.variables, visitor, visitLocal) + if node.equals then + visitToken(node.equals, visitor) + end + visitPunctuated(node.values, visitor, visitExpr) + + visitor.visitStatConstDeclarationEnd(node) + end +end + +local function visitStatFor(node: types.CstStatFor, visitor: Visitor) + if visitor.visitStatFor(node) then visitToken(node.forKeyword, visitor) visitLocal(node.variable, visitor) visitToken(node.equals, visitor) - visitExpression(node.from, visitor) + visitExpr(node.from, visitor) visitToken(node.toComma, visitor) - visitExpression(node.to, 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) + visitStatBlock(node.body, visitor) visitToken(node.endKeyword, visitor) + visitor.visitStatForEnd(node) end end -local function visitForIn(node: luau.AstStatForIn, visitor: Visitor) - if visitor.visitForIn(node) then +local function visitStatForIn(node: types.CstStatForIn, 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) + visitPunctuated(node.values, visitor, visitExpr) visitToken(node.doKeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.endKeyword, visitor) + visitor.visitStatForInEnd(node) 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.CstStatAssign, 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) - visitToken(node.operand, visitor) - visitExpression(node.value, visitor) +local function visitStatCompoundAssign(node: types.CstStatCompoundAssign, visitor: Visitor) + if visitor.visitStatCompoundAssign(node) then + visitExpr(node.variable, visitor) + visitToken(node.operator, visitor) + visitExpr(node.value, visitor) end end -local function visitGeneric(node: luau.AstGenericType, visitor: Visitor) +local function visitGeneric(node: types.CstGenericType, visitor: Visitor) visitToken(node.name, visitor) if node.equals then visitToken(node.equals, visitor) @@ -282,7 +366,7 @@ local function visitGeneric(node: luau.AstGenericType, visitor: Visitor) end end -local function visitGenericPack(node: luau.AstGenericTypePack, visitor: Visitor) +local function visitGenericPack(node: types.CstGenericTypePack, visitor: Visitor) visitToken(node.name, visitor) visitToken(node.ellipsis, visitor) if node.equals then @@ -293,9 +377,10 @@ local function visitGenericPack(node: luau.AstGenericTypePack, visitor: Visitor) end end -local function visitTypeAlias(node: luau.AstStatTypeAlias, visitor: Visitor) - if visitor.visitTypeAlias(node) then - if node.export then +local function visitStatTypeAlias(node: types.CstStatTypeAlias, visitor: Visitor) + if visitor.visitStatTypeAlias(node) then + if node.isExported then + assert(node.export) visitToken(node.export, visitor) end visitToken(node.typeToken, visitor) @@ -317,174 +402,204 @@ local function visitTypeAlias(node: luau.AstStatTypeAlias, visitor: Visitor) end end -local function visitString(node: luau.AstExprConstantString, visitor: Visitor) - if visitor.visitString(node) then - visitor.visitToken(node) +local function visitStatExpr(node: types.CstStatExpr, visitor: Visitor) + if visitor.visitStatExpr(node) then + visitExpr(node.expression, visitor) end end -local function visitNil(node: luau.AstExprConstantNil, visitor: Visitor) - if visitor.visitNil(node) then - visitToken(node, visitor) +local function visitExprConstantString(node: types.CstExprConstantString, visitor: Visitor) + if visitor.visitExprConstantString(node) then + visitor.visitToken(node.value) end end -local function visitBoolean(node: luau.AstExprConstantBool, visitor: Visitor) - if visitor.visitBoolean(node) then - visitToken(node, visitor) +local function visitExprConstantNil(node: types.CstExprConstantNil, visitor: Visitor) + if visitor.visitExprConstantNil(node) then + visitToken(node.token, visitor) end end -local function visitNumber(node: luau.AstExprConstantNumber, visitor: Visitor) - if visitor.visitNumber(node) then - visitToken(node, visitor) +local function visitExprConstantBool(node: types.CstExprConstantBool, visitor: Visitor) + if visitor.visitExprConstantBool(node) then + visitToken(node.token, visitor) end end -local function visitLocalReference(node: luau.AstExprLocal, visitor: Visitor) - if visitor.visitLocalReference(node) then +local function visitExprConstantNumber(node: types.CstExprConstantNumber, visitor: Visitor) + if visitor.visitExprConstantNumber(node) then + visitToken(node.token, visitor) + end +end + +local function visitExprConstantInteger(node: types.CstExprConstantInteger, visitor: Visitor) + if visitor.visitExprConstantInteger(node) then + visitToken(node.token, visitor) + end +end + +local function visitExprLocal(node: types.CstExprLocal, 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.CstExprGlobal, 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 - visitToken(node, visitor) +local function visitExprVarargs(node: types.CstExprVarargs, visitor: Visitor) + if visitor.visitExprVarargs(node) then + visitToken(node.token, visitor) end end -local function visitCall(node: luau.AstExprCall, visitor: Visitor) - if visitor.visitCall(node) then - visitExpression(node.func, visitor) +local function visitExprCall(node: types.CstExprCall, 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) + 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.CstExprUnary, 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.CstExprBinary, 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 visitAttribute(node: types.CstAttribute, visitor: Visitor) + if visitor.visitAttribute(node) then + visitToken(node.name, visitor) end - if node.generics then +end + +local function visitExprFunction( + node: types.CstExprFunction, + visitor: Visitor, + shouldVisitAttributes: boolean?, + shouldVisitFunctionKeyword: boolean? +) + if visitor.visitExprFunction(node) then + if shouldVisitAttributes ~= false then + for _, attribute in node.attributes do + visitAttribute(attribute, visitor) + end + end + if shouldVisitFunctionKeyword ~= false then + visitToken(node.functionKeyword, visitor) + end + if node.openGenerics then + visitToken(node.openGenerics, visitor) + end visitPunctuated(node.generics, visitor, visitGeneric) - end - if node.genericPacks then visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.closeGenerics then + visitToken(node.closeGenerics, visitor) + end + visitToken(node.openParens, visitor) + visitPunctuated(node.parameters, visitor, visitLocal) + if node.hasVararg then + assert(node.vararg) + visitToken(node.vararg, visitor) + end + if node.varargColon then + visitToken(node.varargColon, visitor) + end + if node.varargAnnotation then + visitTypePack(node.varargAnnotation, visitor) + end + visitToken(node.closeParens, visitor) + if node.returnSpecifier then + visitToken(node.returnSpecifier, visitor) + end + if node.returnAnnotation then + visitTypePack(node.returnAnnotation, visitor) + end + visitStatBlock(node.body, visitor) + visitToken(node.endKeyword, visitor) + visitor.visitExprFunctionEnd(node) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) - end - 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) - end - if node.varargAnnotation then - visitTypePack(node.varargAnnotation, visitor) - end - visitToken(node.closeParens, visitor) - if node.returnSpecifier then - visitToken(node.returnSpecifier, visitor) - end - if node.returnAnnotation then - visitTypePack(node.returnAnnotation, visitor) - end - visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) end -local function visitAttribute(node: luau.AstAttribute, visitor) - visitToken(node, visitor) -end - -local function visitAnonymousFunction(node: luau.AstExprAnonymousFunction, visitor: Visitor) - if visitor.visitAnonymousFunction(node) then - for _, attribute in node.attributes do - visitAttribute(attribute, visitor) - end - visitToken(node.functionKeyword, visitor) - visitFunctionBody(node.body, visitor) +local function visitExprInstantiate(node: types.CstExprInstantiate, visitor: Visitor) + if visitor.visitExprInstantiate(node) then + visitExpr(node.expr, visitor) + visitToken(node.leftArrow1, visitor) + visitToken(node.leftArrow2, visitor) + visitPunctuated(node.typeArguments, visitor, visitTypeOrTypePack) + visitToken(node.rightArrow1, visitor) + visitToken(node.rightArrow2, visitor) end end -local function visitFunction(node: luau.AstStatFunction, visitor: Visitor) - if visitor.visitFunction(node) then - for _, attribute in node.attributes do +local function visitStatFunction(node: types.CstStatFunction, visitor: Visitor) + if visitor.visitStatFunction(node) then + -- We need to visit in lexical order + for _, attribute in node.func.attributes do visitAttribute(attribute, visitor) end - visitToken(node.functionKeyword, visitor) - visitExpression(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitToken(node.func.functionKeyword, visitor) + visitExpr(node.name, visitor) + visitExprFunction(node.func, visitor, false, false) end end -local function visitLocalFunction(node: luau.AstStatLocalFunction, visitor: Visitor) - if visitor.visitLocalFunction(node) then - for _, attribute in node.attributes do +local function visitStatLocalFunction(node: types.CstStatLocalFunction, visitor: Visitor) + if visitor.visitStatLocalFunction(node) then + for _, attribute in node.func.attributes do visitAttribute(attribute, visitor) end visitToken(node.localKeyword, visitor) - visitToken(node.functionKeyword, visitor) + visitToken(node.func.functionKeyword, visitor) visitLocal(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitExprFunction(node.func, visitor, false, false) end end -local function visitStatTypeFunction(node: luau.AstStatTypeFunction, visitor: Visitor) +local function visitStatTypeFunction(node: types.CstStatTypeFunction, visitor: Visitor) if visitor.visitStatTypeFunction(node) then - if node.export then + if node.isExported then + assert(node.export) visitToken(node.export, visitor) end visitToken(node.type, visitor) - visitToken(node.functionKeyword, visitor) + visitToken(node.body.functionKeyword, visitor) visitToken(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitExprFunction(node.body, visitor, true, false) end end -local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) - if visitor.visitTableItem(node) then +local function visitTableExprItem(node: types.CstTableExprItem, visitor: Visitor) + if visitor.visitTableExprItem(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) + 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,86 +610,86 @@ local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) end end -local function visitTable(node: luau.AstExprTable, visitor: Visitor) - if visitor.visitTable(node) then +local function visitExprTable(node: types.CstExprTable, visitor: Visitor) + if visitor.visitExprTable(node) then visitToken(node.openBrace, visitor) for _, item in node.entries do - visitTableItem(item, visitor) + visitTableExprItem(item, visitor) end 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.CstExprIndexName, 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) +local function visitExprIndexExpr(node: types.CstExprIndexExpr, visitor: Visitor) + if visitor.visitExprIndexExpr(node) then + visitExpr(node.expression, visitor) visitToken(node.openBrackets, visitor) - visitExpression(node.index, visitor) + visitExpr(node.index, visitor) visitToken(node.closeBrackets, visitor) end end -local function visitGroup(node: luau.AstExprGroup, visitor: Visitor) - if visitor.visitGroup(node) then +local function visitExprGroup(node: types.CstExprGroup, visitor: Visitor) + if visitor.visitExprGroup(node) then visitToken(node.openParens, visitor) - visitExpression(node.expression, 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.CstExprInterpString, 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.CstExprTypeAssertion, 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 +local function visitExprIfElse(node: types.CstExprIfElse, visitor: Visitor) + if visitor.visitExprIfElse(node) then visitToken(node.ifKeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) visitToken(node.thenKeyword, visitor) - visitExpression(node.consequent, visitor) + visitExpr(node.thenExpr, visitor) for _, elseifs in node.elseifs do - visitToken(elseifs.elseifKeyword, visitor) - visitExpression(elseifs.condition, visitor) + visitToken(elseifs.elseIfKeyword, visitor) + visitExpr(elseifs.condition, visitor) visitToken(elseifs.thenKeyword, visitor) - visitExpression(elseifs.consequent, visitor) + visitExpr(elseifs.thenExpr, visitor) end visitToken(node.elseKeyword, visitor) - visitExpression(node.antecedent, 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.CstType | types.CstTypePack, 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.CstTypeReference, visitor: Visitor) if visitor.visitTypeReference(node) then if node.prefix then visitToken(node.prefix, visitor) @@ -595,28 +710,28 @@ local function visitTypeReference(node: luau.AstTypeReference, visitor: Visitor) end end -local function visitTypeBoolean(node: luau.AstTypeSingletonBool, visitor: Visitor) - if visitor.visitTypeBoolean(node) then - visitToken(node, visitor) +local function visitTypeSingletonBool(node: types.CstTypeSingletonBool, visitor: Visitor) + if visitor.visitTypeSingletonBool(node) then + visitToken(node.token, visitor) end end -local function visitTypeString(node: luau.AstTypeSingletonString, visitor: Visitor) - if visitor.visitTypeString(node) then - visitToken(node, visitor) +local function visitTypeSingletonString(node: types.CstTypeSingletonString, visitor: Visitor) + if visitor.visitTypeSingletonString(node) then + visitToken(node.value, visitor) end end -local function visitTypeTypeof(node: luau.AstTypeTypeof, visitor: Visitor) +local function visitTypeTypeof(node: types.CstTypeTypeof, visitor: Visitor) if visitor.visitTypeTypeof(node) then visitToken(node.typeof, visitor) visitToken(node.openParens, visitor) - visitExpression(node.expression, visitor) + visitExpr(node.expression, visitor) visitToken(node.closeParens, visitor) end end -local function visitTypeGroup(node: luau.AstTypeGroup, visitor: Visitor) +local function visitTypeGroup(node: types.CstTypeGroup, visitor: Visitor) if visitor.visitTypeGroup(node) then visitToken(node.openParens, visitor) visitType(node.type, visitor) @@ -624,7 +739,13 @@ local function visitTypeGroup(node: luau.AstTypeGroup, visitor: Visitor) end end -local function visitTypeUnion(node: luau.AstTypeUnion, visitor: Visitor) +local function visitTypeOptional(node: types.CstTypeOptional, visitor: Visitor) + if visitor.visitTypeOptional(node) then + visitToken(node.token, visitor) + end +end + +local function visitTypeUnion(node: types.CstTypeUnion, visitor: Visitor) if visitor.visitTypeUnion(node) then if node.leading then visitToken(node.leading, visitor) @@ -633,7 +754,7 @@ local function visitTypeUnion(node: luau.AstTypeUnion, visitor: Visitor) end end -local function visitTypeIntersection(node: luau.AstTypeIntersection, visitor: Visitor) +local function visitTypeIntersection(node: types.CstTypeIntersection, visitor: Visitor) if visitor.visitTypeIntersection(node) then if node.leading then visitToken(node.leading, visitor) @@ -642,7 +763,7 @@ local function visitTypeIntersection(node: luau.AstTypeIntersection, visitor: Vi end end -local function visitTypeArray(node: luau.AstTypeArray, visitor: Visitor) +local function visitTypeArray(node: types.CstTypeArray, visitor: Visitor) if visitor.visitTypeArray(node) then visitToken(node.openBrace, visitor) if node.access then @@ -653,35 +774,45 @@ local function visitTypeArray(node: luau.AstTypeArray, visitor: Visitor) end end -local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) +local function visitTableTypeItem(node: types.CstTableTypeItem, visitor: Visitor) + if visitor.visitTableTypeItem(node) then + if node.access then + visitToken(node.access, visitor) + end + if node.kind == "indexer" then + visitToken(node.indexerOpen, visitor) + visitType(node.key, visitor) + visitToken(node.indexerClose, visitor) + elseif node.kind == "property" then + if node.indexerOpen then + visitToken(node.indexerOpen, visitor) + end + visitToken(node.key, visitor) + if node.indexerClose then + visitToken(node.indexerClose, visitor) + end + else + exhaustiveMatch(node.kind) + end + visitToken(node.colon, visitor) + visitType(node.value, visitor) + if node.separator then + visitToken(node.separator, visitor) + end + end +end + +local function visitTypeTable(node: types.CstTypeTable, visitor: Visitor) if visitor.visitTypeTable(node) then 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) - visitType(entry.key, visitor) - visitToken(entry.indexerClose, visitor) - elseif entry.kind == "stringproperty" then - visitToken(entry.indexerOpen, visitor) - visitTypeString(entry.key, visitor) - visitToken(entry.indexerClose, visitor) - else - visitToken(entry.key, visitor) - end - visitToken(entry.colon, visitor) - visitType(entry.value, visitor) - if entry.separator then - visitToken(entry.separator, visitor) - end + visitTableTypeItem(entry, visitor) end visitToken(node.closeBrace, visitor) end end -local function visitTypeFunctionParameter(node: luau.AstTypeFunctionParameter, visitor) +local function visitFunctionTypeParameter(node: types.CstFunctionTypeParameter, visitor) if node.name then visitToken(node.name, visitor) end @@ -691,7 +822,7 @@ 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.CstTypeFunction, visitor: Visitor) if visitor.visitTypeFunction(node) then if node.openGenerics then visitToken(node.openGenerics, visitor) @@ -706,7 +837,7 @@ local function visitTypeFunction(node: luau.AstTypeFunction, visitor: Visitor) visitToken(node.closeGenerics, visitor) end visitToken(node.openParens, visitor) - visitPunctuated(node.parameters, visitor, visitTypeFunctionParameter) + visitPunctuated(node.parameters, visitor, visitFunctionTypeParameter) if node.vararg then visitTypePack(node.vararg, visitor) end @@ -716,7 +847,7 @@ local function visitTypeFunction(node: luau.AstTypeFunction, visitor: Visitor) end end -local function visitTypePackExplicit(node: luau.AstTypePackExplicit, visitor: Visitor) +local function visitTypePackExplicit(node: types.CstTypePackExplicit, visitor: Visitor) if visitor.visitTypePackExplicit(node) then if node.openParens then visitToken(node.openParens, visitor) @@ -731,14 +862,14 @@ local function visitTypePackExplicit(node: luau.AstTypePackExplicit, visitor: Vi end end -local function visitTypePackGeneric(node: luau.AstTypePackGeneric, visitor: Visitor) +local function visitTypePackGeneric(node: types.CstTypePackGeneric, 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.CstTypePackVariadic, visitor: Visitor) if visitor.visitTypePackVariadic(node) then if node.ellipsis then visitToken(node.ellipsis, visitor) @@ -747,85 +878,93 @@ 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.CstExpr, 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 == "integer" then + visitExprConstantInteger(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) + visitExprFunction(expression, visitor) + elseif expression.tag == "instantiate" then + visitExprInstantiate(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.CstStat, 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 == "const" then + visitConstStatement(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 +972,13 @@ function visitStatement(statement: luau.AstStat, visitor: Visitor) end end -function visitType(type: luau.AstType, visitor: Visitor) +function visitType(type: types.CstType, 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 +988,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 +1000,7 @@ function visitType(type: luau.AstType, visitor: Visitor) end end -function visitTypePack(type: luau.AstTypePack, visitor: Visitor) +function visitTypePack(type: types.CstTypePack, visitor: Visitor) if type.tag == "explicit" then visitTypePackExplicit(type, visitor) elseif type.tag == "generic" then @@ -873,16 +1012,139 @@ function visitTypePack(type: luau.AstTypePack, visitor: Visitor) end end -local function createVisitor() - return table.clone(defaultVisitor) +--- Creates a `Visitor` where every callback delegates to `visit`. If `visit` is omitted, all callbacks default to visiting every node. +local function create(visit: ((types.CstNode) -> boolean)?): Visitor + if visit then + return { + visitStatBlock = visit, + visitStatBlockEnd = function(s) + visit(s) + end, + visitStatDo = visit, + visitStatIf = visit, + visitStatWhile = visit, + visitStatRepeat = visit, + visitStatBreak = visit, + visitStatContinue = visit, + visitStatReturn = visit, + visitStatLocalDeclaration = visit, + visitStatLocalDeclarationEnd = function(s) + visit(s) + end, + visitStatConstDeclaration = visit, + visitStatConstDeclarationEnd = function(s) + visit(s) + end, + visitStatFor = visit, + visitStatForEnd = function(s) + visit(s) + end, + visitStatForIn = visit, + visitStatForInEnd = function(s) + visit(s) + end, + visitStatAssign = visit, + visitStatCompoundAssign = visit, + visitStatFunction = visit, + visitStatLocalFunction = visit, + visitStatTypeAlias = visit, + visitStatTypeFunction = visit, + visitStatExpr = visit, + + visitExpr = visit, + visitExprEnd = function(s) + visit(s) + end, + visitExprLocal = visit, + visitExprGlobal = visit, + visitExprCall = visit, + visitExprUnary = visit, + visitExprBinary = visit, + visitExprFunction = visit, + visitExprFunctionEnd = function(s) + visit(s) + end, + visitExprInstantiate = visit, + visitTableExprItem = visit :: (types.CstTableExprItem) -> boolean, + 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, + visitTableTypeItem = visit :: (types.CstTableTypeItem) -> boolean, + visitTypeTable = visit, + visitTypeFunction = visit, + + visitTypePackExplicit = visit, + visitTypePackGeneric = visit, + visitTypePackVariadic = visit, + + visitToken = visit :: (types.CstToken) -> boolean, + visitExprConstantNil = visit, + visitExprConstantString = visit, + visitExprConstantBool = visit, + visitExprConstantNumber = visit, + visitExprConstantInteger = visit, + visitLocal = visit, + visitExprVarargs = visit, + + visitAttribute = visit, + } + else + return table.clone(defaultVisitor) + end end -return { - createVisitor = createVisitor, - visitBlock = visitBlock, - visitStatement = visitStatement, - visitExpression = visitExpression, - visitType = visitType, - visitTypePack = visitTypePack, - visitToken = visitToken, -} +--- Dispatches `node` to the appropriate `visitor` callback based on its `kind`. +local function visit(node: types.CstNode, 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.kind == "token" then + visitToken(node, visitor) + elseif (node :: types.CstTableExprItem).isTableItem then + visitTableExprItem(node :: types.CstTableExprItem, visitor) + else + exhaustiveMatch(node.kind) + end +end + +--- Creates a `Visitor` where every callback delegates to `visit`. If `visit` is omitted, all callbacks default to visiting every node. +visitorlib.create = create +--- Visits all statements in a block. +visitorlib.visitBlock = visitStatBlock +--- Visits a single statement, dispatching to the appropriate callback. +visitorlib.visitStatement = visitStatement +--- Visits an expression, dispatching to the appropriate callback. +visitorlib.visitExpression = visitExpr +--- Visits a type annotation, dispatching to the appropriate callback. +visitorlib.visitType = visitType +--- Visits a type pack, dispatching to the appropriate callback. +visitorlib.visitTypePack = visitTypePack +--- Visits a single token. +visitorlib.visitToken = visitToken +--- Visits any CST node, dispatching by `node.kind`. +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..18b1e48c2 --- /dev/null +++ b/lute/std/libs/system/init.luau @@ -0,0 +1,34 @@ +--!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 = {} + +--- Returns the path of the system's temporary directory. +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..2498d8f5f --- /dev/null +++ b/lute/std/libs/tableext.luau @@ -0,0 +1,182 @@ +--!strict +-- @std/tableext +-- stdlib for an extension of the built-in table library in Luau +-- Provides additional utility functions for table operations + +local tableext = {} + +-- Functions on general tables + +--- Returns a new table with the same keys as `table`, where each value has been transformed by applying `f` to it. +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 + +--- Returns a new table containing only the key-value pairs from `table` for which `predicate` returns `true`. +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 + +--- Returns the first key-value pair from `table` for which `predicate` returns `true`. Returns `nil, nil` if the predicate does not hold for any value. +function tableext.find(table: { [K]: V }, predicate: (V) -> boolean): (V?, K?) + for k, v in table do + if predicate(v) then + return v, k + end + end + return nil, nil +end + +--- Merges all additional tables into `tbl` in order. When the same key appears in multiple tables, the value from the last table containing that key wins. Returns `tbl`. +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 + +--- Returns an array of all keys in `tbl`. Order is not guaranteed. +function tableext.keys(tbl: { [K]: V }): { K } + local keys = {} + for k, _ in tbl do + table.insert(keys, k) + end + return keys +end + +--- Returns an array of all values in `tbl`. Order is not guaranteed. +function tableext.values(tbl: { [K]: V }): { V } + local values = {} + for _, v in tbl do + table.insert(values, v) + end + return values +end + +type DeepCloneOptions = { preserveMetatables: boolean?, cloneKeys: boolean?, preserveFrozen: boolean? } + +function resolveValue(potentialTbl: T, seen: { [T]: T }, opts: DeepCloneOptions): T + return if seen[potentialTbl] then seen[potentialTbl] else deepClone(potentialTbl, seen, opts) +end + +function deepClone(tbl: T, seen: { [T]: T }, opts: DeepCloneOptions): T + local clone = {} + + assert(typeof(tbl) == "table", `deepClone expects a table, but got: {typeof(tbl)}`) + + seen[tbl] = clone + + for k, v in tbl :: any do + local resolvedKey = if opts.cloneKeys and typeof(k) == "table" then resolveValue(k, seen, opts) else k + + clone[resolvedKey] = if typeof(v) == "table" then resolveValue(v, seen, opts) else v + end + + if opts.preserveMetatables then + local currentMeta = getmetatable(tbl) + if typeof(currentMeta) == "table" then + setmetatable(clone, currentMeta) + end + end + + if opts.preserveFrozen and table.isfrozen(tbl) then + table.freeze(clone) + end + + return clone +end + +--- Recursively deep-clones `tbl`. By default, metatables are stripped, keys are kept as-is without any cloning, and frozenness of tables is not preserved. +--- If `options.preserveMetatables` is true, metatables are copied onto cloned tables (the metatable itself +--- is shared, not cloned; tables with protected metatables via `__metatable` are skipped). +--- If `options.cloneKeys` is true, keys that are tables will also be deep-cloned. +--- If `options.preserveFrozen` is true, any root level and nested tables that are frozen will be frozen in the clone. +function tableext.deepClone(tbl: T, options: DeepCloneOptions?): T + local cloneCache = {} + + return deepClone(tbl, cloneCache, { + preserveFrozen = options and options.preserveFrozen == true, + preserveMetatables = options and options.preserveMetatables == true, + cloneKeys = options and options.cloneKeys == true, + }) +end + +-- Functions on array-like tables + +--- Converts an array into a set-like table where each element maps to `true`. +@native +function tableext.toSet(tbl: { T }): { [T]: true } + local set: { [T]: true } = {} + for _, v in tbl do + set[v] = true + end + return set +end + +--- Appends all elements from each additional array into `tbl` in order, mutating it in place. +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 + +--- Returns `tbl` with its elements in reverse order. If `inplace` is `true`, reverses `tbl` in place; otherwise returns a new array. +function tableext.reverse(tbl: { T }, inplace: boolean?): { T } + 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 + +--- Returns `true` if `predicate` returns `true` for at least one element of `arr`. +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 + +--- Returns `true` if `predicate` returns `true` for every element of `arr`. +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 + +--- Maps each element of `arr` through `map` and joins the resulting strings with `sep` (default ""). +function tableext.mapConcat(arr: { T }, map: (T) -> string, sep: string?): string + return table.concat(tableext.map(arr, map), sep) +end + +return table.freeze(tableext) diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 1c22cbfe2..378e70023 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -1,78 +1,35 @@ -local task = require("@lute/task") +--!strict +-- @std/task +-- stdlib for `@lute/task` +-- Provides utilities for creating and managing asynchronous tasks -export type task = { - success: boolean?, - result: any, - co: thread, -} +local lutetask = require("@lute/task") -local function create(f, ...): task - local data = {} +local task = {} - data.co = coroutine.create(function(...) - local success, result = pcall(f, ...) - - data.success = success - data.result = result - end) - - coroutine.resume(data.co, ...) - return data +--- Schedules `routine` to run immediately on the next resumption cycle, passing any additional arguments to it. Returns the thread. +function task.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread + return lutetask.spawn(routine, ...) end -local function await(t: task) - if not t.co then - error(`await: argument 1 is not a task`) - end - - while t.success == nil do - task.defer() - end - - if t.success then - return t.result - else - error(t.result) - end +--- Schedules `routine` to run after the current thread yields, passing any additional arguments to it. Returns the thread. +function task.defer(routine: ((T...) -> U...) | thread, ...: T...): thread + return lutetask.defer(routine, ...) end -local function awaitall(...: task) - local tasks = table.pack(...) - tasks.n = nil - - for i, v in tasks do - if not v.co then - error(`awaitAll: argument {i} is not a task`) - end - end - - local done = false - - while not done do - done = true - for i, v in tasks do - if v.success == nil then - done = false - task.defer() - end - end - end - - local results = {} +--- Schedules `routine` to run after `dur` seconds, passing any additional arguments to it. Returns the thread. +function task.delay(dur: number, routine: ((T...) -> U...) | thread, ...: T...): thread + return lutetask.delay(dur, routine, ...) +end - for i, v in tasks do - if v.success then - table.insert(results, v.result) - else - error(v.result) - end - end +--- Yields the current thread for `dur` seconds (or until the next cycle if `dur` is omitted). Returns the actual time waited. +function task.wait(dur: number?): number + return lutetask.wait(dur) +end - return table.unpack(results) +--- Cancels `thread`, preventing it from being resumed again. +function task.cancel(thread: thread) + coroutine.close(thread) end -return table.freeze({ - create = create, - await = await, - awaitall = awaitall, -}) +return table.freeze(task) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau new file mode 100644 index 000000000..f94dd8e48 --- /dev/null +++ b/lute/std/libs/test/assert.luau @@ -0,0 +1,302 @@ +--!strict +--!native +-- @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 that(negate: boolean, value: unknown, msg: string?): Failure? + if negate then + if not value then + return nil + end + return assertion(msg or `Expected a falsy value, but got {formatValue(value)}`, nil, true) + end + + if value then + return nil + end + return assertion(msg or `Expected a truthy value, but got {formatValue(value)}`) +end + +local function throws(negate: boolean, func: (T...) -> ...unknown, ...: T...): Failure? + local success = pcall(func, ...) + + if negate then + if success then + return nil + end + return assertion(`{func} threw an unexpected error.`, nil, true) + end + + if not success then + return nil + end + return assertion(`{func} did not throw error.`) +end + +-- FIXME(Luau): We would like this to be two read-only indexers, but the +-- best option we have otherwise is two error suppressing indexers. +-- Table equality assertionsion with deep comparison +local function tableeq(negate: boolean, lhs: { [any]: any }, rhs: { [any]: any }, depth: number?): Failure? + local stackdepth = depth or 4 + local equal, msg = tablesEqual(lhs, rhs, "") + + if negate then + if not equal then + return nil + end + return assertion("expected tables to not be equal", stackdepth, true) + end + + if equal then + return nil + end + return assertion(msg or "tables are not equal", stackdepth) +end + +local function buffereq(negate: boolean, lhs: buffer, rhs: buffer, depth: number?): Failure? + local stackdepth = depth or 4 + + if typeof(lhs) ~= "buffer" then + return assertion(`lhs is of type {typeof(lhs)}, not buffer`, stackdepth) + end + + if typeof(rhs) ~= "buffer" then + return assertion(`rhs is of type {typeof(rhs)}, not buffer`, stackdepth) + end + + if negate then + if buffer.len(lhs) ~= buffer.len(rhs) then + return nil + end + for offset = 0, buffer.len(lhs) - 1 do + if buffer.readu8(lhs, offset) ~= buffer.readu8(rhs, offset) then + return nil + end + end + return assertion("expected buffers to not be equal", stackdepth, true) + end + + if buffer.len(lhs) ~= buffer.len(rhs) then + return assertion("buffers are of unequal length", stackdepth) + 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), + stackdepth + ) + end + end + return nil +end + +local function eq(negate: boolean, lhs: unknown, rhs: unknown, msg: string?): Failure? + local stackdepth = 4 + + if negate then + if lhs == rhs then + return assertion(msg or `expected {formatValue(lhs)} to not equal {formatValue(rhs)}`, nil, true) + end + if typeof(lhs) ~= typeof(rhs) then + return nil + end + if typeof(lhs) == "buffer" then + return buffereq(negate, lhs :: buffer, rhs :: buffer, stackdepth + 1) + end + if typeof(lhs) == "table" then + return tableeq(negate, lhs :: { [any]: any }, rhs :: { [any]: any }, stackdepth + 1) + end + return nil + end + + if lhs == rhs then + return nil + end + if typeof(lhs) ~= typeof(rhs) then + return assertion(msg or `lhs is of type {typeof(lhs)}, but rhs is of type {typeof(rhs)}`) + end + if typeof(lhs) == "buffer" then + return buffereq(negate, lhs :: buffer, rhs :: buffer, stackdepth + 1) + end + if typeof(lhs) == "table" then + return tableeq(negate, lhs :: { [any]: any }, rhs :: { [any]: any }, stackdepth + 1) + end + return assertion(msg or `{lhs} ~= {rhs}`) +end + +local function throwsWith(negate: boolean, with: any, func: (T...) -> ...unknown, ...: T...): Failure? + local success, err = pcall(func, ...) + + if negate then + if success then + return nil + end + local actualErrorMessage = tostring(err) + if typeof(with) == "string" then + if not stringext.hasSuffix(actualErrorMessage, with) then + return nil + end + return assertion(`Expected function to not throw error with suffix "{with}", but it did`, nil, true) + else + return eq(true, err, with, `Expected function to not throw error "{formatValue(with)}", but it did`) + end + end + + if success then + return assertion(`Expected function to throw/raise error.`) + end + + local actualErrorMessage = tostring(err) + if typeof(with) == "string" then + if not stringext.hasSuffix(actualErrorMessage, with) then + return assertion(`Expected suffix of error message "{with}", but got "{actualErrorMessage}"`) + end + else + return eq(false, err, with, `Expected error "{with}", but got "{formatValue(err)}"`) + end + + return nil +end + +local function strContains( + negate: boolean, + haystack: string, + needle: string, + instances: number?, + msg: string? +): Failure? + if negate then + if instances == nil then + if haystack:find(needle, 1, true) ~= nil then + return assertion(if msg then msg else `Expected "{haystack}" to not contain "{needle}".`, nil, true) + end + return nil + end + + if instances <= 0 then + return assertion("instances must be greater than 0") + end + + if #haystack:split(needle) == instances + 1 then + return assertion( + if msg then msg else `Expected "{haystack}" to not contain "{needle}" {instances} times.`, + nil, + true + ) + end + return nil + end + + if instances == nil then + if haystack:find(needle, 1, true) == nil then + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) + end + return nil + end + + if instances <= 0 then + return assertion("instances must be greater than 0") + end + + if #haystack:split(needle) ~= instances + 1 then + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) + end + + return nil +end + +local function reqassert(req: (boolean, T...) -> Failure?, negate: boolean?): (T...) -> Failure? + return function(...): Failure? + local result: Failure? = req(negate == true, ...) + if result ~= nil then + error(result) + end + return nil + end +end + +-- `never` contains inversions for assertions without an explicit negative helper. +local assertNegation = table.freeze({ + eq = reqassert(eq, true), + throws = reqassert(throws, true), + throwsWith = reqassert(throwsWith, true), + strContains = reqassert(strContains, true), + that = reqassert(that, true), +}) + +return table.freeze({ + eq = reqassert(eq), + neq = assertNegation.eq, + throws = reqassert(throws), + throwsWith = reqassert(throwsWith), + strContains = reqassert(strContains), + strNotContains = assertNegation.strContains, + that = reqassert(that), + never = assertNegation, +}) :: Asserts diff --git a/lute/std/libs/test/env.luau b/lute/std/libs/test/env.luau new file mode 100644 index 000000000..3934713e8 --- /dev/null +++ b/lute/std/libs/test/env.luau @@ -0,0 +1,11 @@ +--!strict +--@std/test/types +-- Environment to store discovered tests + +local types = require("./types") +type TestEnvironment = types.TestEnvironment + +local env: TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, caseindex = {} } + +-- not frozen, the internal test library needs to mutate this during test discovery +return env diff --git a/lute/std/libs/test/failure.luau b/lute/std/libs/test/failure.luau new file mode 100644 index 000000000..87129c543 --- /dev/null +++ b/lute/std/libs/test/failure.luau @@ -0,0 +1,38 @@ +--!strict +-- @std/test/failure +-- utilities for working with test failures +local types = require("./types") + +type Failure = types.Failure + +local failures = {} + +--- Returns a `Failure` describing an assertion failure at the call site, with `msg` as the failure message. +function failures.assertion(msg: string, depth: number?, negation: boolean?): Failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + local stackdepth = depth or 4 + local filename, linenumber = debug.info(stackdepth, "sl") + local name = debug.info(2, "n") + return { + assertion = if negation then `never_{name}` else name, + msg = msg, + filename = filename, + linenumber = linenumber, + __tag = "assertion", + } +end + +--- Returns a `Failure` describing an error that occurred in the `hook` lifecycle callback. +function failures.lifecycle(hook: types.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 + +--- Returns a `Failure` describing an unhandled runtime error, including a stack trace. +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..93b1f38db --- /dev/null +++ b/lute/std/libs/test/init.luau @@ -0,0 +1,101 @@ +--!strict +-- @std/test +-- Standard test library for Luau + +local types = require("@self/types") +local env = require("@self/env") + +type Test = types.Test +type TestCase = types.TestCase +type TestSuite = types.TestSuite +type TestReporter = types.TestReporter + +local TestSuite = {} +TestSuite.__index = TestSuite + +--- Creates a new, empty `TestSuite` with the given `name`. +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 + +--- Registers a test case with `name` in this suite. +function TestSuite:case(name: string, testCase: Test) + table.insert(self.cases, { name = name, case = testCase }) +end + +--- Registers a hook to run before each test case in this suite. +function TestSuite:beforeEach(hook: () -> ()) + self._beforeEach = hook +end + +--- Registers a hook to run once before all test cases in this suite. +function TestSuite:beforeAll(hook: () -> ()) + self._beforeAll = hook +end + +--- Registers a hook to run after each test case in this suite. +function TestSuite:afterEach(hook: () -> ()) + self._afterEach = hook +end + +--- Registers a hook to run once after all test cases in this suite. +function TestSuite:afterAll(hook: () -> ()) + self._afterAll = hook +end + +-- Test library export surface +local test = {} + +--- Registers an anonymous (top-level) test case with the given `name`. +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 + +--- Registers a test suite with `name`, using `registerFn` to define its test cases. +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 + +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..4af51dbdf --- /dev/null +++ b/lute/std/libs/test/reporter.luau @@ -0,0 +1,48 @@ +--!strict +-- @std/test/reporter +-- Standard test reporter library for Luau + +local types = require("./types") + +type FailedTest = types.FailedTest +type TestRunResult = types.TestRunResult + +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 + +--- Prints a simple human-readable summary of `result` to stdout, listing any failures and the pass/fail counts. +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..0bc1a248c --- /dev/null +++ b/lute/std/libs/test/runner.luau @@ -0,0 +1,190 @@ +--!strict +-- @std/test/runner +-- Standard test runner library for Luau +local ps = require("@std/process") +local rt = require("@batteries/richterm") + +local asserts = require("./assert") +local failure = require("./failure") +local reporter = require("./reporter") +local types = require("./types") +local env = require("./env") + +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 fmt = { + debug = function(txt) + print(rt.green(txt)) + end, +} + +local runner = {} + +--- Runs all test cases and suites in `env`, executing lifecycle hooks around each case. Returns the aggregate results. +function runner.run(env: TestEnvironment, runOptions: types.TestRunnerOptions?): 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 afterEachErrorHandler(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 + if runOptions and runOptions.verbose then + fmt.debug(`Running Anonymous.{tc.name}`) + end + + 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 + if runOptions and runOptions.verbose then + fmt.debug(`{suite.name}.{tc.name}`) + end + + 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, afterEachErrorHandler(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 + +--- Runs all globally registered tests, prints the results with the simple reporter, then exits the process with code 0 on success or 1 on failure. +function runner.runRegisteredTests() + local result = runner.run(env) + reporter.simple(result) + if result.failed ~= 0 then + ps.exit(1) + end + ps.exit(0) +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..df14b915d --- /dev/null +++ b/lute/std/libs/test/types.luau @@ -0,0 +1,117 @@ +--!strict +-- @std/test/types +-- Standard test types library for Luau + +--- The name of a lifecycle hook that can run around test cases: +--- - `"beforeEach"`: Runs before each test case in the suite. +--- - `"beforeAll"`: Runs once before all test cases in the suite. +--- - `"afterEach"`: Runs after each test case in the suite. +--- - `"afterAll"`: Runs once after all test cases in the suite. +export type Hook = "beforeEach" | "beforeAll" | "afterEach" | "afterAll" +--- Describes why a test failed: +--- - `"assertion"`: An assertion (e.g. `t.eq`) failed. Includes the assertion name, message, filename, and line number. +--- - `"lifecycle"`: A lifecycle hook threw an error. Includes the hook name, message, filename, and line number. +--- - `"runtime_error"`: The test body threw an unhandled error. Includes the message and stack trace. +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", + } + +--- The assertion functions passed to each test case. Each assertion returns a `Failure` on failure, or `nil` on success. +export type NegatedAsserts = { + eq: (T, T, string?) -> Failure?, + throws: ((T...) -> ...unknown, T...) -> Failure?, + throwsWith: (any, (T...) -> ...unknown, T...) -> Failure?, + strContains: (string, string, number?, string?) -> Failure?, + that: (unknown, string?) -> Failure?, +} + +export type Asserts = { + eq: (T, T, string?) -> Failure?, + neq: (T, T, string?) -> Failure?, + throws: ((T...) -> ...unknown, T...) -> Failure?, + throwsWith: (any, (T...) -> ...unknown, T...) -> Failure?, + strContains: (string, string, number?, string?) -> Failure?, + strNotContains: (string, string, string?) -> Failure?, + that: (unknown, string?) -> Failure?, + never: NegatedAsserts, +} + +--- A test function that receives the assertion helpers and runs one test case. +export type Test = (Asserts) -> () + +--- A named test case pairing a name with its test function. +export type TestCase = { name: string, case: Test } + +--- A named collection of test cases with optional lifecycle hooks. +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, () -> ()) -> (), +} + +-- Additional options for configuring the test runner +export type TestRunnerOptions = { + verbose: boolean, +} + +--- A test case that failed, paired with the failure that caused it. +export type FailedTest = { + test: string, -- name of the test case that failed + failure: Failure, +} + +--- The aggregate results of a test run: total, passed, failed counts and the list of failures. +export type TestRunResult = { + failures: { FailedTest }, + total: number, + failed: number, + passed: number, +} + +--- A function that receives a `TestRunResult` and reports it (e.g. prints to stdout). +export type TestReporter = (TestRunResult) -> () + +-- Testing Environment +--- An index entry mapping a test case name to all anonymous instances and suite instances with that name. +export type CaseIndexEntry = { + anonymous: { TestCase }, + suites: { [string]: { TestCase } }, +} + +--- The full set of discovered tests: anonymous cases, suites, and lookup indexes by name. +export type TestEnvironment = { + anonymous: { TestCase }, + suites: { TestSuite }, + suiteindex: { [string]: TestSuite }, + caseindex: { [string]: CaseIndexEntry }, +} + +--- A function that runs a `TestEnvironment` and returns the results. +export type TestRunner = (TestEnvironment, TestRunnerOptions?) -> TestRunResult + +return {} diff --git a/lute/std/libs/time.luau b/lute/std/libs/time.luau deleted file mode 100644 index bbf1e5a8b..000000000 --- a/lute/std/libs/time.luau +++ /dev/null @@ -1,169 +0,0 @@ -local NANOS_PER_SEC = 1_000_000_000 -local NANOS_PER_MILLI = 1_000_000 -local NANOS_PER_MICRO = 1_000 -local MILLIS_PER_SEC = 1_000 -local MICROS_PER_SEC = 1_000_000 -local SECS_PER_MINUTE = 60 -local MINS_PER_HOUR = 60 -local HOURS_PER_DAY = 24 -local DAYS_PER_WEEK = 7 - -type durationdata = { - seconds: number, - nanoseconds: number, -} - -local duration = {} -duration.__index = duration - -type durationinterface = typeof(duration) -export type duration = setmetatable - -function duration.create(seconds: number, nanoseconds: number): duration - local self = { - seconds = seconds, - nanoseconds = nanoseconds, - } - - return setmetatable(self, duration) -end - -function duration.seconds(seconds: number): duration - return duration.create(seconds, 0) -end - -function duration.milliseconds(milliseconds: number): duration - local seconds = math.floor(milliseconds / MILLIS_PER_SEC) - local nanoseconds = (milliseconds % MILLIS_PER_SEC) * NANOS_PER_MILLI - - return duration.create(seconds, nanoseconds) -end - -function duration.microseconds(microseconds: number): duration - local seconds = math.floor(microseconds / MICROS_PER_SEC) - local nanoseconds = (microseconds % MICROS_PER_SEC) * NANOS_PER_MICRO - - return duration.create(seconds, nanoseconds) -end - -function duration.nanoseconds(nanoseconds: number): duration - local seconds = math.floor(nanoseconds / NANOS_PER_SEC) - local nanosecondsRemaining = nanoseconds % NANOS_PER_SEC - - return duration.create(seconds, nanosecondsRemaining) -end - -function duration.minutes(minutes: number): duration - return duration.create(minutes * SECS_PER_MINUTE, 0) -end - -function duration.hours(hours: number): duration - return duration.create(hours * MINS_PER_HOUR * SECS_PER_MINUTE, 0) -end - -function duration.days(days: number): duration - return duration.create(days * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE, 0) -end - -function duration.weeks(weeks: number): duration - return duration.create(weeks * DAYS_PER_WEEK * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE, 0) -end - -function duration.subsecmillis(self: duration): number - return math.floor(self.nanoseconds / NANOS_PER_MILLI) -end - -function duration.subsecmicros(self: duration): number - return math.floor(self.nanoseconds / NANOS_PER_MICRO) -end - -function duration.subsecnanos(self: duration): number - return self.nanoseconds -end - -function duration.toseconds(self: duration): number - return self.seconds + self.nanoseconds / NANOS_PER_SEC -end - -function duration.tomilliseconds(self: duration): number - return self.seconds * MILLIS_PER_SEC + self:subsecmillis() -end - -function duration.tomicroseconds(self: duration): number - return self.seconds * MICROS_PER_SEC + self:subsecmicros() -end - -function duration.tonanoseconds(self: duration): number - 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 - - if nanoseconds >= NANOS_PER_SEC then - seconds += 1 - nanoseconds -= NANOS_PER_SEC - end - - return duration.create(seconds, nanoseconds) -end - -function duration.__sub(a: duration, b: duration): duration - local seconds = a.seconds - b.seconds - local nanoseconds = a.nanoseconds - b.nanoseconds - - if nanoseconds < 0 then - seconds -= 1 - nanoseconds += NANOS_PER_SEC - end - - return duration.create(seconds, nanoseconds) -end - -function duration.__mul(a: duration, b: number): duration - local seconds = a.seconds * b - local nanoseconds = a.nanoseconds * b - - seconds += math.floor(nanoseconds / NANOS_PER_SEC) - nanoseconds %= NANOS_PER_SEC - - return duration.create(seconds, nanoseconds) -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 - - nanoseconds += (extraSeconds * NANOS_PER_SEC + extraNanos) / b - - return duration.create(seconds, nanoseconds) -end - -function duration.__eq(a: duration, b: duration): boolean - 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 - end - - 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 - end - - return a.seconds <= b.seconds -end - -function duration.__tostring(self: duration): string - return string.format("%d.%09d", self.seconds, self.nanoseconds) -end - -return table.freeze({ - duration = duration, -}) diff --git a/lute/std/libs/time/duration.luau b/lute/std/libs/time/duration.luau new file mode 100644 index 000000000..36a3020ea --- /dev/null +++ b/lute/std/libs/time/duration.luau @@ -0,0 +1,56 @@ +--!strict +-- Submodule of @std/time that provides utility functions for time durations + +local time = require("@lute/time") + +--- A span of time. +export type Duration = time.Duration + +local duration = {} + +--- Creates a `Duration` from a number of `seconds` and an additional number of nanoseconds, `subsecnanos`. +function duration.create(seconds: number, subsecnanos: number): Duration + return time.duration.create(seconds, subsecnanos) +end + +--- Creates a `Duration` from a number of nanoseconds. +function duration.nanoseconds(nanoseconds: number): Duration + return time.duration.nanoseconds(nanoseconds) +end + +--- Creates a `Duration` from a number of microseconds. +function duration.microseconds(microseconds: number): Duration + return time.duration.microseconds(microseconds) +end + +--- Creates a `Duration` from a number of milliseconds. +function duration.milliseconds(milliseconds: number): Duration + return time.duration.milliseconds(milliseconds) +end + +--- Creates a `Duration` from a number of seconds. +function duration.seconds(seconds: number): Duration + return time.duration.seconds(seconds) +end + +--- Creates a `Duration` from a number of minutes. +function duration.minutes(minutes: number): Duration + return time.duration.minutes(minutes) +end + +--- Creates a `Duration` from a number of hours. +function duration.hours(hours: number): Duration + return time.duration.hours(hours) +end + +--- Creates a `Duration` from a number of days. +function duration.days(days: number): Duration + return time.duration.days(days) +end + +--- Creates a `Duration` from a number of weeks. +function duration.weeks(weeks: number): Duration + return time.duration.weeks(weeks) +end + +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..830d90e70 --- /dev/null +++ b/lute/std/libs/time/init.luau @@ -0,0 +1,27 @@ +--!strict +-- @std/time +-- Provides utility functions for time + +local luteTime = require("@lute/time") +local duration = require("@self/duration") + +local time = {} + +--- A span of time. +export type Duration = duration.Duration +--- An opaque point in time, used with `time.since` to measure elapsed time. +export type Instant = luteTime.Instant + +time.duration = duration + +--- Returns the current point in time as an `Instant`. +function time.now(): Instant + return luteTime.now() +end + +--- Returns the number of seconds elapsed since `instant`. +function time.since(instant: Instant): number + return luteTime.since(instant) +end + +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/std/src/stdlib_stub.cpp b/lute/std/src/stdlib_stub.cpp new file mode 100644 index 000000000..3ff1b6acd --- /dev/null +++ b/lute/std/src/stdlib_stub.cpp @@ -0,0 +1,6 @@ +#include "lute/stdlib.h" + +StdLibModuleResult getStdLibModule(std::string_view) +{ + return {StdLibModuleType::NotFound}; +} diff --git a/lute/syntax/CMakeLists.txt b/lute/syntax/CMakeLists.txt new file mode 100644 index 000000000..68ce5cbdf --- /dev/null +++ b/lute/syntax/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(Lute.Syntax STATIC) + +target_sources(Lute.Syntax PRIVATE + + include/lute/syntax.h + + src/parser.cpp + src/syntax.cpp +) + +target_compile_features(Lute.Syntax PUBLIC cxx_std_17) +target_include_directories(Lute.Syntax PUBLIC "include") +target_link_libraries(Lute.Syntax PRIVATE Lute.Common Lute.Runtime Luau.Analysis Luau.Ast Luau.VM) +target_compile_options(Lute.Syntax PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/syntax/include/lute/syntax.h b/lute/syntax/include/lute/syntax.h new file mode 100644 index 000000000..3ab7952aa --- /dev/null +++ b/lute/syntax/include/lute/syntax.h @@ -0,0 +1,40 @@ +#pragma once + +#include "lute/library.h" + +#include "lua.h" +#include "lualib.h" + +namespace luau +{ +static constexpr const char kSpanType[] = "span"; // Uncapitalized because the span library lives under `cst.{kSpanType}` +static constexpr const char kCstPunctuatedType[] = "CstPunctuated"; + +int ltSpan(lua_State* L); +} // namespace luau + +// open the library as a standard global luau library +LUTE_API int luaopen_syntax_parser(lua_State* L); +// open the library as a table on top of the stack +LUTE_API int luteopen_syntax_parser(lua_State* L); + +// open the library as a standard global luau library +LUTE_API int luaopen_syntax(lua_State* L); +// open the library as a table on top of the stack +LUTE_API int luteopen_syntax(lua_State* L); + +struct SyntaxParser : LuteLibrary +{ + static constexpr const char kName[] = "syntax.parser"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; +}; + +struct Syntax : LuteLibrary +{ + static constexpr const char kName[] = "syntax"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; +}; diff --git a/lute/syntax/src/parser.cpp b/lute/syntax/src/parser.cpp new file mode 100644 index 000000000..196dc5ac4 --- /dev/null +++ b/lute/syntax/src/parser.cpp @@ -0,0 +1,3008 @@ +#include "Luau/Parser.h" + +#include "lute/common.h" +#include "lute/syntax.h" + +#include "Luau/Ast.h" +#include "Luau/NotNull.h" +#include "Luau/ToString.h" + +#include +#include +#include + +namespace luau +{ +// Recursively freezes the table at the top of the stack and any descendant tables. +// The table must be at the top of the stack when called. +static void deepFreeze(lua_State* L) +{ + if (!lua_istable(L, -1)) + luaL_error(L, "expected table to freeze"); + + lua_rawcheckstack(L, 2); + + lua_pushnil(L); + while (lua_next(L, -2) != 0) + { + // Freeze the value if it's a table + if (lua_istable(L, -1)) + deepFreeze(L); + + // Pop value, keep key for next iteration + lua_pop(L, 1); + } + + if (lua_getreadonly(L, -1)) + return; + + lua_setreadonly(L, -1, 1); +} + +struct StatResult +{ + std::shared_ptr allocator; + std::shared_ptr names; + + Luau::ParseResult parseResult; +}; + +static StatResult parse(std::string& source) +{ + auto allocator = std::make_shared(); + auto names = std::make_shared(*allocator); + + Luau::ParseOptions options; + options.captureComments = true; + options.allowDeclarationSyntax = false; + options.storeCstData = true; + + auto parseResult = Luau::Parser::parse(source.data(), source.size(), *names, *allocator, options); + + return StatResult{allocator, names, std::move(parseResult)}; +} + +struct ExprResult +{ + std::shared_ptr allocator; + std::shared_ptr names; + + Luau::ParseNodeResult parseResult; +}; + +static ExprResult parseExpr(std::string& source) +{ + auto allocator = std::make_shared(); + auto names = std::make_shared(*allocator); + + Luau::ParseOptions options; + options.captureComments = true; + options.allowDeclarationSyntax = false; + options.storeCstData = true; + + auto parseResult = Luau::Parser::parseExpr(source.data(), source.size(), *names, *allocator, options); + + return ExprResult{allocator, names, std::move(parseResult)}; +} + +static std::vector computeLineOffsets(std::string_view content) +{ + std::vector result{}; + result.emplace_back(0); + + for (size_t i = 0; i < content.size(); i++) + { + auto ch = content[i]; + if (ch == '\r' || ch == '\n') + { + if (ch == '\r' && i + 1 < content.size() && content[i + 1] == '\n') + { + i++; + } + result.push_back(i + 1); + } + } + return result; +} + +static std::vector commentsWithinSpan(const std::vector comments, Luau::Location span) +{ + // TODO: O(n), we could probably binary search if there are a lot of comments + std::vector result; + + for (const auto& comment : comments) + if (span.encloses(comment.location)) + result.emplace_back(comment); + + return result; +} + +struct Trivia +{ + enum class TriviaKind + { + Whitespace, + SingleLineComment, + MultiLineComment, + }; + + TriviaKind kind; + Luau::Location location; + std::string_view text; +}; + +static int iterPunctuatedNext(lua_State* L) +{ + // args: PunctuatedData table, node index + int i = luaL_checkinteger(L, 2) + 1; + + lua_rawcheckstack(L, 2); + + lua_rawgeti(L, 1, i); + + if (lua_isnil(L, -1)) + return 1; // return nil to stop iteration + + lua_pushinteger(L, i); + lua_insert(L, -2); // stack: ..., i + 1, value + + return 2; // index, value +} + +static int iterPunctuated(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + + lua_rawcheckstack(L, 3); + + lua_pushcfunction(L, iterPunctuatedNext, "iterPunctuatedNext"); + lua_pushvalue(L, 1); // state = the punctuated table itself + lua_pushinteger(L, 0); // initial index + return 3; +} + +struct AstSerialize : public Luau::AstVisitor +{ + lua_State* L; + Luau::CstNodeMap cstNodeMap; + std::string_view source; + Luau::Position currentPosition{0, 0}; + std::vector lineOffsets; + std::vector commentLocations; + + // absolute index for the table where we're storing locals + int localTableIndex; + // reference to previously serialized token + int lastTokenRef = LUA_NOREF; + + AstSerialize(lua_State* L, std::string_view source, Luau::CstNodeMap cstNodeMap, std::vector commentLocations) + : L(L) + , cstNodeMap(std::move(cstNodeMap)) + , source(source) + , lineOffsets(computeLineOffsets(source)) + , commentLocations(std::move(commentLocations)) + { + lua_createtable(L, 0, 0); + localTableIndex = lua_absindex(L, -1); + } + + template + Luau::NotNull lookupCstNode(Luau::AstNode* astNode) + { + const auto cstNode = cstNodeMap.find(astNode); + LUTE_ASSERT(cstNode); + return Luau::NotNull{(*cstNode)->as()}; + } + + void advancePosition(std::string_view contents) + { + if (contents.empty()) + return; + + size_t index = 0; + size_t numLines = 0; + while (true) + { + auto newlinePos = contents.find('\n', index); + if (newlinePos == std::string::npos) + break; + numLines++; + index = newlinePos + 1; + } + + currentPosition.line += numLines; + if (numLines > 0) + currentPosition.column = unsigned(contents.size()) - index; + else + currentPosition.column += unsigned(contents.size()); + } + + std::vector extractWhitespace(const Luau::Position& newPos) + { + auto beginPosition = currentPosition; + + LUTE_ASSERT(currentPosition < newPos); + LUTE_ASSERT(currentPosition.line < lineOffsets.size()); + LUTE_ASSERT(newPos.line < lineOffsets.size()); + size_t startOffset = lineOffsets[currentPosition.line] + currentPosition.column; + size_t endOffset = lineOffsets[newPos.line] + newPos.column; + + std::string_view trivia = source.substr(startOffset, endOffset - startOffset); + + // Tokenize whitespace into smaller parts. Whitespace is separated by `\n` characters + std::vector result; + + while (!trivia.empty()) + { + auto index = trivia.find('\n'); + std::string_view part; + if (index == std::string::npos) + part = trivia; + else + { + part = trivia.substr(0, index + 1); + trivia.remove_prefix(index + 1); + } + + advancePosition(part); + result.push_back(Trivia{Trivia::TriviaKind::Whitespace, Luau::Location{beginPosition, currentPosition}, part}); + beginPosition = currentPosition; + + if (index == std::string::npos) + break; + } + LUTE_ASSERT(currentPosition == newPos); + + return result; + } + + std::vector extractTrivia(const Luau::Position& newPos) + { + LUTE_ASSERT(currentPosition <= newPos); + if (currentPosition == newPos) + return {}; + + std::vector result; + + const auto comments = commentsWithinSpan(commentLocations, Luau::Location{currentPosition, newPos}); + for (const auto& comment : comments) + { + if (currentPosition < comment.location.begin) + { + auto whitespace = extractWhitespace(comment.location.begin); + result.insert(result.end(), whitespace.begin(), whitespace.end()); + } + + LUTE_ASSERT(comment.location.begin.line < lineOffsets.size()); + LUTE_ASSERT(comment.location.end.line < lineOffsets.size()); + + size_t startOffset = lineOffsets[comment.location.begin.line] + comment.location.begin.column; + size_t endOffset = lineOffsets[comment.location.end.line] + comment.location.end.column; + + std::string_view commentText = source.substr(startOffset, endOffset - startOffset); + + // TODO: advancePosition is more of a debug check - we can probably just set currentPosition directly here + advancePosition(commentText); + LUTE_ASSERT(currentPosition == comment.location.end); + + // TODO: currently the text includes the `--` / `--[[` characters, should it? + LUTE_ASSERT(comment.type != Luau::Lexeme::BrokenComment); + auto kind = comment.type == Luau::Lexeme::Comment ? Trivia::TriviaKind::SingleLineComment : Trivia::TriviaKind::MultiLineComment; + result.emplace_back(Trivia{kind, comment.location, commentText}); + } + + if (currentPosition < newPos) + { + auto whitespace = extractWhitespace(newPos); + result.insert(result.end(), whitespace.begin(), whitespace.end()); + } + + LUTE_ASSERT(currentPosition == newPos); + + return result; + } + + // Splits a list of trivia into trailing trivia for the previous token, and leading trivia for the next token + // The trailing trivia consists of all trivia up to and including the first '\n' character seen + static std::pair, std::vector> splitTrivia(std::vector trivia) + { + size_t i = 0; + for (i = 0; i < trivia.size(); i++) + { + if (trivia[i].kind == Trivia::TriviaKind::Whitespace && trivia[i].text.find('\n') != std::string::npos) + break; + } + + if (i == trivia.size()) + return {trivia, {}}; + + auto middleIter(trivia.begin()); + std::advance(middleIter, i + 1); + + return {std::vector(trivia.begin(), middleIter), std::vector(middleIter, trivia.end())}; + } + + void serialize(Luau::Location location) + { + lua_rawcheckstack(L, 2); + + lua_createtable(L, 0, 4); + + lua_pushinteger(L, location.begin.line + 1); + lua_setfield(L, -2, "beginLine"); + + lua_pushinteger(L, location.begin.column + 1); + lua_setfield(L, -2, "beginColumn"); + + lua_pushinteger(L, location.end.line + 1); + lua_setfield(L, -2, "endLine"); + + lua_pushinteger(L, location.end.column + 1); + lua_setfield(L, -2, "endColumn"); + + luaL_getmetatable(L, kSpanType); + lua_setmetatable(L, -2); + + lua_setreadonly(L, -1, 1); + } + + void serialize(Luau::AstName& name) + { + lua_rawcheckstack(L, 1); + lua_pushstring(L, name.value); + } + + void serialize(Luau::AstLocal* local, bool createToken = true, std::optional colonPosition = std::nullopt) + { + lua_rawcheckstack(L, 2); + + lua_pushlightuserdata(L, local); + lua_gettable(L, localTableIndex); + + if (lua_isnil(L, -1)) + { + lua_pop(L, 1); + 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); + lua_pushvalue(L, -2); + lua_settable(L, localTableIndex); + + if (createToken) + { + serializeToken(local->location.begin, local->name.value); + lua_setfield(L, -2, "name"); + + if (local->annotation) + { + LUTE_ASSERT(colonPosition); + serializeToken(*colonPosition, ":"); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "colon"); + + if (local->annotation) + local->annotation->visit(this); + else + lua_pushnil(L); + + lua_setfield(L, -2, "annotation"); + } + + if (local->shadow) + serialize(local->shadow); + else + lua_pushnil(L); + lua_setfield(L, -2, "shadows"); + } + } + + void serialize(Luau::AstExprTable::Item& item, Luau::CstExprTable::Item* cstNode) + { + LUTE_ASSERT(cstNode); + + lua_rawcheckstack(L, 2); + + + if (item.kind == Luau::AstExprTable::Item::Kind::List) + { + 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::Kind::Record) + { + 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"); + + LUTE_ASSERT(cstNode->equalsPosition.hasValue()); + serializeToken(cstNode->equalsPosition, "="); + lua_setfield(L, -2, "equals"); + + visit(item.value); + lua_setfield(L, -2, "value"); + } + else if (item.kind == Luau::AstExprTable::Item::Kind::General) + { + lua_createtable(L, 0, 9); + lua_pushstring(L, "general"); + lua_setfield(L, -2, "kind"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "isTableItem"); + + LUTE_ASSERT(cstNode->indexerOpenPosition.hasValue()); + withLocation(Luau::Location{cstNode->indexerOpenPosition, item.value->location.end}); + + serializeToken(cstNode->indexerOpenPosition, "["); + lua_setfield(L, -2, "indexerOpen"); + + visit(item.key); + lua_setfield(L, -2, "key"); + + LUTE_ASSERT(cstNode->indexerClosePosition.hasValue()); + serializeToken(cstNode->indexerClosePosition, "]"); + lua_setfield(L, -2, "indexerClose"); + + LUTE_ASSERT(cstNode->equalsPosition.hasValue()); + serializeToken(cstNode->equalsPosition, "="); + lua_setfield(L, -2, "equals"); + + visit(item.value); + lua_setfield(L, -2, "value"); + } + + if (cstNode->separator != Luau::CstExprTable::Separator::Missing) + serializeToken(cstNode->separatorPosition, cstNode->separator == Luau::CstExprTable::Separator::Comma ? "," : ";"); + else + lua_pushnil(L); + lua_setfield(L, -2, "separator"); + } + + void withLocation(Luau::Location location) + { + serialize(location); + lua_setfield(L, -2, "location"); + } + + void serialize(Luau::AstExprBinary::Op& op) + { + if (op == Luau::AstExprBinary::Op::Op__Count) + luaL_error(L, "encountered illegal operator: Op__Count"); + + lua_pushstring(L, Luau::toString(op).data()); + } + + // preambleSize should encode the size of the fields we're setting up for _all_ nodes. It consists of tag, kind, and location. + 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); + } + + void serializeTrivia(const std::vector& trivia) + { + lua_rawcheckstack(L, 3); + lua_createtable(L, trivia.size(), 0); + + for (size_t i = 0; i < trivia.size(); i++) + { + lua_createtable(L, 0, 3); + + switch (trivia[i].kind) + { + case Trivia::TriviaKind::Whitespace: + lua_pushstring(L, "whitespace"); + break; + case Trivia::TriviaKind::SingleLineComment: + lua_pushstring(L, "comment"); + break; + case Trivia::TriviaKind::MultiLineComment: + lua_pushstring(L, "blockcomment"); + break; + } + lua_setfield(L, -2, "tag"); + + serialize(trivia[i].location); + lua_setfield(L, -2, "location"); + + lua_pushlstring(L, trivia[i].text.data(), trivia[i].text.size()); + lua_setfield(L, -2, "text"); + + lua_rawseti(L, -2, i + 1); + } + } + + // For correct trivia computation, everything must end up going through serializeToken + void serializeToken(Luau::Position position, const char* text) + { + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, 5); + + const auto trivia = extractTrivia(position); + if (lastTokenRef != LUA_NOREF) + { + const auto [trailingTrivia, leadingTrivia] = splitTrivia(trivia); + + lua_getref(L, lastTokenRef); + LUTE_ASSERT(lua_istable(L, -1)); + + serializeTrivia(trailingTrivia); + lua_setfield(L, -2, "trailingTrivia"); + lua_pop(L, 1); + lua_unref(L, lastTokenRef); + lastTokenRef = LUA_NOREF; + + serializeTrivia(leadingTrivia); + } + else + { + serializeTrivia(trivia); + } + LUTE_ASSERT(lua_istable(L, -2)); + lua_setfield(L, -2, "leadingTrivia"); + + size_t textLength = strlen(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_pushstring(L, "token"); + lua_setfield(L, -2, "kind"); + + lastTokenRef = lua_ref(L, -1); + } + + void serializeExprs(Luau::AstArray& exprs, size_t nrec = 0) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, exprs.size, nrec); + + for (size_t i = 0; i < exprs.size; i++) + { + exprs.data[i]->visit(this); + lua_rawseti(L, -2, i + 1); + } + } + + void serializeStats(Luau::AstArray& stats, size_t nrec = 0) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, stats.size, nrec); + + for (size_t i = 0; i < stats.size; i++) + { + stats.data[i]->visit(this); + lua_rawseti(L, -2, i + 1); + } + } + + void serializeAttributes(Luau::AstArray& attrs, size_t nrec = 0) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, attrs.size, nrec); + + for (size_t i = 0; i < attrs.size; i++) + { + serializeAttribute(attrs.data[i]); + lua_rawseti(L, -2, i + 1); + } + } + + template + void serializePunctuated(Luau::AstArray nodes, Luau::AstArray separators, const char* separatorText) + { + lua_rawcheckstack(L, 3); + + lua_createtable(L, nodes.size, 1); + + lua_createtable(L, separators.size, 0); + + for (size_t i = 0; i < nodes.size; i++) + { + nodes.data[i]->visit(this); + lua_rawseti(L, -3, i + 1); + + if (i < separators.size) + { + serializeToken(separators.data[i], separatorText); + lua_rawseti(L, -2, i + 1); + } + } + + lua_setfield(L, -2, "separators"); + + luaL_getmetatable(L, kCstPunctuatedType); + lua_setmetatable(L, -2); + } + + void serializePunctuated(Luau::AstArray nodes, Luau::AstArray separators, const char* separatorText) + { + lua_rawcheckstack(L, 3); + + lua_createtable(L, nodes.size, 1); + + lua_createtable(L, separators.size, 0); + + for (size_t i = 0; i < nodes.size; i++) + { + const Luau::AstTypeOrPack& node = nodes.data[i]; + if (node.type) + node.type->visit(this); + else + node.typePack->visit(this); + lua_rawseti(L, -3, i + 1); + + if (i < separators.size) + { + serializeToken(separators.data[i], separatorText); + lua_rawseti(L, -2, i + 1); + } + } + + lua_setfield(L, -2, "separators"); + + luaL_getmetatable(L, kCstPunctuatedType); + lua_setmetatable(L, -2); + } + + void serializePunctuated( + Luau::AstArray nodes, + Luau::AstArray separators, + const char* separatorText, + Luau::AstArray colonPositions + ) + { + lua_rawcheckstack(L, 3); + + lua_createtable(L, nodes.size, 1); + + lua_createtable(L, separators.size, 0); + + for (size_t i = 0; i < nodes.size; i++) + { + serialize(nodes.data[i], /* createToken */ true, colonPositions.size > i ? std::make_optional(colonPositions.data[i]) : std::nullopt); + lua_rawseti(L, -3, i + 1); + + if (i < separators.size) + { + serializeToken(separators.data[i], separatorText); + lua_rawseti(L, -2, i + 1); + } + } + + lua_setfield(L, -2, "separators"); + + luaL_getmetatable(L, kCstPunctuatedType); + lua_setmetatable(L, -2); + } + + void serializeAttribute(Luau::AstAttr* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize); // location, kind, token + + serializeToken(node->location.begin, ("@" + std::string(node->name.value)).c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, "attribute"); + lua_setfield(L, -2, "kind"); + + withLocation(node->location); + } + + void serializeEof(Luau::Position eofPosition) + { + serializeToken(eofPosition, ""); + + lua_pushstring(L, "eof"); + lua_setfield(L, -2, "tag"); + } + + void serialize(Luau::AstExprGroup* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "group", "expr"); + + serializeToken(node->location.begin, "("); + lua_setfield(L, -2, "openParens"); + + node->expr->visit(this); + lua_setfield(L, -2, "expression"); + + const Luau::NotNull cstNode = lookupCstNode(node); + + serializeToken(cstNode->closePosition, ")"); + lua_setfield(L, -2, "closeParens"); + } + + void serialize(Luau::AstExprConstantNil* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "nil", "expr"); + + serializeToken(node->location.begin, "nil"); + lua_setfield(L, -2, "token"); + } + + void serialize(Luau::AstExprConstantBool* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "boolean", "expr"); + + lua_pushboolean(L, node->value); + lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, node->value ? "true" : "false"); + lua_setfield(L, -2, "token"); + } + + void serialize(Luau::AstExprConstantNumber* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + const auto cstNode = lookupCstNode(node); + + serializeNodePreamble(node, "number", "expr"); + + lua_pushnumber(L, node->value); + lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, cstNode->value.data); + lua_setfield(L, -2, "token"); + } + + void serialize(Luau::AstExprConstantInteger* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + const auto cstNode = lookupCstNode(node); + + serializeNodePreamble(node, "integer", "expr"); + + lua_pushnumber(L, node->value); + lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, cstNode->value.data); + lua_setfield(L, -2, "token"); + } + + void serialize(Luau::AstExprConstantString* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + const auto cstNode = lookupCstNode(node); + serializeNodePreamble(node, "string", "expr"); + + switch (cstNode->quoteStyle) + { + case Luau::CstExprConstantString::QuoteStyle::QuotedSingle: + lua_pushstring(L, "single"); + break; + case Luau::CstExprConstantString::QuoteStyle::QuotedDouble: + lua_pushstring(L, "double"); + break; + case Luau::CstExprConstantString::QuoteStyle::QuotedRaw: + lua_pushstring(L, "block"); + break; + case Luau::CstExprConstantString::QuoteStyle::QuotedInterp: + lua_pushstring(L, "interp"); + break; + } + lua_setfield(L, -2, "quoteStyle"); + + lua_pushnumber(L, cstNode->blockDepth); + lua_setfield(L, -2, "blockDepth"); + + serializeToken(node->location.begin, cstNode->sourceString.data); + lua_setfield(L, -2, "value"); + + // Unlike normal tokens, string content contains quotation marks that were not included during advancement + // For simplicity, lets set the current position manually + LUTE_ASSERT(currentPosition <= node->location.end); + currentPosition = node->location.end; + } + + void serialize(Luau::AstExprLocal* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "local", "expr"); + + serializeToken(node->location.begin, node->local->name.value); + lua_setfield(L, -2, "token"), + + serialize(node->local); + lua_setfield(L, -2, "local"); + + lua_pushboolean(L, node->upvalue); + lua_setfield(L, -2, "upvalue"); + } + + void serialize(Luau::AstExprGlobal* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "global", "expr"); + + serializeToken(node->location.begin, node->name.value); + lua_setfield(L, -2, "name"); + } + + void serialize(Luau::AstExprVarargs* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "vararg", "expr"); + + serializeToken(node->location.begin, "..."); + lua_setfield(L, -2, "token"); + } + + void serialize(Luau::AstExprCall* node) + { + const auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 6); + + serializeNodePreamble(node, "call", "expr"); + + node->func->visit(this); + lua_setfield(L, -2, "func"); + + if (cstNode->openParens.hasValue()) + serializeToken(cstNode->openParens, "("); + else + lua_pushnil(L); + lua_setfield(L, -2, "openParens"); + + serializePunctuated(node->args, cstNode->commaPositions, ","); + lua_setfield(L, -2, "arguments"); + + lua_pushboolean(L, node->self); + lua_setfield(L, -2, "self"); + + serialize(node->argLocation); + lua_setfield(L, -2, "argLocation"); + + if (cstNode->closeParens.hasValue()) + serializeToken(cstNode->closeParens, ")"); + else + lua_pushnil(L); + lua_setfield(L, -2, "closeParens"); + } + + void serialize(Luau::AstExprInstantiate* node) + { + const auto& cstNode = lookupCstNode(node)->instantiation; + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 6); + + serializeNodePreamble(node, "instantiate", "expr"); + + node->expr->visit(this); + lua_setfield(L, -2, "expr"); + + serializeToken(cstNode.leftArrow1Position, "<"); + lua_setfield(L, -2, "leftArrow1"); + + serializeToken(cstNode.leftArrow2Position, "<"); + lua_setfield(L, -2, "leftArrow2"); + + serializePunctuated(node->typeArguments, cstNode.commaPositions, ","); + lua_setfield(L, -2, "typeArguments"); + + serializeToken(cstNode.rightArrow1Position, ">"); + lua_setfield(L, -2, "rightArrow1"); + + serializeToken(cstNode.rightArrow2Position, ">"); + lua_setfield(L, -2, "rightArrow2"); + } + + void serialize(Luau::AstExprIndexName* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "indexname", "expr"); + + node->expr->visit(this); + lua_setfield(L, -2, "expression"); + + serializeToken(node->opPosition, std::string(1, node->op).data()); + lua_setfield(L, -2, "accessor"); + + serializeToken(node->indexLocation.begin, node->index.value); + lua_setfield(L, -2, "index"); + serialize(node->indexLocation); + lua_setfield(L, -2, "indexLocation"); + } + + void serialize(Luau::AstExprIndexExpr* node) + { + const auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "index", "expr"); + + node->expr->visit(this); + lua_setfield(L, -2, "expression"); + + serializeToken(cstNode->openBracketPosition, "["); + lua_setfield(L, -2, "openBrackets"); + + node->index->visit(this); + lua_setfield(L, -2, "index"); + + serializeToken(cstNode->closeBracketPosition, "]"); + lua_setfield(L, -2, "closeBrackets"); + } + + enum FunctionSerializationState + { + AttributesAndFunctionKeywordSerialized, + FunctionKeywordSerialized, + NothingSerialized + }; + + // Serialization needs to happen in program-lexical order, so attributes and function keyword might have been serialized already + void serialize(Luau::AstExprFunction* node, FunctionSerializationState state = NothingSerialized) + { + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 18); + + serializeNodePreamble(node, "function", "expr"); + + const auto cstNode = lookupCstNode(node); + + switch (state) + { + case AttributesAndFunctionKeywordSerialized: + // Attributes and function keyword are on the stack, so we just need to set them in the function expr table + // This is only called from serialize AstStatFunction or AstStatLocalFunction, so we assume the stack has the following structure: + // -3: attributes + // -2: functionKeyword + // -1: function expr table (created above) + lua_insert(L, -3); + lua_setfield(L, -3, "functionKeyword"); + lua_setfield(L, -2, "attributes"); + break; + case FunctionKeywordSerialized: + // Function keyword is on the stack, so we just need to set it in the function expr table + // This is only called from serialize AstStatTypeFunction, so we assume the stack has the following structure: + // -2: functionKeyword + // -1: function expr table (created above) + lua_insert(L, -2); + lua_setfield(L, -2, "functionKeyword"); + + lua_newtable(L); + lua_setfield(L, -2, "attributes"); + break; + case NothingSerialized: + serializeAttributes(node->attributes); + lua_setfield(L, -2, "attributes"); + + serializeToken(cstNode->functionKeywordPosition, "function"); + lua_setfield(L, -2, "functionKeyword"); + break; + } + + if (node->generics.size > 0 || node->genericPacks.size > 0) + { + serializeToken(cstNode->openGenericsPosition, "<"); + 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"); + + serializeToken(cstNode->closeGenericsPosition, ">"); + lua_setfield(L, -2, "closeGenerics"); + } + else + { + lua_createtable(L, 0, 0); + lua_setfield(L, -2, "generics"); + + lua_createtable(L, 0, 0); + lua_setfield(L, -2, "genericPacks"); + } + + if (node->self) + serialize(node->self, /* createToken= */ false); + else + lua_pushnil(L); + lua_setfield(L, -2, "self"); + + if (node->argLocation) + { + serializeToken(node->argLocation->begin, "("); + lua_setfield(L, -2, "openParens"); + } + + serializePunctuated(node->args, cstNode->argsCommaPositions, ",", cstNode->argsAnnotationColonPositions); + lua_setfield(L, -2, "parameters"); + + if (node->vararg) + { + lua_pushboolean(L, 1); + lua_setfield(L, -2, "hasVararg"); + + serializeToken(node->varargLocation.begin, "..."); + lua_setfield(L, -2, "vararg"); + } + else + { + lua_pushboolean(L, 0); + lua_setfield(L, -2, "hasVararg"); + + lua_pushnil(L); + lua_setfield(L, -2, "vararg"); + } + + if (node->varargAnnotation) + serializeToken(cstNode->varargAnnotationColonPosition, ":"); + else + lua_pushnil(L); + lua_setfield(L, -2, "varargColon"); + + if (node->varargAnnotation) + { + if (auto variadic = node->varargAnnotation->as()) + serializeTypePack(variadic, true); + else + node->varargAnnotation->visit(this); + } + else + lua_pushnil(L); + 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"); + } + + if (node->returnAnnotation) + serializeToken(cstNode->returnSpecifierPosition, ":"); + else + lua_pushnil(L); + lua_setfield(L, -2, "returnSpecifier"); + + if (node->returnAnnotation) + node->returnAnnotation->visit(this); + else + lua_pushnil(L); + 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"); + } + + void serialize(Luau::AstExprTable* node) + { + const auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "table", "expr"); + + serializeToken(node->location.begin, "{"); + lua_setfield(L, -2, "openBrace"); + + lua_createtable(L, node->items.size, 0); + for (size_t i = 0; i < node->items.size; i++) + { + serialize(node->items.data[i], &cstNode->items.data[i]); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "entries"); + + serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); + lua_setfield(L, -2, "closeBrace"); + } + + void serialize(Luau::AstExprUnary* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "unary", "expr"); + + const auto cstNode = lookupCstNode(node); + serializeToken(cstNode->opPosition, toString(node->op).data()); + lua_setfield(L, -2, "operator"); + + node->expr->visit(this); + lua_setfield(L, -2, "operand"); + } + + void serialize(Luau::AstExprBinary* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "binary", "expr"); + + node->left->visit(this); + lua_setfield(L, -2, "lhsOperand"); + + const auto cstNode = lookupCstNode(node); + serializeToken(cstNode->opPosition, Luau::toString(node->op).data()); + lua_setfield(L, -2, "operator"); + + node->right->visit(this); + lua_setfield(L, -2, "rhsOperand"); + } + + void serialize(Luau::AstExprTypeAssertion* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "cast", "expr"); + + node->expr->visit(this); + lua_setfield(L, -2, "operand"); + + const auto cstNode = lookupCstNode(node); + serializeToken(cstNode->opPosition, "::"); + lua_setfield(L, -2, "operator"); + + node->annotation->visit(this); + lua_setfield(L, -2, "annotation"); + } + + void serialize(Luau::AstExprIfElse* node) + { + auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 7); + + serializeNodePreamble(node, "conditional", "expr"); + + serializeToken(node->location.begin, "if"); + lua_setfield(L, -2, "ifKeyword"); + + node->condition->visit(this); + lua_setfield(L, -2, "condition"); + + if (node->hasThen) + { + serializeToken(cstNode->thenPosition, "then"); + lua_setfield(L, -2, "thenKeyword"); + + node->trueExpr->visit(this); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "thenExpr"); + + lua_createtable(L, 0, preambleSize + 4); + int i = 0; + while (node->hasElse && node->falseExpr->is() && cstNode->isElseIf) + { + lua_createtable(L, 0, 4); + + node = node->falseExpr->as(); + cstNode = lookupCstNode(node); + + serializeToken(node->location.begin, "elseif"); + lua_setfield(L, -2, "elseIfKeyword"); + + node->condition->visit(this); + lua_setfield(L, -2, "condition"); + + if (node->hasThen) + { + serializeToken(cstNode->thenPosition, "then"); + lua_setfield(L, -2, "thenKeyword"); + node->trueExpr->visit(this); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "thenExpr"); + + lua_rawseti(L, -2, i + 1); + i++; + } + lua_setfield(L, -2, "elseifs"); + + if (node->hasElse) + { + serializeToken(cstNode->elsePosition, "else"); + lua_setfield(L, -2, "elseKeyword"); + node->falseExpr->visit(this); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "elseExpr"); + } + + void serialize(Luau::AstExprInterpString* node) + { + const auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "interpolatedstring", "expr"); + + lua_createtable(L, node->strings.size, 0); + lua_createtable(L, node->expressions.size, 0); + + for (size_t i = 0; i < node->strings.size; i++) + { + auto position = i > 0 ? cstNode->stringPositions.data[i] : node->location.begin; + serializeToken(position, std::string(cstNode->sourceStrings.data[i].data, cstNode->sourceStrings.data[i].size).data()); + lua_rawseti(L, -3, i + 1); + + // Unlike normal tokens, interpolated string parts contain extra characters (`, } or {) that were not included during advancement + // For simplicity, lets set the current position manually. We don't have an end position for these parts, so we must compute + // If string part was single line, end position = current position + 2 (start and end character) + // If string parts was multi line, end position = current position + 1 (just end character) + if (position.line == currentPosition.line) + currentPosition.column += 2; + else + currentPosition.column += 1; + + if (i < node->expressions.size) + { + node->expressions.data[i]->visit(this); + lua_rawseti(L, -2, i + 1); + } + } + lua_setfield(L, -3, "expressions"); + lua_setfield(L, -2, "strings"); + } + + void serialize(Luau::AstExprError* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "error", "expr"); + + serializeExprs(node->expressions); + lua_setfield(L, -2, "expressions"); + + // TODO: messageIndex reference + } + + void serializeStat(Luau::AstStatBlock* node) + { + const auto cstNode = cstNodeMap.find(node); + const Luau::CstStatDo* cstDo = cstNode ? (*cstNode)->as() : nullptr; + + if (cstDo) + { + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "do", "stat"); + + serializeToken(node->location.begin, "do"); + lua_setfield(L, -2, "doKeyword"); + + // In lieu of a C++ AstStatBlock object to recurse on, manually construct a Luau AstStatBlock table + lua_createtable(L, 0, preambleSize + 1); + + lua_pushstring(L, "block"); + lua_setfield(L, -2, "tag"); + + lua_pushstring(L, "stat"); + lua_setfield(L, -2, "kind"); + + Luau::Location blockLocation; + if (node->body.size > 0) + { + blockLocation.begin = node->body.data[0]->location.begin; + blockLocation.end = node->body.data[node->body.size - 1]->location.end; + } + else + blockLocation = node->location; + + withLocation(blockLocation); + + serializeStats(node->body); + lua_setfield(L, -2, "statements"); + + 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) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 8); + + serializeNodePreamble(node, "conditional", "stat"); + + serializeToken(node->location.begin, "if"); + lua_setfield(L, -2, "ifKeyword"); + + node->condition->visit(this); + lua_setfield(L, -2, "condition"); + + serializeToken(node->thenLocation->begin, "then"); + lua_setfield(L, -2, "thenKeyword"); + + node->thenbody->visit(this); + lua_setfield(L, -2, "thenBlock"); + + lua_createtable(L, 0, 0); + int i = 0; + while (node->elsebody && node->elsebody->is()) + { + lua_createtable(L, 0, 4); + + auto elseif = node->elsebody->as(); + serializeToken(elseif->location.begin, "elseif"); + lua_setfield(L, -2, "elseIfKeyword"); + + elseif->condition->visit(this); + lua_setfield(L, -2, "condition"); + + serializeToken(elseif->thenLocation->begin, "then"); + lua_setfield(L, -2, "thenKeyword"); + + elseif->thenbody->visit(this); + lua_setfield(L, -2, "thenBlock"); + + lua_rawseti(L, -2, i + 1); + node = elseif; + i++; + } + lua_setfield(L, -2, "elseifs"); + + if (node->elsebody) + { + LUTE_ASSERT(node->elseLocation); + serializeToken(node->elseLocation->begin, "else"); + lua_setfield(L, -2, "elseKeyword"); + + node->elsebody->visit(this); + lua_setfield(L, -2, "elseBlock"); + + serializeToken(node->elsebody->location.end, "end"); + lua_setfield(L, -2, "endKeyword"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "elseKeyword"); + + lua_pushnil(L); + lua_setfield(L, -2, "elseBlock"); + + serializeToken(node->thenbody->location.end, "end"); + lua_setfield(L, -2, "endKeyword"); + } + } + + void serializeStat(Luau::AstStatWhile* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 5); + + serializeNodePreamble(node, "while", "stat"); + + serializeToken(node->location.begin, "while"); + lua_setfield(L, -2, "whileKeyword"); + + node->condition->visit(this); + lua_setfield(L, -2, "condition"); + + if (node->hasDo) + serializeToken(node->doLocation.begin, "do"); + else + lua_pushnil(L); + 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"); + } + + void serializeStat(Luau::AstStatRepeat* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "repeat", "stat"); + + serializeToken(node->location.begin, "repeat"); + 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"); + + node->condition->visit(this); + lua_setfield(L, -2, "condition"); + } + + void serializeStat(Luau::AstStatBreak* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "break", "stat"); + + serializeToken(node->location.begin, "break"); + lua_setfield(L, -2, "token"); + } + + void serializeStat(Luau::AstStatContinue* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "continue", "stat"); + + serializeToken(node->location.begin, "continue"); + lua_setfield(L, -2, "token"); + } + + void serializeStat(Luau::AstStatReturn* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "return", "stat"); + + serializeToken(node->location.begin, "return"); + lua_setfield(L, -2, "returnKeyword"); + + const auto cstNode = lookupCstNode(node); + serializePunctuated(node->list, cstNode->commaPositions, ","); + lua_setfield(L, -2, "expressions"); + } + + void serializeStat(Luau::AstStatExpr* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "expression", "stat"); + + node->expr->visit(this); + lua_setfield(L, -2, "expression"); + } + + void serializeStat(Luau::AstStatLocal* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + const char* keyword = node->isConst ? "const" : "local"; + const char* keywordField = node->isConst ? "constKeyword" : "localKeyword"; + + serializeNodePreamble(node, keyword, "stat"); + + serializeToken(node->location.begin, keyword); + lua_setfield(L, -2, keywordField); + + const auto cstNode = lookupCstNode(node); + serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); + lua_setfield(L, -2, "variables"); + + if (node->equalsSignLocation) + serializeToken(node->equalsSignLocation->begin, "="); + else + lua_pushnil(L); + lua_setfield(L, -2, "equals"); + + serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); + lua_setfield(L, -2, "values"); + } + + void serializeStat(Luau::AstStatFor* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 11); + + const auto cstNode = lookupCstNode(node); + + serializeNodePreamble(node, "for", "stat"); + + serializeToken(node->location.begin, "for"); + lua_setfield(L, -2, "forKeyword"); + + serialize(node->var, /* createToken= */ true, std::make_optional(cstNode->annotationColonPosition)); + lua_setfield(L, -2, "variable"); + + serializeToken(cstNode->equalsPosition, "="); + lua_setfield(L, -2, "equals"); + + node->from->visit(this); + lua_setfield(L, -2, "from"); + + serializeToken(cstNode->endCommaPosition, ","); + lua_setfield(L, -2, "toComma"); + + node->to->visit(this); + lua_setfield(L, -2, "to"); + + if (cstNode->stepCommaPosition.hasValue()) + { + serializeToken(cstNode->stepCommaPosition, ","); + lua_setfield(L, -2, "stepComma"); + } + + if (node->step) + node->step->visit(this); + else + lua_pushnil(L); + lua_setfield(L, -2, "step"); + + if (node->hasDo) + serializeToken(node->doLocation.begin, "do"); + else + lua_pushnil(L); + 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"); + } + + void serializeStat(Luau::AstStatForIn* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 7); + + const auto cstNode = lookupCstNode(node); + + serializeNodePreamble(node, "forin", "stat"); + + serializeToken(node->location.begin, "for"); + lua_setfield(L, -2, "forKeyword"); + + serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); + lua_setfield(L, -2, "variables"); + + if (node->hasIn) + serializeToken(node->inLocation.begin, "in"); + else + lua_pushnil(L); + lua_setfield(L, -2, "inKeyword"); + + serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); + lua_setfield(L, -2, "values"); + + if (node->hasDo) + serializeToken(node->doLocation.begin, "do"); + else + lua_pushnil(L); + 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"); + } + + void serializeStat(Luau::AstStatAssign* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + const auto cstNode = lookupCstNode(node); + + serializeNodePreamble(node, "assign", "stat"); + + serializePunctuated(node->vars, cstNode->varsCommaPositions, ","); + lua_setfield(L, -2, "variables"); + + serializeToken(cstNode->equalsPosition, "="); + lua_setfield(L, -2, "equals"); + + serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); + lua_setfield(L, -2, "values"); + } + + void serializeStat(Luau::AstStatCompoundAssign* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "compoundassign", "stat"); + + node->var->visit(this); + lua_setfield(L, -2, "variable"); + + const auto cstNode = lookupCstNode(node); + serializeToken(cstNode->opPosition, (Luau::toString(node->op) + "=").data()); + lua_setfield(L, -2, "operator"); + + node->value->visit(this); + lua_setfield(L, -2, "value"); + } + + void serializeStat(Luau::AstStatFunction* node) + { + lua_rawcheckstack(L, 4); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "function", "stat"); + + const auto cstNode = lookupCstNode(node); + + // Serialization needs to happen in program-lexical order, so we serialize attributes and function keyword first + serializeAttributes(node->func->attributes); + + serializeToken(cstNode->functionKeywordPosition, "function"); + + node->name->visit(this); + lua_setfield(L, -4, "name"); + + serialize(node->func, AttributesAndFunctionKeywordSerialized); + lua_setfield(L, -2, "func"); + } + + void serializeStat(Luau::AstStatLocalFunction* node) + { + lua_rawcheckstack(L, 4); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "localfunction", "stat"); + + // Serialization needs to happen in program-lexical order, so we serialize attributes and function keyword before the func expr + serializeAttributes(node->func->attributes); + + const auto cstNode = lookupCstNode(node); + + serializeToken(cstNode->localKeywordPosition, "local"); + lua_setfield(L, -3, "localKeyword"); + + serializeToken(cstNode->functionKeywordPosition, "function"); + + serialize(node->name); + lua_setfield(L, -4, "name"); + + serialize(node->func, AttributesAndFunctionKeywordSerialized); + lua_setfield(L, -2, "func"); + } + + static Luau::AstArray splitArray(Luau::AstArray arr, size_t index) + { + if (arr.size < index) + return arr; + return {arr.data + index, arr.size - index}; + } + + void serializeStat(Luau::AstStatTypeAlias* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 10); + + serializeNodePreamble(node, "typealias", "stat"); + + const auto cstNode = lookupCstNode(node); + + if (node->exported) + { + lua_pushboolean(L, 1); + lua_setfield(L, -2, "isExported"); + + serializeToken(node->location.begin, "export"); + lua_setfield(L, -2, "export"); + } + else + { + lua_pushboolean(L, 0); + lua_setfield(L, -2, "isExported"); + + lua_pushnil(L); + lua_setfield(L, -2, "export"); + } + + serializeToken(cstNode->typeKeywordPosition, "type"); + lua_setfield(L, -2, "typeToken"); + + serializeToken(node->nameLocation.begin, node->name.value); + lua_setfield(L, -2, "name"); + + if (node->generics.size > 0 || node->genericPacks.size > 0) + { + serializeToken(cstNode->genericsOpenPosition, "<"); + 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"); + + serializeToken(cstNode->genericsClosePosition, ">"); + lua_setfield(L, -2, "closeGenerics"); + } + + serializeToken(cstNode->equalsPosition, "="); + lua_setfield(L, -2, "equals"); + + node->type->visit(this); + lua_setfield(L, -2, "type"); + } + + void serializeStat(Luau::AstStatTypeFunction* node) + { + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 5); + + const auto cstNode = lookupCstNode(node); + + serializeNodePreamble(node, "typefunction", "stat"); + + if (node->exported) + { + lua_pushboolean(L, 1); + lua_setfield(L, -2, "isExported"); + + serializeToken(node->location.begin, "export"); + lua_setfield(L, -2, "export"); + } + else + { + lua_pushboolean(L, 0); + lua_setfield(L, -2, "isExported"); + + lua_pushnil(L); + lua_setfield(L, -2, "export"); + } + + serializeToken(cstNode->typeKeywordPosition, "type"); + lua_setfield(L, -2, "type"); + + serializeToken(cstNode->functionKeywordPosition, "function"); + + serializeToken(node->nameLocation.begin, node->name.value); + lua_setfield(L, -3, "name"); + + serialize(node->body, FunctionKeywordSerialized); + lua_setfield(L, -2, "body"); + } + + void serializeStat(Luau::AstStatDeclareFunction* node) + { + // TODO: declarations + } + + void serializeStat(Luau::AstStatDeclareGlobal* node) + { + // TODO: declarations + } + + void serializeStat(Luau::AstStatDeclareExternType* node) + { + // TODO: declarations + } + + void serializeStat(Luau::AstStatError* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "error", "stat"); + + serializeExprs(node->expressions); + lua_setfield(L, -2, "expressions"); + + serializeStats(node->statements); + lua_setfield(L, -2, "statements"); + + // TODO: messageIndex reference + } + + void serializeType(Luau::AstTypeReference* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 6); + + serializeNodePreamble(node, "reference", "type"); + + const auto cstNode = node->prefix || node->hasParameterList ? lookupCstNode(node).get() : nullptr; + + if (node->prefix) + { + LUTE_ASSERT(node->prefixLocation); + serializeToken(node->prefixLocation->begin, node->prefix->value); + lua_setfield(L, -2, "prefix"); + + LUTE_ASSERT(cstNode); + LUTE_ASSERT(cstNode->prefixPointPosition.hasValue()); + serializeToken(cstNode->prefixPointPosition, "."); + lua_setfield(L, -2, "prefixPoint"); + } + + serializeToken(node->nameLocation.begin, node->name.value); + lua_setfield(L, -2, "name"); + + if (node->hasParameterList) + { + LUTE_ASSERT(cstNode); + serializeToken(cstNode->openParametersPosition, "<"); + lua_setfield(L, -2, "openParameters"); + + serializePunctuated(node->parameters, cstNode->parametersCommaPositions, ","); + lua_setfield(L, -2, "parameters"); + + serializeToken(cstNode->closeParametersPosition, ">"); + lua_setfield(L, -2, "closeParameters"); + } + } + + void serializeType(Luau::AstTypeTable* node) + { + const auto cstNode = lookupCstNode(node); + + if (cstNode->isArray) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "array", "type"); + + serializeToken(node->location.begin, "{"); + lua_setfield(L, -2, "openBrace"); + + if (node->indexer->accessLocation) + { + LUTE_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); + serializeToken(node->indexer->accessLocation->begin, node->indexer->access == Luau::AstTableAccess::Read ? "read" : "write"); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "access"); + + node->indexer->resultType->visit(this); + lua_setfield(L, -2, "type"); + + serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); + lua_setfield(L, -2, "closeBrace"); + + return; + } + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "table", "type"); + + serializeToken(node->location.begin, "{"); + lua_setfield(L, -2, "openBrace"); + + lua_createtable(L, cstNode->items.size, 0); + const Luau::AstTableProp* prop = node->props.begin(); + for (size_t i = 0; i < cstNode->items.size; i++) + { + lua_rawcheckstack(L, 2); + + Luau::CstTypeTable::Item item = cstNode->items.data[i]; + + if (item.kind == Luau::CstTypeTable::Item::Kind::Indexer) + { + LUTE_ASSERT(node->indexer); + + lua_createtable(L, 0, 8); + + lua_pushstring(L, "indexer"); + lua_setfield(L, -2, "kind"); + + if (node->indexer->accessLocation) + { + LUTE_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); + serializeToken(node->indexer->accessLocation->begin, node->indexer->access == Luau::AstTableAccess::Read ? "read" : "write"); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "access"); + + serializeToken(item.indexerOpenPosition, "["); + lua_setfield(L, -2, "indexerOpen"); + + node->indexer->indexType->visit(this); + lua_setfield(L, -2, "key"); + + serializeToken(item.indexerClosePosition, "]"); + lua_setfield(L, -2, "indexerClose"); + + serializeToken(item.colonPosition, ":"); + lua_setfield(L, -2, "colon"); + + node->indexer->resultType->visit(this); + lua_setfield(L, -2, "value"); + + if (item.separator != Luau::CstExprTable::Separator::Missing) + serializeToken(item.separatorPosition, item.separator == Luau::CstExprTable::Separator::Comma ? "," : ";"); + else + lua_pushnil(L); + lua_setfield(L, -2, "separator"); + } + else + { + lua_createtable(L, 0, 9); + + lua_pushstring(L, "property"); + lua_setfield(L, -2, "kind"); + + if (prop->accessLocation) + { + LUTE_ASSERT(prop->access != Luau::AstTableAccess::ReadWrite); + serializeToken(prop->accessLocation->begin, prop->access == Luau::AstTableAccess::Read ? "read" : "write"); + } + else + lua_pushnil(L); + lua_setfield(L, -2, "access"); + + if (item.kind == Luau::CstTypeTable::Item::Kind::StringProperty) + { + serializeToken(item.indexerOpenPosition, "["); + lua_setfield(L, -2, "indexerOpen"); + + Luau::Position initialPosition = item.stringPosition; + + switch (item.stringInfo->quoteStyle) + { + case Luau::CstExprConstantString::QuoteStyle::QuotedSingle: + lua_pushstring(L, "single"); + break; + case Luau::CstExprConstantString::QuoteStyle::QuotedDouble: + lua_pushstring(L, "double"); + break; + default: + LUTE_ASSERT(false); + } + lua_setfield(L, -2, "quoteStyle"); + + serializeToken(item.stringPosition, item.stringInfo->sourceString.data); + lua_setfield(L, -2, "key"); + + // Unlike normal tokens, string content contains quotation marks that were not included during advancement + // For simplicity, lets set the current position manually + // If string part was single line, end position = current position + 2 (start and end character) + // If string parts was multi line, end position = current position + 1 (just end character) + if (initialPosition.line == currentPosition.line) + currentPosition.column += 2; + else + currentPosition.column += 1; + + serializeToken(item.indexerClosePosition, "]"); + lua_setfield(L, -2, "indexerClose"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "indexerOpen"); + + lua_pushnil(L); + lua_setfield(L, -2, "quoteStyle"); + + serializeToken(prop->location.begin, prop->name.value); + lua_setfield(L, -2, "key"); + + lua_pushnil(L); + lua_setfield(L, -2, "indexerClose"); + } + + serializeToken(item.colonPosition, ":"); + lua_setfield(L, -2, "colon"); + + prop->type->visit(this); + lua_setfield(L, -2, "value"); + + if (item.separator != Luau::CstExprTable::Separator::Missing) + serializeToken(item.separatorPosition, item.separator == Luau::CstExprTable::Separator::Comma ? "," : ";"); + else + lua_pushnil(L); + lua_setfield(L, -2, "separator"); + + ++prop; + } + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "entries"); + + serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); + lua_setfield(L, -2, "closeBrace"); + } + + void serializeType(Luau::AstTypeFunction* node) + { + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 10); + + 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"); + + 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"); + + serializeToken(cstNode->closeGenericsPosition, ">"); + lua_setfield(L, -2, "closeGenerics"); + } + + serializeToken(cstNode->openArgsPosition, "("); + lua_setfield(L, -2, "openParens"); + + // CstPunctuated + lua_createtable(L, node->argTypes.types.size, 1); + + lua_createtable(L, cstNode->argumentsCommaPositions.size, 0); + + for (size_t i = 0; i < node->argTypes.types.size; i++) + { + // CstFunctionTypeParameter + lua_createtable(L, 0, 3); + + if (i < node->argNames.size && node->argNames.data[i].has_value()) + serializeToken(node->argNames.data[i]->second.begin, node->argNames.data[i]->first.value); + else + lua_pushnil(L); + lua_setfield(L, -2, "name"); + + if (i < cstNode->argumentNameColonPositions.size && cstNode->argumentNameColonPositions.data[i].hasValue()) + serializeToken(cstNode->argumentNameColonPositions.data[i], ":"); + else + lua_pushnil(L); + lua_setfield(L, -2, "colon"); + + node->argTypes.types.data[i]->visit(this); + lua_setfield(L, -2, "type"); + + lua_rawseti(L, -3, i + 1); + + if (i < cstNode->argumentsCommaPositions.size) + serializeToken(cstNode->argumentsCommaPositions.data[i], ","); + else + lua_pushnil(L); + + lua_rawseti(L, -2, i + 1); + } + + lua_setfield(L, -2, "separators"); + + luaL_getmetatable(L, kCstPunctuatedType); + lua_setmetatable(L, -2); + + lua_setfield(L, -2, "parameters"); + + if (node->argTypes.tailType) + node->argTypes.tailType->visit(this); + else + lua_pushnil(L); + lua_setfield(L, -2, "vararg"); + + serializeToken(cstNode->closeArgsPosition, ")"); + lua_setfield(L, -2, "closeParens"); + + serializeToken(cstNode->returnArrowPosition, "->"); + lua_setfield(L, -2, "returnArrow"); + + node->returnTypes->visit(this); + lua_setfield(L, -2, "returnTypes"); + } + + void serializeType(Luau::AstTypeTypeof* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + 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"); + + node->expr->visit(this); + lua_setfield(L, -2, "expression"); + + serializeToken(cstNode->closePosition, ")"); + lua_setfield(L, -2, "closeParens"); + } + + void serializeType(Luau::AstTypeUnion* node) + { + const auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 4); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "union", "type"); + + if (cstNode->leadingPosition.hasValue()) + serializeToken(cstNode->leadingPosition, "|"); + else + lua_pushnil(L); + lua_setfield(L, -2, "leading"); + + // types: CstPunctuated + lua_createtable(L, node->types.size, 1); + + // types.separators: { CstToken<"|"> } + lua_createtable(L, cstNode->separatorPositions.size, 0); + + size_t separatorPositions = 0; + for (size_t i = 0; i < node->types.size; i++) + { + if (node->types.data[i]->is()) + { + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node->types.data[i], "optional", "type"); + + serializeToken(node->types.data[i]->location.begin, "?"); + lua_setfield(L, -2, "token"); + + // types[i + 1] = optionalNode + lua_rawseti(L, -3, i + 1); + + // 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_rawseti(L, -2, i + 1); + } + else + { + node->types.data[i]->visit(this); + + lua_rawseti(L, -3, i + 1); + + // 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_rawseti(L, -2, i + 1); + } + } + + lua_setfield(L, -2, "separators"); + + luaL_getmetatable(L, kCstPunctuatedType); + lua_setmetatable(L, -2); + + lua_setfield(L, -2, "types"); + } + + void serializeType(Luau::AstTypeIntersection* node) + { + const auto cstNode = lookupCstNode(node); + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "intersection", "type"); + + if (cstNode->leadingPosition.hasValue()) + serializeToken(cstNode->leadingPosition, "&"); + else + lua_pushnil(L); + lua_setfield(L, -2, "leading"); + + serializePunctuated(node->types, cstNode->separatorPositions, "&"); + lua_setfield(L, -2, "types"); + } + + void serializeType(Luau::AstTypeSingletonBool* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "boolean", "type"); + + lua_pushboolean(L, node->value); + lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, node->value ? "true" : "false"); + lua_setfield(L, -2, "token"); + } + + void serializeType(Luau::AstTypeSingletonString* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + const auto cstNode = lookupCstNode(node); + serializeNodePreamble(node, "string", "type"); + + switch (cstNode->quoteStyle) + { + case Luau::CstExprConstantString::QuoteStyle::QuotedSingle: + lua_pushstring(L, "single"); + break; + case Luau::CstExprConstantString::QuoteStyle::QuotedDouble: + lua_pushstring(L, "double"); + break; + default: + LUTE_ASSERT(false); + } + lua_setfield(L, -2, "quoteStyle"); + + serializeToken(node->location.begin, cstNode->sourceString.data); + lua_setfield(L, -2, "value"); + + // Unlike normal tokens, string content contains quotation marks that were not included during advancement + // For simplicity, lets set the current position manually + LUTE_ASSERT(currentPosition <= node->location.end); + currentPosition = node->location.end; + } + + void serializeType(Luau::AstTypeGroup* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "group", "type"); + + serializeToken(node->location.begin, "("); + lua_setfield(L, -2, "openParens"); + + node->type->visit(this); + lua_setfield(L, -2, "type"); + + const Luau::NotNull cstNode = lookupCstNode(node); + + serializeToken(cstNode->closePosition, ")"); + lua_setfield(L, -2, "closeParens"); + } + + void serializeType(Luau::AstGenericType* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + + serializeNodePreamble(node, "generic", "type"); + + const auto cstNode = lookupCstNode(node); + + serializeToken(node->location.begin, node->name.value); + lua_setfield(L, -2, "name"); + + if (node->defaultValue) + serializeToken(cstNode->defaultEqualsPosition, "="); + else + lua_pushnil(L); + lua_setfield(L, -2, "equals"); + + if (node->defaultValue) + node->defaultValue->visit(this); + else + lua_pushnil(L); + lua_setfield(L, -2, "default"); + } + + void serializeType(Luau::AstGenericTypePack* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "generic", "typepack"); + + const auto cstNode = lookupCstNode(node); + + serializeToken(node->location.begin, node->name.value); + lua_setfield(L, -2, "name"); + + serializeToken(cstNode->ellipsisPosition, "..."); + lua_setfield(L, -2, "ellipsis"); + + if (node->defaultValue) + serializeToken(cstNode->defaultEqualsPosition, "="); + else + lua_pushnil(L); + lua_setfield(L, -2, "equals"); + + if (node->defaultValue) + node->defaultValue->visit(this); + else + lua_pushnil(L); + lua_setfield(L, -2, "default"); + } + + void serializeType(Luau::AstTypeError* node) + { + // TODO: types + } + + void serializeTypePack(Luau::AstTypePackExplicit* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 4); + + serializeNodePreamble(node, "explicit", "typepack"); + + const auto cstNode = lookupCstNode(node); + + if (cstNode->openParenthesesPosition.hasValue() && cstNode->closeParenthesesPosition.hasValue()) + serializeToken(cstNode->openParenthesesPosition, "("); + else + lua_pushnil(L); + lua_setfield(L, -2, "openParens"); + + serializePunctuated(node->typeList.types, cstNode->commaPositions, ","); + lua_setfield(L, -2, "types"); + + if (node->typeList.tailType) + node->typeList.tailType->visit(this); + else + lua_pushnil(L); + lua_setfield(L, -2, "tailType"); + + if (cstNode->openParenthesesPosition.hasValue() && cstNode->closeParenthesesPosition.hasValue()) + serializeToken(cstNode->closeParenthesesPosition, ")"); + else + lua_pushnil(L); + lua_setfield(L, -2, "closeParens"); + } + + void serializeTypePack(Luau::AstTypePackGeneric* node) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "generic", "typepack"); + + serializeToken(node->location.begin, node->genericName.value); + lua_setfield(L, -2, "name"); + + const auto cstNode = lookupCstNode(node); + serializeToken(cstNode->ellipsisPosition, "..."); + lua_setfield(L, -2, "ellipsis"); + } + + void serializeTypePack(Luau::AstTypePackVariadic* node, bool forVarArg = false) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + serializeNodePreamble(node, "variadic", "typepack"); + + if (!forVarArg) + serializeToken(node->location.begin, "..."); + else + lua_pushnil(L); + lua_setfield(L, -2, "ellipsis"); + + node->variadicType->visit(this); + lua_setfield(L, -2, "type"); + } + + bool visit(Luau::AstExpr* node) override + { + node->visit(this); + return false; + } + + bool visit(Luau::AstExprGroup* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprConstantNil* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprConstantBool* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprConstantNumber* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprConstantInteger* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprConstantString* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprLocal* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprGlobal* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprVarargs* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprCall* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprInstantiate* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprIndexName* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprIndexExpr* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprFunction* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprTable* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprUnary* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprBinary* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprTypeAssertion* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprIfElse* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprInterpString* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstExprError* node) override + { + serialize(node); + return false; + } + + bool visit(Luau::AstStat* node) override + { + node->visit(this); + return false; + } + + bool visit(Luau::AstStatBlock* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatIf* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatWhile* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatRepeat* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatBreak* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatContinue* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatReturn* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatExpr* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatLocal* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatFor* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatForIn* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatAssign* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatCompoundAssign* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatFunction* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatLocalFunction* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatTypeAlias* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatTypeFunction* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatDeclareFunction* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatDeclareGlobal* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatDeclareExternType* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstStatError* node) override + { + serializeStat(node); + return false; + } + + bool visit(Luau::AstType* node) override + { + return true; + } + + bool visit(Luau::AstTypeReference* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeTable* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeFunction* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeTypeof* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeUnion* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeIntersection* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeSingletonBool* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeSingletonString* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstTypeError* node) override + { + return true; + } + + bool visit(Luau::AstTypePack* node) override + { + return true; + } + + bool visit(Luau::AstTypePackExplicit* node) override + { + serializeTypePack(node); + return false; + } + + bool visit(Luau::AstTypePackVariadic* node) override + { + serializeTypePack(node); + return false; + } + + bool visit(Luau::AstTypePackGeneric* node) override + { + serializeTypePack(node); + return false; + } + + bool visit(Luau::AstTypeGroup* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstGenericType* node) override + { + serializeType(node); + return false; + } + + bool visit(Luau::AstGenericTypePack* node) override + { + serializeType(node); + return false; + } +}; + + +int luau_parse(lua_State* L) +{ + std::string source = luaL_checkstring(L, 1); + + StatResult result = parse(source); + + auto& errors = result.parseResult.errors; + + if (!errors.empty()) + { + std::vector locationStrings{}; + locationStrings.reserve(errors.size()); + + size_t size = 0; + for (auto error : errors) + { + locationStrings.emplace_back(Luau::toString(error.getLocation())); + size += locationStrings.back().size() + 2 + error.getMessage().size() + 1; + } + + std::string fullError; + fullError.reserve(size); + + for (size_t i = 0; i < errors.size(); i++) + { + fullError += locationStrings[i]; + fullError += ": "; + fullError += errors[i].getMessage(); + fullError += "\n"; + } + + luaL_error(L, "parsing failed:\n%s", fullError.c_str()); + } + + lua_rawcheckstack(L, 6); + + AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; + + lua_createtable(L, 0, 4); + + serializer.visit(result.parseResult.root); + lua_setfield(L, -2, "root"); + + serializer.serializeEof(result.parseResult.root->location.end); + lua_setfield(L, -2, "eof"); + + lua_pushnumber(L, result.parseResult.lines); + lua_setfield(L, -2, "lines"); + + lua_createtable(L, serializer.lineOffsets.size(), 0); + for (size_t i = 0; i < serializer.lineOffsets.size(); i++) + { + lua_pushinteger(L, i + 1); + lua_pushnumber(L, serializer.lineOffsets[i] + 1); + lua_settable(L, -3); + } + lua_setfield(L, -2, "lineOffsets"); + + deepFreeze(L); + + return 1; +} + +int luau_parseexpr(lua_State* L) +{ + std::string source = luaL_checkstring(L, 1); + + ExprResult result = parseExpr(source); + + auto& errors = result.parseResult.errors; + + if (!errors.empty()) + { + std::vector locationStrings{}; + locationStrings.reserve(errors.size()); + + size_t size = 0; + for (auto error : errors) + { + locationStrings.emplace_back(Luau::toString(error.getLocation())); + size += locationStrings.back().size() + 2 + error.getMessage().size() + 1; + } + + std::string fullError; + fullError.reserve(size); + + for (size_t i = 0; i < errors.size(); i++) + { + fullError += locationStrings[i]; + fullError += ": "; + fullError += errors[i].getMessage(); + fullError += "\n"; + } + + luaL_error(L, "parsing failed:\n%s", fullError.c_str()); + } + + AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; + serializer.visit(result.parseResult.root); + + deepFreeze(L); + + return 1; +} + +static int initSyntaxParserLibrary(lua_State* L) +{ + if (luaL_newmetatable(L, kSpanType)) + { + lua_pushcfunction(L, luau::ltSpan, "span.__lt"); + lua_setfield(L, -2, "__lt"); + + lua_setreadonly(L, -1, 1); + } + + lua_pop(L, 1); + + luaL_newmetatable(L, kCstPunctuatedType); + + lua_pushcfunction(L, luau::iterPunctuated, "punctuated.__iter"); + lua_setfield(L, -2, "__iter"); + + lua_setreadonly(L, -1, 1); + + lua_pop(L, 1); + + return 1; +} +} // namespace luau + +const luaL_Reg SyntaxParser::lib[] = {{"parse", luau::luau_parse}, {"parseExpr", luau::luau_parseexpr}, {nullptr, nullptr}}; + +const char* const SyntaxParser::properties[] = {"cst", luau::kSpanType}; + +int SyntaxParser::pushLibrary(lua_State* L) +{ + lua_createtable(L, 0, std::size(SyntaxParser::lib) + std::size(SyntaxParser::properties)); + + for (auto& [name, func] : SyntaxParser::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return luau::initSyntaxParserLibrary(L); +} + +LUTE_API int luaopen_syntax_parser(lua_State* L) +{ + return SyntaxParser::openAsGlobal(L); +} + +LUTE_API int luteopen_syntax_parser(lua_State* L) +{ + return SyntaxParser::pushLibrary(L); +} \ No newline at end of file diff --git a/lute/syntax/src/syntax.cpp b/lute/syntax/src/syntax.cpp new file mode 100644 index 000000000..de01acf3f --- /dev/null +++ b/lute/syntax/src/syntax.cpp @@ -0,0 +1,153 @@ +#include "lute/syntax.h" + +#include "lute/common.h" + +#include + +namespace luau +{ +struct Span +{ + uint32_t beginLine; + uint32_t beginColumn; + uint32_t endLine; + uint32_t endColumn; +}; + +static Span checkSpan(lua_State* L, int index) +{ + lua_checkstack(L, 1); + + if (!lua_istable(L, index)) + luaL_typeerror(L, index, "span"); + + if (lua_getfield(L, index, "beginLine") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int beginLine = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, index, "beginColumn") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int beginColumn = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, index, "endLine") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int endLine = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, index, "endColumn") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int endColumn = lua_tonumber(L, -1); + lua_pop(L, 1); + + return {static_cast(beginLine), static_cast(beginColumn), static_cast(endLine), static_cast(endColumn)}; +} + +static int createSpan(lua_State* L) +{ + lua_checkstack(L, 2); + + // check that the argument is compliant with the span interface! + Span span = checkSpan(L, 1); + + lua_createtable(L, 0, 4); + + lua_pushinteger(L, span.beginLine); + lua_setfield(L, -2, "beginLine"); + + lua_pushinteger(L, span.beginColumn); + lua_setfield(L, -2, "beginColumn"); + + lua_pushinteger(L, span.endLine); + lua_setfield(L, -2, "endLine"); + + lua_pushinteger(L, span.endColumn); + lua_setfield(L, -2, "endColumn"); + + luaL_getmetatable(L, kSpanType); + lua_setmetatable(L, -2); + + lua_setreadonly(L, -1, 1); + + return 1; +} + +int ltSpan(lua_State* L) +{ + Span lhs = checkSpan(L, 1); + Span rhs = checkSpan(L, 2); + + lua_checkstack(L, 2); + + // 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; +} + +static int makeSpanLibrary(lua_State* L) +{ + lua_checkstack(L, 2); + + lua_createtable(L, 0, 1); + + lua_pushcfunction(L, createSpan, "create"); + lua_setfield(L, -2, "create"); + + lua_setreadonly(L, -1, 1); + + if (luaL_newmetatable(L, kSpanType)) + { + 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 + +const luaL_Reg Syntax::lib[] = {{nullptr, nullptr}}; + +const char* const Syntax::properties[] = {"cst", luau::kSpanType}; + +int Syntax::pushLibrary(lua_State* L) +{ + lua_createtable(L, 0, std::size(Syntax::lib) + std::size(Syntax::properties)); + + lua_createtable(L, 0, 0); + lua_setreadonly(L, -1, 1); + + lua_setfield(L, -2, "cst"); + + luau::makeSpanLibrary(L); + lua_setfield(L, -2, luau::kSpanType); + + lua_setreadonly(L, -1, 1); + return 1; +} + +LUTE_API int luaopen_syntax(lua_State* L) +{ + return Syntax::openAsGlobal(L); +} + +LUTE_API int luteopen_syntax(lua_State* L) +{ + return Syntax::pushLibrary(L); +} diff --git a/lute/system/include/lute/system.h b/lute/system/include/lute/system.h index b856ca0b8..647294407 100644 --- a/lute/system/include/lute/system.h +++ b/lute/system/include/lute/system.h @@ -1,41 +1,19 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" -#include // open the library as a standard global luau library -int luaopen_system(lua_State* L); +LUTE_API int luaopen_system(lua_State* L); // open the library as a table on top of the stack -int luteopen_system(lua_State* L); +LUTE_API int luteopen_system(lua_State* L); -namespace libsystem +struct System : LuteLibrary { - -static const char kArchitectureProperty[] = "arch"; -static const char kOperatingSystemProperty[] = "os"; - -int lua_cpus(lua_State* L); -int lua_threadcount(lua_State* L); -int lua_freememory(lua_State* L); -int lua_totalmemory(lua_State* L); -int lua_hostname(lua_State* L); -int lua_uptime(lua_State* L); - -static const luaL_Reg lib[] = { - {"cpus", lua_cpus}, - {"threadcount", lua_threadcount}, - {"freememory", lua_freememory}, - {"totalmemory", lua_totalmemory}, - {"hostname", lua_hostname}, - {"uptime", lua_uptime}, - - {nullptr, nullptr} -}; - -static const std::string properties[] = { - kArchitectureProperty, - kOperatingSystemProperty, + static constexpr const char kName[] = "system"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace libsystem diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 331a9db48..888ad892f 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -1,16 +1,24 @@ #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 { + +static const char kArchitectureProperty[] = "arch"; +static const char kOperatingSystemProperty[] = "os"; + int lua_cpus(lua_State* L) { int count = uv_available_parallelism(); @@ -60,6 +68,9 @@ int lua_cpus(lua_State* L) lua_settable(L, -3); }; + // free the cpu info array allocated by libuv + uv_free_cpu_info(cpus, count); + return 1; } @@ -88,24 +99,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,21 +122,40 @@ int lua_uptime(lua_State* L) return 1; } -} // namespace libsystem -int luaopen_system(lua_State* L) +int lua_tmpdir(lua_State* L) { - luteopen_system(L); - lua_setglobal(L, "system"); + 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 luteopen_system(lua_State* L) +const char* const System::properties[] = { + libsystem::kArchitectureProperty, + libsystem::kOperatingSystemProperty, +}; + +const luaL_Reg System::lib[] = { + {"cpus", libsystem::lua_cpus}, + {"threadCount", libsystem::lua_threadcount}, + {"freeMemory", libsystem::lua_freememory}, + {"totalMemory", libsystem::lua_totalmemory}, + {"hostName", libsystem::lua_hostname}, + {"uptime", libsystem::lua_uptime}, + {"tmpdir", libsystem::lua_tmpdir}, + {nullptr, nullptr}, +}; + +int System::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(libsystem::lib) + std::size(libsystem::properties)); + lua_createtable(L, 0, std::size(System::lib) + std::size(System::properties)); - for (auto& [name, func] : libsystem::lib) + for (auto& [name, func] : System::lib) { if (!name || !func) break; @@ -160,3 +178,13 @@ int luteopen_system(lua_State* L) return 1; } + +LUTE_API int luaopen_system(lua_State* L) +{ + return System::openAsGlobal(L); +} + +LUTE_API int luteopen_system(lua_State* L) +{ + return System::pushLibrary(L); +} diff --git a/lute/task/CMakeLists.txt b/lute/task/CMakeLists.txt index ed796ef88..01d9daa05 100644 --- a/lute/task/CMakeLists.txt +++ b/lute/task/CMakeLists.txt @@ -8,5 +8,5 @@ target_sources(Lute.Task PRIVATE target_compile_features(Lute.Task PUBLIC cxx_std_17) target_include_directories(Lute.Task PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Task PRIVATE Lute.Runtime Lute.Time Luau.VM uv_a) +target_link_libraries(Lute.Task PRIVATE Lute.Runtime Lute.Common Lute.Time Luau.VM uv_a) target_compile_options(Lute.Task PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/task/include/lute/task.h b/lute/task/include/lute/task.h index 99b60f6f3..a784e8f23 100644 --- a/lute/task/include/lute/task.h +++ b/lute/task/include/lute/task.h @@ -1,29 +1,19 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" // open the library as a standard global luau library -int luaopen_task(lua_State* L); +LUTE_API int luaopen_task(lua_State* L); // open the library as a table on top of the stack -int luteopen_task(lua_State* L); +LUTE_API int luteopen_task(lua_State* L); -namespace task +struct Task : LuteLibrary { - -int lua_defer(lua_State* L); -int lua_wait(lua_State* L); -int lua_spawn(lua_State* L); -int lute_resume(lua_State* L); - -static const luaL_Reg lib[] = { - {"defer", lua_defer}, - {"wait", lua_wait}, - {"spawn", lua_spawn}, - - {"resume", lute_resume}, - - {nullptr, nullptr}, + static constexpr const char kName[] = "task"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace task diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index b4338ca76..47152f118 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -1,16 +1,22 @@ #include "lute/task.h" -#include "lua.h" -#include "lualib.h" +#include "lute/common.h" #include "lute/ref.h" #include "lute/runtime.h" -#include "uv.h" -#include "lute/runtime.h" #include "lute/time.h" + +#include "Luau/Common.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,19 +28,35 @@ struct WaitData uint64_t startedAtMs; + bool closed = false; bool putDeltaTimeOnStack; -}; + int nargs; + void close() + { + if (!closed) + { + uv_timer_stop(&uvTimer); + closed = true; + } + } -static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack) + ~WaitData() + { + close(); + } +}; + +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); + uv_timer_init(getRuntimeLoop(L), &yield->uvTimer); yield->resumptionToken = getResumeToken(L); - yield->startedAtMs = uv_now(uv_default_loop()); + yield->startedAtMs = uv_now(getRuntimeLoop(L)); yield->uvTimer.data = yield; yield->putDeltaTimeOnStack = putDeltaTimeOnStack; + yield->nargs = nargs; uv_timer_start( &yield->uvTimer, @@ -45,50 +67,211 @@ 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) - lua_pushnumber(L, static_cast(uv_now(uv_default_loop()) - yield->startedAtMs) / 1000.0); - - delete yield; + int stackReturnAmount = yield->putDeltaTimeOnStack ? yield->nargs + 1 : yield->nargs; + + if (yield->putDeltaTimeOnStack) + lua_pushnumber(L, static_cast(uv_now(getRuntimeLoop(L)) - yield->startedAtMs) / 1000.0); + + 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 ); } -namespace task +namespace { -int lua_defer(lua_State* L) + +struct LuaThread { - Runtime* runtime = getRuntime(L); + LuaThread(lua_State* L, std::string_view caller = "") + : parent(L) + , runtime(getRuntime(parent)) + { + // At this point, the lua stack should look like: + // [function, args...] + // or + // [thread, args...] + + // If passed a thread, we push the 0 or more arguments onto that threads stack and resume it. + // If passed a function, we push the function and the 0 or more arguments onto a newly created thread's stack + int toPush = lua_gettop(parent); + // We only want to resume with the actual number of arguments here, regardless of if the first argument is a thread or a function + int numResumeArgs = toPush > 1 ? toPush - 1 : 0; + if (!lua_checkstack(parent, 1)) + luaL_error(L, "Not enough stack space to create a new thread"); + if (lua_isfunction(parent, 1)) + { + child = lua_newthread(parent); + } + else if (lua_isthread(parent, 1)) + { + child = lua_tothread(parent, 1); + lua_pushvalue(parent, 1); + lua_remove(parent, 1); + toPush--; + } + else + { + luaL_error(parent, "%s: Expected a thread or function as the first argument.", caller.data()); + } + this->resumeArgs = numResumeArgs; + + // At this point the invariant here is that the caller stack should look like: + // If the first argument was a function + // [function, 0 or more args to resume, thread to return ] + // If the first argument was a thread + // [ 0 or more args to resume the thread with, thread to return ] + if (!lua_checkstack(child, toPush)) + luaL_error(L, "Not enough stack space to push arguments to child thread"); + for (int i = 1; i <= toPush; i++) + lua_xpush(parent, child, i); + + // The top of the stack must be the thread that we will return + LUTE_ASSERT(lua_isthread(parent, -1)); + } - runtime->runningThreads.push_back({true, getRefForThread(L), 0}); - return lua_yield(L, 0); + int defer() + { + runtime->runningThreads.push_back({true, getRefForThread(child), resumeArgs}); + return 1; + } + + int resume() + { + auto st = getChildThreadStatus(); + bool suspended = st.first == LUA_COSUS; + if (!suspended) + { + luaL_error(parent, "cannot resume thread with status %s", st.second); + return 1; + } + + int resumeStatus = lua_resume(child, parent, resumeArgs); + bool ok = resumeStatus == LUA_OK || resumeStatus == LUA_YIELD || resumeStatus == LUA_BREAK; + + if (!ok) + { + runtime->reportError(child); + return 1; + } + + return 1; + } + +private: + std::pair getChildThreadStatus() + { + int st = lua_costatus(parent, child); + return {st, statnames[st]}; + } + + lua_State* parent = nullptr; + Runtime* runtime = nullptr; + lua_State* child = nullptr; + int resumeArgs = 0; }; int lua_spawn(lua_State* L) { + LuaThread newThread{L, "task.spawn"}; + return newThread.resume(); +} + +int lua_deferSelf(lua_State* L) +{ + lua_pushthread(L); + LuaThread newThread{L, "task.deferSelf"}; + newThread.defer(); + return lua_yield(L, 0); +} + +int lua_defer(lua_State* L) +{ + + LuaThread newThread{L, "task.defer"}; + return newThread.defer(); +} + +int lute_resume(lua_State* L) +{ + if (!lua_isthread(L, 1)) + { + luaL_error(L, "Expected a thread as the first argument."); + return 0; + } + LuaThread newThread{L, "task.resume"}; + return newThread.resume(); +} + + +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); - - lua_insert(L, 1); } - else if (!lua_isthread(L, 1)) + else if (lua_isthread(L, 1)) { - luaL_error(L, "can only pass threads or functions to task.spawn"); + 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); - lute_resume(L); return 1; } @@ -116,54 +299,30 @@ int lua_wait(lua_State* L) break; }; - yieldLuaStateFor(L, milliseconds, true); + yieldLuaStateFor(L, milliseconds, true, 0); return lua_yield(L, 0); } -int lute_resume(lua_State* L) -{ - Runtime* runtime = getRuntime(L); - - lua_State* thread = lua_tothread(L, 1); - luaL_argexpected(L, thread, 1, "thread"); - - int currentThreadStatus = lua_costatus(L, thread); - if (currentThreadStatus != LUA_COSUS) - { - luaL_errorL(L, "cannot resume %s coroutine", statnames[currentThreadStatus]); - - return 1; - }; +} // anonymous namespace - lua_remove(L, 1); - - int args = lua_gettop(L); - lua_xmove(L, thread, args); +const char* const Task::properties[] = {nullptr}; - int resumptionStatus = lua_resume(thread, L, args); - if (resumptionStatus != LUA_OK && resumptionStatus != LUA_YIELD && resumptionStatus != LUA_BREAK) - { - runtime->reportError(thread); - } - - return 0; -} - -} // namespace task - -int luaopen_task(lua_State* L) -{ - luaL_register(L, "task", task::lib); - - return 1; -} +const luaL_Reg Task::lib[] = { + {"spawn", lua_spawn}, + {"defer", lua_defer}, + {"resume", lute_resume}, + {"deferSelf", lua_deferSelf}, + {"wait", lua_wait}, + {"delay", lua_delay}, + {nullptr, nullptr}, +}; -int luteopen_task(lua_State* L) +int Task::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(task::lib)); + lua_createtable(L, 0, std::size(Task::lib)); - for (auto& [name, func] : task::lib) + for (auto& [name, func] : Task::lib) { if (!name || !func) break; @@ -176,3 +335,13 @@ int luteopen_task(lua_State* L) return 1; } + +LUTE_API int luaopen_task(lua_State* L) +{ + return Task::openAsGlobal(L); +} + +LUTE_API int luteopen_task(lua_State* L) +{ + return Task::pushLibrary(L); +} diff --git a/lute/time/include/lute/time.h b/lute/time/include/lute/time.h index c7b2620bd..42149e1eb 100644 --- a/lute/time/include/lute/time.h +++ b/lute/time/include/lute/time.h @@ -1,14 +1,18 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" + #include "uv.h" + #include // open the library as a standard global luau library -int luaopen_time(lua_State* L); +LUTE_API int luaopen_time(lua_State* L); // open the library as a table on top of the stack -int luteopen_time(lua_State* L); +LUTE_API int luteopen_time(lua_State* L); static const char kInstantType[] = "instant"; static const char kDurationType[] = "duration"; @@ -19,9 +23,11 @@ double getSecondsFromTimespec(uv_timespec64_t timespec); uv_timespec64_t getTimespecFromDuration(lua_State* L, int idx); int createDurationFromTimespec(lua_State* L, uv_timespec64_t timespec); int createDurationFromSeconds(lua_State* L, double seconds); +void init_duration_lib(lua_State* L); namespace duration { +int lua_createduration(lua_State* L); int lua_nanoseconds(lua_State* L); int lua_microseconds(lua_State* L); int lua_milliseconds(lua_State* L); @@ -32,6 +38,7 @@ int lua_days(lua_State* L); int lua_weeks(lua_State* L); static const luaL_Reg lib[] = { + {"create", lua_createduration}, {"nanoseconds", lua_nanoseconds}, {"microseconds", lua_microseconds}, {"milliseconds", lua_milliseconds}, @@ -46,20 +53,10 @@ static const luaL_Reg lib[] = { } // namespace duration -namespace libtime +struct Time : LuteLibrary