From b47eef117321f2726b0bc6e53b8ee0a3d2285f5a Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Feb 2026 22:34:52 -0600 Subject: [PATCH] Add live syncback functionality and optimize service encoding - Introduced a new endpoint `/api/syncback` for one-click full sync from Roblox Studio to the filesystem, eliminating the need for an active WebSocket connection. - Implemented `SyncbackRequest` and `ServiceChunk` structures to handle service-level properties and child instances during syncback. - Enhanced `encodeService` to include ObjectValue carriers for accurate reference handling and prevent data loss. - Updated the syncback process to utilize Luau-style relative paths for reference attributes, improving path management and consistency. - Integrated session caching in the matching algorithm to optimize performance during syncback operations. These changes significantly improve the live syncback experience, ensuring reliable data synchronization and enhanced performance during development. --- .cursor/commands/luau-optimize.md | 563 +++++++++ .cursor/rules/atlas-api.mdc | 114 +- .cursor/rules/atlas-dedup.mdc | 41 +- .cursor/rules/atlas-plugin.mdc | 43 +- .cursor/rules/atlas-project.mdc | 14 +- .cursor/rules/atlas-src.mdc | 27 +- .cursor/rules/atlas-syncback.mdc | 59 +- .cursor/rules/atlas.mdc | 5 +- .cursor/rules/{luau => luau-rfcs}/CNAME | 0 .cursor/rules/{luau => luau-rfcs}/_config.yml | 0 ...abstract-module-paths-and-init-dot-luau.md | 0 .../amended-require-resolution.md | 0 .../behavior-eq-metamethod.md | 0 .../behavior-stricter-utf8-library.md | 0 .../change-global-version.md | 0 .../{luau => luau-rfcs}/config-luauconfig.md | 0 .../{luau => luau-rfcs}/config-luaurc.md | 0 .../deprecate-getfenv-setfenv.md | 0 .../deprecate-table-getn-foreach.md | 0 ...oposals-leading-to-ambiguity-in-grammar.md | 0 .../explicit-type-parameter-instantiation.md | 0 .../function-bit32-byteswap.md | 0 .../function-bit32-countlz-countrz.md | 0 .../function-buffer-bits.md | 0 .../function-coroutine-close.md | 0 .../function-debug-info.md | 0 .../{luau => luau-rfcs}/function-inlining.md | 0 .../{luau => luau-rfcs}/function-math-lerp.md | 0 .../{luau => luau-rfcs}/function-math-map.md | 0 .../function-string-pack-unpack.md | 0 .../function-table-clear.md | 0 .../function-table-clone.md | 0 .../function-table-create-find.md | 0 .../function-table-freeze.md | 0 .../function-vector-lerp.md | 0 .../generalized-iteration.md | 0 .../generic-function-subtyping.md | 0 .../{luau => luau-rfcs}/generic-functions.md | 0 .../index-type-operator.md | 0 .cursor/rules/{luau => luau-rfcs}/index.md | 0 .../keyof-type-operator.md | 0 .../len-metamethod-rawlen.md | 0 .../local-type-inference.md | 0 .../lower-bounds-calculation.md | 0 .../rules/{luau => luau-rfcs}/luau-mascot.md | 0 .cursor/rules/{luau => luau-rfcs}/mascot.png | Bin .../{luau => luau-rfcs}/math-constants.md | 0 .../math-isnan-isfinite-isinf.md | 0 .../metatable-type-functions.md | 0 .../method-type-issubtypeof.md | 0 .../{luau => luau-rfcs}/negation-types.md | 0 .../never-and-unknown-types.md | 0 .../{luau => luau-rfcs}/new-nonstrict.md | 0 .../new-require-by-string-semantics.md | 0 .../{luau => luau-rfcs}/property-readonly.md | 0 .../{luau => luau-rfcs}/property-writeonly.md | 0 .../rawget-type-operator.md | 0 .../recursive-type-restriction.md | 0 .../relax-recursive-type-restriction.md | 0 .../require-by-string-aliases.md | 0 .../reserve-dollar-sign.md | 0 .../sealed-table-subtyping.md | 0 .../{luau => luau-rfcs}/shared-self-types.md | 0 ...on-types-in-user-defined-type-functions.md | 0 ...er-types-in-user-defined-type-functions.md | 0 .../syntax-array-like-table-types.md | 0 .../syntax-attribute-functions-deprecated.md | 0 .../syntax-attribute-functions-native.md | 0 .../syntax-attributes-functions-parameters.md | 0 .../syntax-attributes-functions.md | 0 .../syntax-compound-assignment.md | 0 .../syntax-continue-statement.md | 0 ...ntax-default-type-alias-type-parameters.md | 0 .../syntax-floor-division-operator.md | 0 .../syntax-if-expression.md | 0 .../syntax-leading-bar-and-ampersand.md | 0 .../syntax-named-function-type-args.md | 0 .../syntax-number-literals.md | 0 .../syntax-property-access-modifiers.md | 0 .../syntax-singleton-types.md | 0 .../syntax-string-interpolation.md | 0 .../syntax-type-alias-type-packs.md | 0 .../syntax-type-ascription-bidi.md | 0 .../syntax-type-ascription.md | 0 .../syntax-typed-variadics.md | 0 .../type-ascription-by-inhabitance.md | 0 .../{luau => luau-rfcs}/type-byte-buffer.md | 0 .../type-error-suppression.md | 0 .../{luau => luau-rfcs}/type-long-integer.md | 80 +- .../types-library-optional.md | 0 .../{luau => luau-rfcs}/udtf-is-extern.md | 0 ...unsealed-table-assign-optional-property.md | 0 .../unsealed-table-literals.md | 0 ...le-subtyping-strips-optional-properties.md | 0 .../user-defined-type-functions.md | 0 .../vector-library-vector2-constructor.md | 0 .../{luau => luau-rfcs}/vector-library.md | 0 .../luau/getting-started/compatibility.md | 133 ++ .cursor/rules/luau/getting-started/intro.md | 147 +++ .cursor/rules/luau/getting-started/lint.md | 387 ++++++ .cursor/rules/luau/getting-started/syntax.md | 318 +++++ .cursor/rules/luau/getting-started/why.md | 28 + .cursor/rules/luau/guides/performance.md | 212 ++++ .cursor/rules/luau/guides/profile.md | 60 + .cursor/rules/luau/guides/sandbox.md | 77 ++ .cursor/rules/luau/index.mdx | 62 + .cursor/rules/luau/reference/grammar.md | 109 ++ .cursor/rules/luau/reference/library.md | 1097 +++++++++++++++++ .cursor/rules/luau/reference/types-library.md | 368 ++++++ .cursor/rules/luau/types/basic-types.md | 240 ++++ .cursor/rules/luau/types/considerations.md | 42 + .cursor/rules/luau/types/generics.md | 71 ++ .../luau/types/object-oriented-programs.md | 84 ++ .cursor/rules/luau/types/overview.md | 160 +++ .cursor/rules/luau/types/refinements.md | 82 ++ .cursor/rules/luau/types/roblox-types.md | 34 + .cursor/rules/luau/types/tables.md | 87 ++ .cursor/rules/luau/types/type-functions.md | 53 + .../luau/types/unions-and-intersections.md | 92 ++ .../roblox/common/navigation/platform.yaml | 2 + .../audio-objects/French-Female-Voice.wav | 3 + .../audio/audio-objects/French-Male-Voice.wav | 3 + .../audio-objects/German-Female-Voice.wav | 3 + .../audio/audio-objects/German-Male-Voice.wav | 3 + .../audio-objects/Italian-Female-Voice.wav | 3 + .../audio-objects/Italian-Male-Voice.wav | 3 + .../audio/audio-objects/Narrator-Voice.wav | 3 + .../audio-objects/Spanish-Female-Voice.wav | 3 + .../audio-objects/Spanish-Male-Voice.wav | 3 + .../datatypes/Font/Builder-Extended.png | 3 + .../datatypes/Font/Builder-Mono.png | 3 + .../AnimationConstraint-IsKinematic.mp4 | 3 + .cursor/rules/roblox/en-us/audio/objects.md | 47 +- .../roblox/en-us/get-started/monetization.md | 5 +- .../en-us/get-started/why-build-on-roblox.md | 34 - .../roblox/en-us/parts/model-generation.md | 2 +- .../roblox/en-us/parts/textures-decals.md | 8 +- .../en-us/physics/constraints/animation.md | 6 +- .../roblox/en-us/production/creator-store.md | 2 + .../production/monetization/immersive-ads.md | 27 +- .../regional-content-availability.md | 23 + .../promotion/rewarded-video-ads.md | 10 +- .../production/publishing/adaptive-design.md | 18 +- .../roblox/en-us/reference/cloud/openapi.json | 24 +- .../en-us/reference/engine/STUDIO_VERSION | 4 +- .../engine/classes/AnalyticsService.yaml | 16 +- .../reference/engine/classes/AudioPlayer.yaml | 3 +- .../engine/classes/AudioSpeechToText.yaml | 3 +- .../engine/classes/AvatarCreationService.yaml | 3 +- .../engine/classes/AvatarEditorService.yaml | 28 +- .../engine/classes/ChangeHistoryService.yaml | 2 +- .../engine/classes/ContentProvider.yaml | 7 + .../engine/classes/GeometryService.yaml | 56 + .../reference/engine/classes/HttpService.yaml | 2 +- .../en-us/reference/engine/classes/Sound.yaml | 9 +- .../engine/classes/TeleportService.yaml | 6 +- .../engine/classes/VideoSampler.yaml | 1 + .../scripting/security/access-control.md | 26 +- .cursor/rules/roblox/en-us/scripting/sync.md | 4 +- .../rules/roblox/en-us/studio/ui-overview.md | 14 +- plugin/src/ChangeBatcher/encodeInstance.lua | 4 +- plugin/src/ChangeBatcher/encodeService.lua | 4 +- plugin/src/PatchTree.lua | 15 + plugin/src/testUtils/MockApiContext.lua | 50 +- src/syncback/property_filter.rs | 21 - 165 files changed, 5205 insertions(+), 206 deletions(-) create mode 100644 .cursor/commands/luau-optimize.md rename .cursor/rules/{luau => luau-rfcs}/CNAME (100%) rename .cursor/rules/{luau => luau-rfcs}/_config.yml (100%) rename .cursor/rules/{luau => luau-rfcs}/abstract-module-paths-and-init-dot-luau.md (100%) rename .cursor/rules/{luau => luau-rfcs}/amended-require-resolution.md (100%) rename .cursor/rules/{luau => luau-rfcs}/behavior-eq-metamethod.md (100%) rename .cursor/rules/{luau => luau-rfcs}/behavior-stricter-utf8-library.md (100%) rename .cursor/rules/{luau => luau-rfcs}/change-global-version.md (100%) rename .cursor/rules/{luau => luau-rfcs}/config-luauconfig.md (100%) rename .cursor/rules/{luau => luau-rfcs}/config-luaurc.md (100%) rename .cursor/rules/{luau => luau-rfcs}/deprecate-getfenv-setfenv.md (100%) rename .cursor/rules/{luau => luau-rfcs}/deprecate-table-getn-foreach.md (100%) rename .cursor/rules/{luau => luau-rfcs}/disallow-proposals-leading-to-ambiguity-in-grammar.md (100%) rename .cursor/rules/{luau => luau-rfcs}/explicit-type-parameter-instantiation.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-bit32-byteswap.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-bit32-countlz-countrz.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-buffer-bits.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-coroutine-close.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-debug-info.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-inlining.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-math-lerp.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-math-map.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-string-pack-unpack.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-table-clear.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-table-clone.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-table-create-find.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-table-freeze.md (100%) rename .cursor/rules/{luau => luau-rfcs}/function-vector-lerp.md (100%) rename .cursor/rules/{luau => luau-rfcs}/generalized-iteration.md (100%) rename .cursor/rules/{luau => luau-rfcs}/generic-function-subtyping.md (100%) rename .cursor/rules/{luau => luau-rfcs}/generic-functions.md (100%) rename .cursor/rules/{luau => luau-rfcs}/index-type-operator.md (100%) rename .cursor/rules/{luau => luau-rfcs}/index.md (100%) rename .cursor/rules/{luau => luau-rfcs}/keyof-type-operator.md (100%) rename .cursor/rules/{luau => luau-rfcs}/len-metamethod-rawlen.md (100%) rename .cursor/rules/{luau => luau-rfcs}/local-type-inference.md (100%) rename .cursor/rules/{luau => luau-rfcs}/lower-bounds-calculation.md (100%) rename .cursor/rules/{luau => luau-rfcs}/luau-mascot.md (100%) rename .cursor/rules/{luau => luau-rfcs}/mascot.png (100%) rename .cursor/rules/{luau => luau-rfcs}/math-constants.md (100%) rename .cursor/rules/{luau => luau-rfcs}/math-isnan-isfinite-isinf.md (100%) rename .cursor/rules/{luau => luau-rfcs}/metatable-type-functions.md (100%) rename .cursor/rules/{luau => luau-rfcs}/method-type-issubtypeof.md (100%) rename .cursor/rules/{luau => luau-rfcs}/negation-types.md (100%) rename .cursor/rules/{luau => luau-rfcs}/never-and-unknown-types.md (100%) rename .cursor/rules/{luau => luau-rfcs}/new-nonstrict.md (100%) rename .cursor/rules/{luau => luau-rfcs}/new-require-by-string-semantics.md (100%) rename .cursor/rules/{luau => luau-rfcs}/property-readonly.md (100%) rename .cursor/rules/{luau => luau-rfcs}/property-writeonly.md (100%) rename .cursor/rules/{luau => luau-rfcs}/rawget-type-operator.md (100%) rename .cursor/rules/{luau => luau-rfcs}/recursive-type-restriction.md (100%) rename .cursor/rules/{luau => luau-rfcs}/relax-recursive-type-restriction.md (100%) rename .cursor/rules/{luau => luau-rfcs}/require-by-string-aliases.md (100%) rename .cursor/rules/{luau => luau-rfcs}/reserve-dollar-sign.md (100%) rename .cursor/rules/{luau => luau-rfcs}/sealed-table-subtyping.md (100%) rename .cursor/rules/{luau => luau-rfcs}/shared-self-types.md (100%) rename .cursor/rules/{luau => luau-rfcs}/support-for-generic-function-types-in-user-defined-type-functions.md (100%) rename .cursor/rules/{luau => luau-rfcs}/support-for-thread-and-buffer-types-in-user-defined-type-functions.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-array-like-table-types.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-attribute-functions-deprecated.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-attribute-functions-native.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-attributes-functions-parameters.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-attributes-functions.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-compound-assignment.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-continue-statement.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-default-type-alias-type-parameters.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-floor-division-operator.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-if-expression.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-leading-bar-and-ampersand.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-named-function-type-args.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-number-literals.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-property-access-modifiers.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-singleton-types.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-string-interpolation.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-type-alias-type-packs.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-type-ascription-bidi.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-type-ascription.md (100%) rename .cursor/rules/{luau => luau-rfcs}/syntax-typed-variadics.md (100%) rename .cursor/rules/{luau => luau-rfcs}/type-ascription-by-inhabitance.md (100%) rename .cursor/rules/{luau => luau-rfcs}/type-byte-buffer.md (100%) rename .cursor/rules/{luau => luau-rfcs}/type-error-suppression.md (100%) rename .cursor/rules/{luau => luau-rfcs}/type-long-integer.md (67%) rename .cursor/rules/{luau => luau-rfcs}/types-library-optional.md (100%) rename .cursor/rules/{luau => luau-rfcs}/udtf-is-extern.md (100%) rename .cursor/rules/{luau => luau-rfcs}/unsealed-table-assign-optional-property.md (100%) rename .cursor/rules/{luau => luau-rfcs}/unsealed-table-literals.md (100%) rename .cursor/rules/{luau => luau-rfcs}/unsealed-table-subtyping-strips-optional-properties.md (100%) rename .cursor/rules/{luau => luau-rfcs}/user-defined-type-functions.md (100%) rename .cursor/rules/{luau => luau-rfcs}/vector-library-vector2-constructor.md (100%) rename .cursor/rules/{luau => luau-rfcs}/vector-library.md (100%) create mode 100644 .cursor/rules/luau/getting-started/compatibility.md create mode 100644 .cursor/rules/luau/getting-started/intro.md create mode 100644 .cursor/rules/luau/getting-started/lint.md create mode 100644 .cursor/rules/luau/getting-started/syntax.md create mode 100644 .cursor/rules/luau/getting-started/why.md create mode 100644 .cursor/rules/luau/guides/performance.md create mode 100644 .cursor/rules/luau/guides/profile.md create mode 100644 .cursor/rules/luau/guides/sandbox.md create mode 100644 .cursor/rules/luau/index.mdx create mode 100644 .cursor/rules/luau/reference/grammar.md create mode 100644 .cursor/rules/luau/reference/library.md create mode 100644 .cursor/rules/luau/reference/types-library.md create mode 100644 .cursor/rules/luau/types/basic-types.md create mode 100644 .cursor/rules/luau/types/considerations.md create mode 100644 .cursor/rules/luau/types/generics.md create mode 100644 .cursor/rules/luau/types/object-oriented-programs.md create mode 100644 .cursor/rules/luau/types/overview.md create mode 100644 .cursor/rules/luau/types/refinements.md create mode 100644 .cursor/rules/luau/types/roblox-types.md create mode 100644 .cursor/rules/luau/types/tables.md create mode 100644 .cursor/rules/luau/types/type-functions.md create mode 100644 .cursor/rules/luau/types/unions-and-intersections.md create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Female-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Male-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Female-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Male-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Female-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Male-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/Narrator-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Female-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Male-Voice.wav create mode 100644 .cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Extended.png create mode 100644 .cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Mono.png create mode 100644 .cursor/rules/roblox/en-us/assets/physics/constraints/AnimationConstraint-IsKinematic.mp4 create mode 100644 .cursor/rules/roblox/en-us/production/promotion/regional-content-availability.md diff --git a/.cursor/commands/luau-optimize.md b/.cursor/commands/luau-optimize.md new file mode 100644 index 00000000..bda66262 --- /dev/null +++ b/.cursor/commands/luau-optimize.md @@ -0,0 +1,563 @@ +# /luau-optimize - Roblox Luau Performance Optimization + +Optimize Roblox Luau files for maximum performance, type safety, and native codegen quality using `luau-compile`, `luau-analyze`, `luau-ast`, and the `luau` CLI. + +All scripts are **Roblox Luau**. Roblox APIs are always available. The vector type is `Vector3` (Roblox userdata). We are NOT using the new type solver. Studio testing uses `--!optimize 1` by default; live experiences use `--!optimize 2`. All `luau-compile` invocations must include `--vector-lib=Vector3 --vector-ctor=new --vector-type=Vector3`. Prefer `.luau` extension over `.lua` everywhere. + +**Workspace hygiene:** CLI tools generate residual files (`stats.json`, `profile.out`, `coverage.out`, `trace.json`). At the start of execution, create a temp working directory and use it for all tool output: + +``` +# Windows +$LUAU_TMP = New-Item -ItemType Directory -Path "$env:TEMP\luau-optimize-$(Get-Random)" +# Linux/Mac +LUAU_TMP=$(mktemp -d) +``` + +Route all output there: `--stats-file=$LUAU_TMP\stats.json`, write benchmark files to `$LUAU_TMP\bench.luau`, run profiling/coverage tools with `$LUAU_TMP` as working directory. The OS handles cleanup -- no manual deletion needed. + +--- + +## Phase 0 -- Scope Resolution and Configuration + +### Resolve target + +- If user specifies a file: optimize that file. +- If user specifies a directory: discover all `.luau`/`.lua` files, enter Phase 6 (multi-file mode). +- If no argument: optimize the currently open file. + +### Infer script context + +Check in this order: + +1. **Filename suffix** (highest priority): + - `*.server.luau` / `*.server.lua` -> **server** (`--!native` is high-value) + - `*.client.luau` / `*.client.lua` -> **client** (`--!native` less beneficial due to device diversity) + - `*.legacy.luau` / `*.legacy.lua` -> **ambiguous** (could be cloned/moved at runtime). Ask the user. + - `*.luau` / `*.lua` (no suffix) -> ModuleScript, fall through to path check. +2. **Path-based inference** (for ModuleScripts and ambiguous cases): + - `ServerScriptService`/`ServerStorage` in path -> server-side module + - `StarterPlayer`/`StarterGui`/`StarterCharacterScripts`/`ReplicatedFirst` in path -> client-side module + - `ReplicatedStorage`/`shared` in path -> shared module (conservative `--!native`) + - Ambiguous or outside Roblox project -> ask the user + +### Quiz the user + +Use the AskQuestion tool with these three questions: + +**Question 1: "Compilation optimization intensity"** +- `minimal` -- Headers (`--!strict`/`--!native`/`--!optimize 2`), type annotations on all function signatures, deprecated pattern replacements only +- `moderate` -- + function restructuring for inlining, import hoisting, compound operators, fastcall enablement, allocation reduction +- `insane` -- + every micro-optimization from the pattern catalog, full bytecode analysis, register pressure optimization, closure caching analysis + +**Question 2: "Algorithm optimization"** +- `none` -- Don't touch logic or data flow +- `low hanging fruit` -- Obvious O(n^2)->dict, redundant iterations, missing caches +- `moderate` -- + data structure changes, caching strategies, event-driven refactors, loop fusion +- `insane` -- + full algorithmic redesign where beneficial, dynamic programming, architectural restructuring + +**Question 3: "Benchmarking"** +- `yes` -- Benchmark all non-trivial changes with `luau` CLI (when function is standalone) +- `no` -- Trust bytecode/codegen metrics only +- `+EV only` -- Benchmark only when algorithmic changes or ambiguous bytecode results warrant it + +### Derived behavior + +| Setting | minimal | moderate | insane | +|---|---|---|---| +| Headers | Always | Always | Always | +| Type annotations | Function signatures | + hot path locals | + all locals | +| Pattern replacements | Deprecated only | Full Priority 2 | + Priority 4 | +| Function restructuring | No | Yes (Priority 3) | Aggressive splitting | +| Bytecode verification | After all changes | After each priority | After each change | +| Algorithmic changes | Per algo quiz | Per algo quiz | Per algo quiz | +| `@native` vs `--!native` | `--!native` unless big file | Selective `@native` on hot functions | Full per-function analysis | + +**Restructuring scope** scales with compilation intensity: +- **minimal**: No restructuring. Only additive changes (headers, annotations, pattern swaps). +- **moderate**: Break up monoliths, reorder locals for inlining, hoist imports. Preserve exports/API. +- **insane**: Aggressive restructuring permitted. May change internal function boundaries, rewrite data flow. + +--- + +## Phase 1 -- Deep Code Read and Algorithmic Analysis + +This phase comes FIRST. Algorithmic improvements produce order-of-magnitude speedups that dwarf any bytecode optimization. This is also the largest code change, so it must happen before any bytecode-level work. + +Read the entire source file thoroughly before touching any CLI tools. + +### Step 1 -- Structural decomposition + +Monolithic functions hide their algorithmic structure behind interleaved concerns. Before analyzing complexity, understand what the code is *actually doing*: + +- **Identify responsibilities**: A 200-line function usually contains 3-5 logical steps that should be separate functions. +- **Trace data flow**: What goes in, what comes out, what is mutated along the way. +- **Separate concerns**: Validation, transformation, I/O, state management, error handling are often tangled. Separating them reveals the core algorithm. +- **Name the steps**: If you can't name what a code block does in 3-5 words, it's doing too much. +- **Look for hidden abstractions**: Repeated "iterate, filter, transform, collect" patterns that could be a single helper. + +When the quiz allows restructuring (moderate/insane): +- Physically decompose monoliths into focused local functions. Each does one thing. +- This decomposition often *reveals* optimization opportunities invisible in the monolith: step 2 recomputes what step 1 already knew, or an inner loop could be a lookup table built in the outer loop. +- Smaller functions are easier for the compiler to inline at `-O2` -- clearer code AND better bytecode. +- Preserve the original function as a thin orchestrator calling the decomposed pieces. + +Even at "minimal" intensity, still perform this mental decomposition to inform the analysis below. + +### Step 2 -- Algorithmic analysis + +With the structure understood (or cleaned up), analyze: + +**Complexity analysis** -- Identify time complexity of every significant function: +- O(n^2) or worse: nested loops, repeated linear searches, quadratic string building +- O(n) where O(1) is possible: linear search replaceable with dictionary/set lookup +- Repeated work: same computation done multiple times, cacheable/memoizable +- Unnecessary copies: cloning data that could be referenced or mutated in place +- Hidden quadratics: O(n) function that calls another O(n) function inside a loop + +**Data structure fitness** -- Is the right data structure being used? +- Array for membership testing -> set (dictionary with `true` values) +- Linear scan for lookup by key -> dictionary +- Repeated sort of mostly-sorted data -> maintain sorted order on insert +- Flat list with frequent removal from middle -> swap-and-pop +- Large string built incrementally -> `table.concat` pattern or `buffer` +- Multiple parallel arrays -> single array of structs (table of tables) +- Unbounded growth without cleanup -> size limits, LRU eviction, periodic pruning + +**Caching and memoization:** +- Pure functions called repeatedly with same args -> memoize +- Expensive property reads in loops -> cache in local before loop +- Computed values unchanged within a scope -> hoist out +- Derived state recomputed from scratch on every access -> maintain incrementally + +**Redundant work elimination:** +- Multiple passes over same data -> fuse into one +- Recomputing derived state -> maintain incrementally +- Sort/filter that could be done once and reused +- Deep clone where shallow clone or reference suffices +- Intermediate result immediately discarded after extracting one field + +**Architectural patterns:** +- Per-frame polling where event-driven would work (RunService events are expensive) +- Synchronous work that could be chunked across frames with `task.wait()` +- Undisconnected event connections (memory leak + wasted computation) +- Rebuilding entire state on small change -> incremental update +- God-object owning too much state -> split responsibilities + +**Hot path identification:** +- Event handlers on RunService (PreAnimation, PreRender, PreSimulation, PostSimulation, Heartbeat) +- Functions called inside loops +- Recursive functions +- Functions called from other hot functions + +### Step 3 -- Present findings and quiz the user + +Before applying any changes, present a summary of all findings organized by impact. For each finding: + +- **What**: One-line description of the issue +- **Where**: Function name / line range +- **Why**: What makes it slow (e.g., "O(n^2) nested scan on every frame") +- **Fix**: Proposed change in plain language +- **Impact**: Estimated improvement (order-of-magnitude, constant-factor, or clarity-only) + +Then quiz the user using the AskQuestion tool: + +- For fewer than 5 findings: one question per finding with options `apply` / `skip` / `modify` +- For 5+ findings: group by category with options `apply all` / `cherry-pick` / `skip all`: + - Structural decomposition + - Data structure changes + - Algorithm changes + - Architectural changes + +If the algorithm quiz from Phase 0 was "none", present findings as informational only (no apply options). The user still sees what was found. + +If the user selects "modify", wait for their input before proceeding with that specific change. + +### Step 4 -- Apply approved changes + +Apply only user-approved changes, in this order (dependencies flow downward): + +1. **Structural decomposition** -- break monoliths into focused functions (foundation for everything else) +2. **Data structure changes** -- swap arrays for dicts, add indexes, etc. +3. **Algorithm changes** -- replace O(n^2) with O(n), add caching/memoization +4. **Architectural changes** -- event-driven refactors, incremental updates + +Benchmark algorithmic changes if the benchmarking quiz warrants it (see Phase 4.5). + +--- + +## Phase 2 -- Baseline Capture + +Run all diagnostic tools on the code (post-algorithmic changes if any were applied in Phase 1). These metrics are the baseline for bytecode optimization phases. + +### Tools to run + +Detect OS: use `--target=x64_ms` on Windows, `--target=x64` on Linux/Mac. Route stats output to temp directory. + +``` +luau-analyze --mode=strict +luau-analyze --annotate +luau-compile --remarks -O2 --vector-lib=Vector3 --vector-ctor=new --vector-type=Vector3 +luau-compile --text -O2 --vector-lib=Vector3 --vector-ctor=new --vector-type=Vector3 +luau-compile --codegen --target=x64_ms --record-stats=function --stats-file=$LUAU_TMP\stats.json -O2 --vector-lib=Vector3 --vector-ctor=new --vector-type=Vector3 +``` + +### Metrics to capture + +- Total bytecode instruction count (from `--text`) +- Allocation count: NEWTABLE, NEWCLOSURE occurrences (from `--remarks`) +- Inlining success/failure count (from `--remarks`: "inlining succeeded"/"inlining failed") +- Type coverage: count of `any` types in `--annotate` output +- Register spills and skipped functions (from `--record-stats` JSON `lowerStats`) +- Lint warning count (from `--mode=strict`) + +--- + +## Phase 3 -- Compilation-Level Code Analysis + +Using both the source (already read in Phase 1) and tool output from Phase 2, identify bytecode-level optimization opportunities: + +- **Missing types**: `any` inferences hurting native codegen (especially `Vector3`, `CFrame`, `buffer` params). JIT uses annotations directly -- no runtime type analysis. Unannotated params assumed to be tables. +- **Import patterns**: `math.max` resolved at load time via GETIMPORT. Broken by `getfenv`/`setfenv`/`loadstring` (marks env "impure"). +- **Allocation in loops**: NEWTABLE, NEWCLOSURE in hot loops. High allocation rate = more GC assist work. +- **Closure caching**: Repeated function expressions cached when: no upvalues, or all upvalues immutable and module-scope. Mutable captures prevent caching. +- **Upvalue mutability**: ~90% immutable in typical code. Immutable = no allocation, no closing, faster access. Mutable = extra object. +- **Method call patterns**: `obj:Method()` uses fast method call instruction. Avoid `obj.Method(obj)`. `__index` should point at a table directly (not function or deep chain). +- **Inline caching**: Best when field name known at compile time, no metatables, uniform shapes. +- **pcall in hot paths**: Prevents native codegen optimization. +- **String concatenation**: `..` in loops -> `table.concat` or string interpolation. +- **Deoptimizing APIs**: `getfenv`/`setfenv` (even read-only!), `loadstring` disable ALL import optimization and fastcalls. +- **Builtin global writes**: `math = ...` disables fastcall. Lint `BuiltinGlobalWrite` catches this. +- **Metamethod cost**: `__eq` always called on `==`/`~=` even for rawequal values. +- **GC pressure**: Incremental GC with "GC assists" -- allocating code pays proportional GC work. + +--- + +## Phase 4 -- Compilation Optimization + +Apply changes in priority order. Re-run `luau-compile --remarks -O2 --vector-lib=Vector3 --vector-ctor=new --vector-type=Vector3 ` after structural changes to verify the compiler benefits. + +### Priority 1 -- Headers and type safety (always apply) + +- Add `--!strict` / `--!native` / `--!optimize 2` header. (`--!optimize 2` is default in live but not Studio testing.) +- Fix type errors from `luau-analyze`. +- Add missing type annotations where `--annotate` shows `any`. Focus on function signatures first, then locals in hot paths per intensity level. +- Annotate `Vector3`, `CFrame`, `buffer` params explicitly -- native codegen generates specialized vector code. +- Replace `getfenv`/`setfenv` -- disables builtins, imports, and optimizations globally. +- Replace `table.getn` -> `#t`, `table.foreach`/`table.foreachi` -> `for..in`. + +### Priority 2 -- Low-hanging structural wins + +- Hoist library functions: `local floor = math.floor` enables GETIMPORT fastcall. Fastcall builtins: `assert`, `type`, `typeof`, `rawget`/`rawset`/`rawequal`, `getmetatable`/`setmetatable`, `tonumber`/`tostring`, most `math.*` (not `noise`, `random`/`randomseed`), `bit32.*`, some `string.*`/`table.*`. Partial specializations: `assert` (unused return + truthy), `bit32.extract` (constant field/width), `select(n, ...)` O(1). +- Replace `pairs(t)`/`ipairs(t)` -> `for k, v in t do`. Generalized iteration skips the `pairs()` call. `for i=1,#t` is slightly slower. +- `math.floor(a / b)` -> `a // b` (dedicated VM opcode, `//=` compound form). +- Use compound assignment (`+=`, `-=`, `*=`, `//=`, `..=`) -- LHS evaluated once. +- String concat in loops -> `table.concat` or backtick interpolation (lowers to optimized `string.format`). +- `table.create(n)` for known-size arrays. Sequential fill: `local t = table.create(N); for i=1,N do t[i] = ... end`. +- `table.insert(t, v)` for unknown-size append -- `#t` is O(1) cached, worst case O(log N). +- `a + (b - a) * t` -> `math.lerp(a, b, t)` (exact at endpoints, monotonic). +- Manual byte swap -> `bit32.byteswap(n)` (CPU bswap). +- Manual log2 -> `bit32.countlz(n)` (CPU instruction, ~8x faster). +- Manual linear search -> `table.find(t, v)`. +- Manual `pairs` clone -> `table.clone(t)`. +- Manual `string.byte`/`string.char` -> `string.pack`/`string.unpack`. +- `rawlen(t)` when metamethods not needed. +- Explicit `./`/`../`/`@` prefixes in `require()`. + +### Priority 3 -- Function structure for inlining + +- Break monoliths into small local functions (compiler inlines at `-O2`). +- Inlining requirements: local, non-mutated, non-recursive, not OOP (`:` syntax), not metamethod. Disabled by `getfenv`/`setfenv`. +- Inlining + constant folding cascade: `local function double(x) return x*2 end; local y = double(5)` folds to `y = 10`. +- Create local function wrappers for frequently-called imported module methods. +- Move `pcall` out of hot loops. +- Use `obj:Method()` not `obj.Method(obj)` -- fast method call instruction. +- `__index` should point at a table directly for inline caching. +- Reduce mutable upvalue captures in loop closures. Immutable upvalues = no allocation, no closing. +- Set all table fields at once (compiler infers hash capacity from subsequent assignments). +- Loop unrolling: constant bounds only, simple body. +- Native codegen size limits: 64K instructions/block, 32K blocks/function, 1M instructions/module. Split if exceeded. + +### Priority 4 -- Micro-optimizations (hot paths only) + +- Table shape consistency (same fields, same order) -- inline caching predicts hash slots. +- `buffer` for binary data (fixed-size, offset-based, efficient native lowering). +- `buffer.readu32` + `bit32` over `buffer.readbits` when schema known. +- Strength reduction: `* 2^n` -> `bit32.lshift`, `/ 2^n` -> `bit32.rshift`. +- Minimize `tostring`/`tonumber` in tight loops. +- `table.freeze` for readonly config (avoids proxy `__index` overhead). +- Keep `__eq` cheap -- fires on every `==`/`~=` and `table.find`. +- `math.isfinite`/`math.isnan`/`math.isinf` over `x ~= x`. +- Annotate `Vector3` params for native specialization. +- `if expr then A else B` over `cond and A or B` (one branch, falsy-safe). + +--- + +## Phase 4.5 -- Benchmark with `luau` CLI + +When a non-trivial optimization is applied to an isolated, pure-logic function (no Roblox API dependencies), write a small benchmark harness: + +```luau +local function original(...) + -- paste original implementation +end + +local function optimized(...) + -- paste optimized implementation +end + +local ITERATIONS = 1_000_000 +local clock = os.clock + +for _ = 1, 1000 do original(...) end +for _ = 1, 1000 do optimized(...) end + +local t0 = clock() +for _ = 1, ITERATIONS do original(...) end +local t1 = clock() +for _ = 1, ITERATIONS do optimized(...) end +local t2 = clock() + +print(`Original: {t1 - t0:.4f}s`) +print(`Optimized: {t2 - t1:.4f}s`) +print(`Speedup: {(t1 - t0) / (t2 - t1):.2f}x`) +``` + +Run with both paths: + +``` +luau -O2 bench.luau +luau -O2 --codegen bench.luau +``` + +**When to benchmark:** Self-contained function, algorithmic/data structure change, ambiguous bytecode diff, tiebreak between approaches. + +**When NOT to benchmark:** Roblox API dependencies (use Studio Script Profiler instead), purely additive changes, cold code. + +Write the benchmark file to `$LUAU_TMP\bench.luau`, not the project directory. Run `luau` from `$LUAU_TMP` as working directory so `profile.out` and other residuals land there too. + +--- + +## Phase 5 -- Verification + +Re-run all Phase 2 tools and present a before/after comparison: + +- Bytecode instruction count delta +- Allocation count delta +- Inlining success rate delta +- Type coverage improvement (fewer `any` types) +- Register spill delta +- Lint warning delta +- Benchmark results (if Phase 4.5 was used) + +If any metric regressed, investigate and explain why (or revert). + +--- + +## Phase 6 -- Multi-file Mode + +When optimizing a directory: + +1. Run Phase 1 (code read) on all files to identify algorithmic improvement potential. +2. Run Phase 2 (baseline) on all files to identify bytecode optimization potential. +3. Prioritize by: worst algorithmic complexity, most lint warnings, most `any` types, most allocations, most failed inlines. +4. Process files one at a time through Phases 1-5. +5. Present aggregate metrics at the end. + +--- + +## Key Principles + +These principles govern every optimization decision: + +- **Same implementation, better performance** -- behavioral equivalence is non-negotiable. Never change what the code does, only how fast it does it. +- **Algorithms first, bytecode second** -- an O(n) algorithm with unoptimized bytecode beats an O(n^2) algorithm with perfect bytecode every time. +- **Low-hanging fruit first** -- headers and types before restructuring, restructuring before micro-optimizations. +- **Compiler feedback loop** -- verify each structural change with `luau-compile --remarks`. If the compiler didn't benefit, the change wasn't worth it. +- **Small functions over monoliths** -- easier for the compiler to inline, easier for humans to maintain, easier to reason about algorithmically. +- **Local functions for inlining** -- even at the cost of duplicating imported behavior in high-traffic code. +- **Quantify everything** -- before/after metrics for every optimization pass. No "I think this is faster" -- prove it. +- **Type annotations drive native codegen quality** -- every `any` type is a missed specialization opportunity. The JIT uses annotations directly with no runtime analysis. +- **Event-driven over polling** -- avoid per-frame calculations when events suffice. + +--- + +## Reference: Type System (Old Typechecker Only) + +**Available NOW:** +- `--!strict` / `--!nonstrict` / `--!nocheck` +- Basic annotations: `local x: number`, `function f(a: string): boolean` +- Optional: `string?`, Union: `number | string`, Intersection: `T1 & T2` +- Generics: `function id(x: T): T`, explicit instantiation: `f<>()` +- Cast: `x :: string` +- Array: `{number}`, Dict: `{[string]: number}`, Table: `type T = { field: type }` +- `typeof()`, `export type`, typed variadics, alias defaults, singletons +- `never`, `unknown`, `read`/`write` modifiers +- Type refinements: `type()`, `typeof()`, `:IsA()`, truthiness, equality, `assert()` + +**DO NOT recommend (requires new typechecker):** +- `keyof`, `index`, `rawkeyof`, `rawget` +- `getmetatable`, `setmetatable` +- User-defined type functions, negation types (`~T`) +- Relaxed recursive type restrictions + +## Reference: Type Refinement Patterns + +The compiler narrows types after certain checks, improving both type safety and native codegen. Use refinements to provide type information without explicit annotations where the type is known from a runtime check: + +- **Truthiness:** `if x then` narrows `x` from falsy (`nil`/`false`) +- **Type guards:** `if type(x) == "number" then` narrows `x` to `number` +- **Typeof guards (Roblox):** `if typeof(x) == "Vector3" then` narrows to `Vector3`; `x:IsA("TextLabel")` narrows `Instance` to subclass +- **Equality:** `if x == "hello" then` narrows `x` to singleton `"hello"` +- **Assert:** `assert(type(x) == "string")` narrows `x` to `string` after the call +- **Composition:** Supports `and`, `or`, `not` for compound refinements + +## Reference: Deprecated Patterns + +| Deprecated | Replacement | Impact | +|---|---|---| +| `getfenv()` / `setfenv()` | `debug.info(i, "snl")` | Disables ALL optimizations. Even read-only `getfenv()` deoptimizes. | +| `loadstring()` | Restructure with `require` | Marks env "impure", disables imports | +| `table.getn(t)` | `#t` | Slower | +| `table.foreach(t, f)` | `for k, v in t do` | Slower | +| `table.foreachi(t, f)` | `for i, v in t do` | Slower | +| `wait()` | `task.wait()` | Deprecated Roblox API | +| `obj.Method(obj, ...)` | `obj:Method(...)` | Misses fast method call instruction | +| `string:method()` | `string.method(s)` | Fastcall: `string.byte(s)` faster than `s:byte()` | + +## Reference: Lint Rules + +| Lint | Name | Optimization impact | +|---|---|---| +| 10 | BuiltinGlobalWrite | Overwriting builtins disables fastcall | +| 22 | DeprecatedApi | Deprecated APIs: perf/correctness issues | +| 23 | TableOperations | Wrong index, redundant insert, `#` on non-arrays | +| 3 | GlobalUsedAsLocal | Global in one function -> should be local | +| 7 | LocalUnused | Dead locals | +| 12 | UnreachableCode | Dead code | +| 25 | MisleadingAndOr | `a and false or c` bugs | + +## Reference: Bytecode Pattern Catalog + +| Slow | Fast | Why | +|---|---|---| +| `math.floor(a / b)` | `a // b` | Dedicated VM opcode | +| `data[i].x = data[i].x + 1` | `data[i].x += 1` | LHS evaluated once | +| `a + (b - a) * t` | `math.lerp(a, b, t)` | Exact at endpoints, monotonic | +| `"pre" .. v .. "suf"` | `` `pre{v}suf` `` | Optimized `string.format` | +| Manual byte swap | `bit32.byteswap(n)` | CPU bswap | +| Manual log2 loop | `bit32.countlz(n)` | CPU instruction, ~8x faster | +| `cond and A or B` | `if cond then A else B` | Safe for falsy, one branch | + +## Reference: Allocation Reduction + +| Slow | Fast | Why | +|---|---|---| +| `local t = {}` in loop | `table.clear(t)` + reuse | Avoids GC pressure | +| Repeated `table.insert` | `table.create(n)` + indexed writes | Preallocated | +| Manual `pairs` clone | `table.clone(t)` | Faster, copies layout | +| `string.byte`/`char` loops | `string.pack`/`unpack` | Native implementation | +| String binary data | `buffer` type | Fixed-size, efficient | + +## Reference: Inlining Rules + +| Blocks inlining | Enables inlining | Why | +|---|---|---| +| `function Module.foo()` | `local function foo()` | Mutable table prevents | +| `function Obj:method()` | `local function method(self)` | OOP syntax not inlineable | +| Recursive function | Split base/recursive | Recursion prevents | +| Deep upvalue captures | Pass as parameters | Reduces closure cost | + +## Reference: Native Codegen + +**`@native` vs `--!native`:** +- `--!native`: entire script. Good for math-heavy utility modules. +- `@native`: per-function. Better for selective hot functions or near the 1M instruction limit. +- Inner functions do NOT inherit `@native`. +- Top-level code runs once; `@native` has minimal benefit there. + +**Hurts native:** `getfenv`/`setfenv`, wrong types to typed functions, non-numeric math args, breakpoints, size limits (64K instructions/block, 32K blocks/function, 1M/module). + +**Helps native:** Type annotations (especially `Vector3`), small functions, consistent table shapes, `buffer` ops, `bit32` ops. + +## Reference: CLI Tools + +### `luau-compile` -- Static bytecode/codegen analysis (primary tool) + +Modes (mutually exclusive): `binary` (raw bytecode), `text` (human-readable opcodes with source annotations), `remarks` (source with inlining/allocation comments -- **most useful**), `codegen` (native assembly, requires `--target`). + +Options: `-O` (0-2, default 1; `-O2` enables inlining), `-g` (debug level 0-2), `--target=` (`x64`, `x64_ms`, `a64`, `a64_nf`; use `x64_ms` on Windows), `--record-stats=` (JSON stats: bytecodeInstructionCount, lowerStats with spills/skipped/blocks/errors), `--bytecode-summary` (opcode distribution, requires `--record-stats=function`), `--stats-file=` (default `stats.json`), `--timetrace` (trace.json), `--vector-lib`/`--vector-ctor`/`--vector-type` (always set for Roblox). + +### `luau-analyze` -- Type checking and linting + +Modes: omitted (typecheck + lint), `--annotate` (source with all inferred types inline -- **critical for finding `any`**). + +Options: `--mode=strict` (force strict even without directive), `--formatter=plain` (machine-parseable), `--formatter=gnu` (grep-friendly), `--timetrace`. + +### `luau` -- Runtime execution, profiling, and coverage + +**Limited for Roblox scripts** (most depend on engine APIs). Only usable for pure-logic modules. For Roblox scripts, use Studio Script Profiler and `debug.dumpcodesize()`. + +Options: `-O`, `-g`, `--codegen` (native execution), `--profile[=N]` (sampling at N Hz, default 10000, outputs `profile.out`), `--coverage` (outputs `coverage.out`), `--timetrace`, `-a` (program args). + +### `luau-ast` -- AST dump + +No flags. Outputs full JSON AST with node types, source locations, variable scopes, type annotations. Useful for programmatic analysis: function nesting depth, function body size (monolith detection), closure captures (upvalue analysis), table construction patterns, loop-invariant expression detection. + +## Reference: VM Architecture + +**Fastcall builtins:** `assert`, `type`, `typeof`, `rawget`/`rawset`/`rawequal`/`rawlen`, `getmetatable`/`setmetatable`, `tonumber`/`tostring`, most `math.*` (not `noise`/`random`/`randomseed`), `bit32.*`, some `string.*`/`table.*`. Partial: `assert` (unused return + truthy), `bit32.extract` (constant field/width), `select(n, ...)` O(1). With `-O2`: constant-arg builtins folded; `math.pi`, `math.huge` folded. + +**Inline caching:** HREF-style for table fields. Compiler predicts hash slot; VM corrects. Best: compile-time field name, no metatables, uniform shapes. + +**Import resolution:** `math.max` resolved at load time. Invalidated by `getfenv`/`setfenv`/`loadstring`. + +**GC:** Incremental mark-sweep with "GC assists" (allocation pays proportional GC). Paged sweeper (16 KB pages). No `__gc` metamethod. + +**Compiler:** Multi-pass. Constant folding across functions/locals. Upvalue optimization. `-O2`: inlining, loop unrolling (constant bounds), aggressive constant folding. Interprocedural limited to single modules. + +**Table length:** `#t` is O(1) cached, worst case O(log N) branch-free binary search. `table.insert`/`table.remove` update the cache. + +**Upvalues:** ~90% immutable. Immutable = no allocation, no closing, faster access. Mutable = extra object. + +**Closures:** Cached when no upvalues, or all upvalues immutable and module-scope. + +## Reference: Documentation to Consult + +Read these docs during execution if needed for specific detail. Two doc trees: +- `.cursor/rules/luau/` -- Official Luau site docs (guides, types, reference, getting-started) +- `.cursor/rules/luau-rfcs/` -- Luau RFCs (language proposals, library additions) + +**Luau site docs (primary):** +- `.cursor/rules/luau/guides/performance.md` -- **The official Luau performance guide.** Covers interpreter, compiler, fastcalls, inline caching, imports, table creation, GC, inlining, loop unrolling, upvalues, closures. +- `.cursor/rules/luau/guides/profile.md` -- Profiling guide (flame graphs, sampling) +- `.cursor/rules/luau/types/` -- Full type system docs (basic-types, tables, generics, refinements, unions-and-intersections, object-oriented-programs, roblox-types, considerations) +- `.cursor/rules/luau/getting-started/lint.md` -- All 28 lint rules +- `.cursor/rules/luau/reference/library.md` -- Complete library reference +- `.cursor/rules/luau/getting-started/compatibility.md` -- Luau vs Lua differences + +**Luau RFCs (specific features):** +- `.cursor/rules/luau-rfcs/function-inlining.md` -- Inlining RFC (no user `@inline`, automatic at `-O2`) +- `.cursor/rules/luau-rfcs/syntax-attribute-functions-native.md` -- `@native` per-function attribute +- `.cursor/rules/luau-rfcs/function-table-create-find.md`, `function-table-clear.md`, `function-table-clone.md`, `function-table-freeze.md` -- Table operations +- `.cursor/rules/luau-rfcs/function-math-lerp.md` -- `math.lerp` +- `.cursor/rules/luau-rfcs/function-bit32-byteswap.md`, `function-bit32-countlz-countrz.md` -- CPU-level bit ops +- `.cursor/rules/luau-rfcs/function-buffer-bits.md`, `type-byte-buffer.md` -- Buffer type and ops +- `.cursor/rules/luau-rfcs/vector-library.md` -- Vector library and fastcalls +- `.cursor/rules/luau-rfcs/function-string-pack-unpack.md` -- Binary string operations +- `.cursor/rules/luau-rfcs/syntax-floor-division-operator.md` -- `//` dedicated opcode +- `.cursor/rules/luau-rfcs/syntax-compound-assignment.md` -- `+=` single LHS evaluation +- `.cursor/rules/luau-rfcs/deprecate-getfenv-setfenv.md` -- Why fenv disables optimizations +- `.cursor/rules/luau-rfcs/deprecate-table-getn-foreach.md` -- Deprecated table functions +- `.cursor/rules/luau-rfcs/generalized-iteration.md` -- Modern iteration patterns +- `.cursor/rules/luau-rfcs/generic-functions.md`, `never-and-unknown-types.md`, `syntax-type-ascription.md` -- Type system RFCs + +**Cursor rules:** +- `.cursor/rules/luau.mdc` -- Luau language rules and optimization tips + +**Roblox docs:** +- `.cursor/rules/roblox/en-us/luau/native-code-gen.md` -- Native codegen (size limits, Vector3 annotation impact) +- `.cursor/rules/roblox/en-us/luau/type-checking.md` -- Type annotation syntax and modes +- `.cursor/rules/roblox/en-us/performance-optimization/improve.md` -- Script computation, memory, physics, rendering +- `.cursor/rules/roblox/en-us/performance-optimization/design.md` -- Event-driven patterns, frame budgets +- `.cursor/rules/roblox/en-us/luau/variables.md` -- Local vs global performance +- `.cursor/rules/roblox/en-us/luau/scope.md` -- Scope performance implications diff --git a/.cursor/rules/atlas-api.mdc b/.cursor/rules/atlas-api.mdc index 1ed8b100..f406aabf 100644 --- a/.cursor/rules/atlas-api.mdc +++ b/.cursor/rules/atlas-api.mdc @@ -45,6 +45,7 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem | `/api/write` | POST | Two-way sync: plugin sends changes | | `/api/open/:id` | POST | Open script in editor (`{"cursor": [line, col]}`) | | `/api/ref-patch/:id` | GET | Fix Ref properties after syncback | +| `/api/syncback` | POST | Live syncback: full Roblox → filesystem sync | | `/api/socket/:cursor` | GET | WebSocket upgrade for live patch streaming | ### Server Info Response (`GET /api/rojo`) @@ -95,6 +96,99 @@ Recomputed fresh on each `GET /api/rojo` connect. Uses a two-phase tree lock: sn **Format detection:** `Name/init.luau` → directory | `Name.luau` → file | neither → use `has_children` (`.lua` also supported) +### Live Syncback Endpoint (`POST /api/syncback`) + +One-click full sync from Studio to filesystem. Does **not** require an active WebSocket connection -- the plugin POSTs directly from the NotConnected page using the host/port fields. + +**Request (`SyncbackRequest`):** + +```rust +pub struct SyncbackRequest { + pub protocol_version: f64, // f64 because Luau has no integer type + pub server_version: String, + pub place_id: Option, + pub data: Vec, // Single rbxm blob containing ALL children from ALL services + pub services: Vec, +} +``` + +**`ServiceChunk`:** + +```rust +pub struct ServiceChunk { + pub class_name: String, + pub child_count: u32, + pub ref_target_count: u32, + pub properties: HashMap, // Service-level properties (omitted if empty) + pub refs: HashMap, // Ref property name → 1-based carrier index (omitted if empty) +} +``` + +**Data format:** The `data` field is a single rbxm binary blob containing all children from all services concatenated. Each service's range in the blob is `child_count + ref_target_count` consecutive entries. First `child_count` entries are real children (parented to service). Next `ref_target_count` entries are temporary ObjectValue carriers encoding Ref properties (destroyed after resolving). The `refs` map connects property names to carrier indices. + +**Validation:** +- Protocol version must match `PROTOCOL_VERSION` (6) → 400 Bad Request +- Server version MAJOR.MINOR must match → 400 Bad Request +- Place ID must pass `servePlaceIds`/`blockedPlaceIds` checks if configured → 403 Forbidden + +**Response:** `{"status": "syncback_initiated"}` + +**Server-side types:** + +```rust +pub struct SyncbackPayload { + pub data: Vec, + pub services: Vec, +} + +pub enum ServerExitReason { + SyncbackRequested(SyncbackPayload), +} +``` + +**`SyncbackSignal` (`src/web/mod.rs`):** Thread-safe signal for API handler → serve loop communication. Uses `Mutex>` + `Notify`. `fire()` deposits payload and notifies; `take_payload()` retrieves it. `LiveServer::start()` uses `tokio::select!` to wait for either new connections OR the syncback signal, exiting the accept loop when fired. + +### Live Syncback Data Flow + +``` +Plugin (NotConnected page) + │ User clicks "Syncback" button → SyncbackConfirm dialog + │ User confirms → performSyncback() + │ Encodes services via encodeService() + serializes children via SerializationService + ▼ +HTTP POST /api/syncback (MessagePack body) + │ + ▼ +handle_api_syncback() [src/web/api.rs] + │ Validates protocol, version, place ID + │ Fires SyncbackSignal + ▼ +LiveServer::start() receives notification [src/web/mod.rs] + │ Exits accept loop + │ Returns ServerExitReason::SyncbackRequested(payload) + ▼ +run_live_syncback() [src/cli/serve.rs] + │ build_dom_from_chunks(payload) → reconstructs DOM from rbxm + service chunks + │ syncback_loop(incremental = false) → clean mode, full filesystem rewrite + │ write_to_vfs_parallel() → writes files + │ refresh_git_index() → updates git index + ▼ +Serve loop restarts (new session, ready for connections) +``` + +### Live Syncback vs Two-Way Sync + +| Aspect | Live Syncback (`/api/syncback`) | Two-Way Sync (`/api/write`) | +|--------|--------------------------------|---------------------------| +| **Purpose** | Full Roblox → filesystem sync | Incremental property/instance changes | +| **Scope** | Entire instance tree (all services) | Individual instance changes | +| **Mode** | Clean mode (replaces everything) | Incremental (add/update/remove) | +| **Server restart** | Yes (restarts serve loop) | No (continues serving) | +| **Data format** | Single rbxm blob + service chunks | Patch-based (added/updated/removed) | +| **Connection** | No WebSocket needed (HTTP POST) | Requires active WebSocket session | +| **When used** | User-initiated one-click syncback | Real-time bidirectional sync | +| **Session** | Creates new session after restart | Uses existing live session | + ## WebSocket Protocol (v6) **Connection:** `ws://host:port/api/socket/:cursor` @@ -652,14 +746,16 @@ Only **Updated** items get defaults. Added/Removed items remain `nil`. The plugin uses a recursive change-count scoring algorithm (implemented in `matching.lua`) to pair incoming virtual instances with existing Studio instances during hydration. -**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, parentVirtual, parentStudio)` +**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, virtualInstances)` **Algorithm:** 1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring. -2. **Ambiguous groups:** For groups with multiple candidates, compute pairwise `_computeChangeCount()` (recursive, scores entire subtree). Greedy assignment sorted by cost ascending, child-order tiebreaker. +2. **Ambiguous groups:** For groups with multiple candidates, compute pairwise `computePairCost()` (recursive, scores entire subtree). Greedy assignment sorted by cost ascending, child-order tiebreaker. 3. **Leftovers:** Unmatched virtual = new instances, unmatched Studio = deleted. -**Value equality:** Shared `trueEquals.lua` module used by both `matching.lua` and `diff.lua` (fuzzy equality for floats, Color3, CFrame, Vector3; NaN handling; nil/null-ref equivalence). +**Performance:** File-level `--!native` and `--!optimize 2` directives for native codegen. `MatchingSession` caches scoring results across recursive calls (`matchCache` for match results, `costCache` for pair costs), avoiding redundant computation when hydrate re-calls `matchChildren` on the same children. `cacheVirtual()` filters children to only those existing in `virtualInstances`, preventing crashes on incomplete virtual instance trees. + +**Value equality:** Shared `trueEquals.lua` module used by both `matching.lua` and `diff.lua` (fuzzy equality for floats with epsilon 0.0001, Color3, CFrame, Vector3; NaN handling; nil/null-ref equivalence). ## Protocol Enforcement @@ -701,8 +797,10 @@ Browser: `http://localhost:34872` → Network tab → WebSocket messages | Server | Plugin | |--------|--------| | `src/web/api.rs` - Endpoints | `plugin/src/ServeSession.lua` - Connection | -| `src/web/interface.rs` - Packets | `plugin/src/ApiContext.lua` - HTTP/WS client | -| `src/serve_session.rs` - Session | `plugin/src/ChangeBatcher/` - Two-way sync | -| `src/change_processor.rs` - FS writer | `plugin/src/Reconciler/` - Patch application | -| `src/rojo_ref.rs` - Ref paths | `plugin/src/Reconciler/matching.lua` - Matching | -| `src/git.rs` - Git integration | `plugin/src/PatchTree.lua` - Default selections | +| `src/web/interface.rs` - Packets, syncback types | `plugin/src/ApiContext.lua` - HTTP/WS client | +| `src/web/mod.rs` - SyncbackSignal, LiveServer | `plugin/src/ChangeBatcher/` - Two-way sync | +| `src/serve_session.rs` - Session | `plugin/src/ChangeBatcher/encodeService.lua` - Live syncback encoding | +| `src/cli/serve.rs` - Live syncback handler | `plugin/src/Reconciler/` - Patch application | +| `src/change_processor.rs` - FS writer | `plugin/src/Reconciler/matching.lua` - Matching | +| `src/rojo_ref.rs` - Ref paths | `plugin/src/PatchTree.lua` - Default selections | +| `src/git.rs` - Git integration | `plugin/src/App/init.lua` - Live syncback UI | diff --git a/.cursor/rules/atlas-dedup.mdc b/.cursor/rules/atlas-dedup.mdc index b160f952..8c9dcbd9 100644 --- a/.cursor/rules/atlas-dedup.mdc +++ b/.cursor/rules/atlas-dedup.mdc @@ -36,6 +36,10 @@ The matching algorithm exists in 3 parallel implementations that must produce id All three use the same recursive change-count scoring + greedy assignment algorithm. Changes to the algorithm must be applied to all three. +All three now have **`MatchingSession` caching** to avoid redundant recursive scoring: +- **Rust:** `cost_cache: RefCell>` -- caches `compute_change_count` results +- **Lua:** `matchCache` (parent virtual ID → Studio instance → MatchResult), `costCache` (virtual ID → Studio instance → cost) + ### Key Files Map | File | Subsystem | Purpose | @@ -45,7 +49,7 @@ All three use the same recursive change-count scoring + greedy assignment algori | `src/syncback/matching.rs` | Matching | Syncback matching: `match_children` (WeakDom) | | `src/snapshot/matching.rs` | Matching | Forward sync matching: `match_forward` (Snapshot vs Tree) | | `plugin/src/Reconciler/matching.lua` | Matching | Lua matching: `Matching.matchChildren` (Virtual vs Studio) | -| `src/rojo_ref.rs` | Ref Paths | `ref_target_path_from_tree`, `RefPathIndex`, `Rojo_Ref_*` constants | +| `src/rojo_ref.rs` | Ref Paths | `ref_target_path_from_tree`, `compute_relative_ref_path`, `resolve_ref_path_to_absolute`, `RefPathIndex`, `Rojo_Ref_*` constants | | `src/snapshot/tree.rs` | Ref Paths | `get_instance_by_path`, `filesystem_name_for` | | `src/syncback/ref_properties.rs` | Ref Paths | `collect_referents`, `tentative_fs_path`, `RefLinks` | | `src/variant_eq.rs` | Matching | `variant_eq` property comparison (Rust) | @@ -213,8 +217,6 @@ function Matching.matchChildren( **Fast-path: (Name, ClassName) Grouping.** Group children by `(Name, ClassName)`. 1:1 groups get instant-matched with zero scoring. -> **Note on Ref Pin:** The Lua matching module has a commented-out Ref pin fast-path (scanning parent Ref properties to pin child matches). In practice this is a no-op because during initial hydration the instanceMap is empty. The Rust implementations do not have Ref pin logic. Ref property correctness is maintained by `computeChangeCount` counting Ref property diffs. - **Ambiguous Groups: Recursive Scoring + Greedy Assignment.** For groups with multiple candidates on at least one side: ``` @@ -271,6 +273,14 @@ Syncback matching has precomputed blake3 subtree hashes. If `new_hash == old_has **Lua (`trueEquals`):** Recursive deep equality. Treats `nil` and null ref (`{Ref = "000...0"}`) as equal. Floats use epsilon 0.0001 (absolute + relative). CFrame/Vector3 component-wise. Color3 via RGB ints. EnumItem via `.Value`. +### Session Caching + +All three implementations wrap scoring in a `MatchingSession` that caches results: +- **Rust:** `compute_change_count` checks `cost_cache` before scoring. If `(ref_a, ref_b)` was scored before, returns cached value. +- **Lua:** `computePairCost` checks `costCache`; `matchChildren` checks `matchCache`. Avoids redundant work when hydrate re-calls matching on the same children. + +The Lua matching file uses `--!native` and `--!optimize 2` file-level directives for native codegen performance. `cacheVirtual()` filters children to only those existing in `virtualInstances`, preventing crashes on incomplete virtual instance trees. + ### Known Limitation: Greedy vs Optimal The greedy algorithm is NOT globally optimal. Edge cases exist where a locally-best pick forces a globally-worse assignment. The Hungarian algorithm (O(N^3)) would give optimal results, but ambiguous groups are typically small (2-10 instances). Accepted for current release. @@ -291,7 +301,21 @@ Dedup'd ModuleScript → segment "Foo~2.luau" Init-style Script → segment "Foo" (directory) ``` -Paths are slash-separated from DataModel's children downward: `"Workspace/TestModel/Part1.model.json5"`. +Absolute paths are slash-separated from DataModel's children downward: `"Workspace/TestModel/Part1.model.json5"`. + +### Luau-Style Relative Ref Paths + +Syncback now writes **relative** ref paths using Luau require-by-string semantics via `compute_relative_ref_path()`: + +- `@self` -- target is the source instance itself +- `@self/Child.luau` -- target is a descendant of source +- `./Sibling.luau` -- target is a same-parent sibling +- `../Uncle.luau` -- target is a parent's sibling +- `@game/Service/Path` -- cross-service reference + +Forward sync resolves both absolute and relative formats via `resolve_ref_path_to_absolute()` + `get_instance_by_path()`. + +**Placeholder system:** During syncback, before dedup suffixes are finalized, ref paths are written as `__ROJO_REF__TO___` placeholders. After the syncback walk assigns final filesystem names, `fix_ref_paths()` substitutes placeholders with actual relative paths. ### Key Functions @@ -492,9 +516,9 @@ Duplicate-named children under ProjectNode are currently skipped during syncback The greedy assignment may produce suboptimal pairing in rare ambiguous groups. A globally optimal algorithm (Hungarian, O(N^3)) would be better but is deferred since ambiguous groups are typically small. -### Matching Cache Not Implemented +### ~~Matching Cache Not Implemented~~ (Resolved) -Phase 2e from the original plan: optional cache mapping `parent_ref → (children_list_hash, match_assignments)` to avoid re-running matching when children haven't changed. Optimization only, not needed for correctness. +`MatchingSession` caching is now implemented in all three matching implementations. See "Session Caching" in Subsystem 3. The per-parent invalidation cache (mapping `parent_ref → (children_list_hash, match_assignments)`) remains deferred. ### ~~Stale Ref Paths After Rename~~ (Resolved) @@ -506,7 +530,7 @@ Both dedup-triggered renames and user-initiated renames now update `Rojo_Ref_*` - **Shared filesystem-segment derivation:** Consolidate tentative filesystem name computation into one shared helper used by `rojo_ref.rs`, `tree.rs`, `api.rs`, and `ref_properties.rs`. Currently duplicated across 4+ call sites. - **Optimal assignment algorithm:** Replace greedy with Hungarian for large ambiguous groups. -- **Matching cache:** Add per-parent cache with invalidation for performance. +- **Per-parent matching invalidation cache:** `MatchingSession` cost/match caching is implemented, but the per-parent cache with `children_list_hash` invalidation (avoids re-running matching when children haven't changed) remains deferred. - **Duplicated name→index grouping:** All three matching implementations repeat the same HashMap pattern. Could be factored but types differ. --- @@ -549,3 +573,6 @@ The dedup system was built across these plans (chronological): 8. **ambiguous_paths_audit_fixes** -- Second audit: recursive matching, `added_paths` format, dedup-aware ref collection, full-FS-name cleanup grouping, shared `trueEquals` 9. **audit_ambiguous_paths_followup** -- Third audit: metadata drift after cleanup rename, co-removed sibling exclusion, `added_paths` extension/suffix, attribute-scoped final paths 10. **audit_ref_two-way_sync** -- Ref audit: round-trip fidelity, path computation correctness, meta lifecycle, VFS suppression, wire format +11. **matching_session_caching** -- Session-scoped caching for all three matching implementations, `--!native`/`--!optimize 2` for Lua, valid children tracking +12. **luau-style_ref_paths** -- Relative ref paths (`@self`, `./`, `../`, `@game/`), source-aware placeholder system +13. **live_syncback** -- One-click full sync from Studio via `POST /api/syncback`, service property encoding, server restart diff --git a/.cursor/rules/atlas-plugin.mdc b/.cursor/rules/atlas-plugin.mdc index 0dfb3a67..fc2b6145 100644 --- a/.cursor/rules/atlas-plugin.mdc +++ b/.cursor/rules/atlas-plugin.mdc @@ -25,9 +25,10 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica - `Studio/` - Studio API wrappers (`StudioPluginAction`, `StudioPluginContext`, `StudioPluginGui`, `StudioToggleButton`, `StudioToolbarContext`, `StudioToolbar`) - General: `Header`, `TextInput`, `TextButton`, `Checkbox`, `Dropdown`, `Tag`, `IconButton`, `Tooltip`, `TouchRipple`, `VirtualScroller`, `ScrollingFrame`, `Spinner`, `BorderedContainer`, `EditableImage`, `SlicedImage`, `ClassIcon` - **StatusPages/** - Main UI pages - - `NotConnected.lua` - Connection form (host/port input) + - `NotConnected.lua` - Connection form (host/port input), "Syncback" button (Danger style) - `Connecting.lua` - Loading screen - `Confirming.lua` - Patch confirmation dialog with push/pull/ignore per-node selections + - `SyncbackConfirm` - Floating dialog for live syncback confirmation (rendered by `App/init.lua`) - `Connected.lua` - Status display with patch history - `Error.lua` - Error display - `Settings/` - Settings UI (`init.lua`, `Setting.lua`) @@ -56,7 +57,8 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica - **init.lua** - Main batcher: accumulates changes every 200ms, pause/resume, deferred Ref resolution - **createPatchSet.lua** - Converts pending changes to PatchSet - **encodePatchUpdate.lua** - Encodes property updates including Ref properties via InstanceMap -- **encodeInstance.lua** - Full instance encoding (for additions/syncback) +- **encodeInstance.lua** - Full instance encoding (for additions/syncback); includes duplicate detection helpers (`findDuplicateNames`, `hasDuplicateSiblings`, `isPathUnique`) +- **encodeService.lua** - Encodes service-level properties/attributes/refs for live syncback; uses ObjectValue carriers for Ref properties - **encodeProperty.lua** - Single property value encoding via RbxDom - **propertyFilter.lua** - Property type filtering (only `UniqueId` blocked; `Ref` is allowed) @@ -242,12 +244,22 @@ Pairs incoming virtual instances with existing Studio instances during hydration **Algorithm:** 1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring. -2. **Ambiguous groups:** For groups with multiple candidates, compute pairwise `_computeChangeCount()` (recursive, scores entire subtree). Greedy assignment sorted by cost ascending, child-order tiebreaker for stability. +2. **Ambiguous groups:** For groups with multiple candidates, compute pairwise `computePairCost()` (recursive, scores entire subtree). Greedy assignment sorted by cost ascending, child-order tiebreaker for stability. 3. **Leftovers:** Unmatched virtual = new instances, unmatched studio = deleted. +**Performance:** File-level `--!native` and `--!optimize 2` directives for native codegen. `MatchingSession` caches results across recursive calls: +```lua +type MatchingSession = { + matchCache: { [string]: { [Instance]: MatchResult } }, + costCache: { [string]: { [Instance]: number } }, +} +``` + +**Valid children tracking:** `cacheVirtual()` filters children to only those existing in `virtualInstances`, preventing crashes on incomplete virtual instance trees. + **Sort stability note:** `table.sort` is NOT stable in Luau. Comparator breaks ties by insertion index to ensure deterministic ordering. -**Value equality:** Shared `trueEquals.lua` module used by both `matching.lua` and `diff.lua` -- fuzzy equality for floats (epsilon 0.0001), Color3 via RGB ints, CFrame/Vector3 component-wise, NaN handling, nil/null-ref equivalence. +**Value equality:** Shared `trueEquals.lua` module used by both `matching.lua` and `diff.lua` -- fuzzy equality for floats (epsilon 0.0001, absolute + relative), Color3 via RGB ints, CFrame/Vector3 component-wise, NaN handling, nil/null-ref equivalence, EnumItem/number interop. ## InstanceMap (`InstanceMap.lua`) @@ -308,6 +320,29 @@ Watches Studio changes and sends them to the server in real-time. Enabled by def - `__paused` flag blocks flushes during confirmation dialog - `RunService:IsRunning()` blocks during Play mode +## Live Syncback + +One-click full sync from Studio to filesystem. Available from the **NotConnected page** -- does not require an active WebSocket session. Sends directly to the server via HTTP POST using the host/port fields from the connection form. + +**`SYNCBACK_SERVICES` constant (14 services):** Lighting, MaterialService, ReplicatedFirst, ReplicatedStorage, ServerScriptService, ServerStorage, SoundService, StarterGui, StarterPack, StarterPlayer, Teams, TextChatService, VoiceChatService, Workspace. + +**Flow:** +1. User clicks "Syncback" button on NotConnected page → `SyncbackConfirm` floating dialog appears +2. Dialog warns: "This will overwrite your project files with the current Studio state. This cannot be undone." +3. User confirms → `performSyncback()`: + a. Iterates `SYNCBACK_SERVICES`, calls `encodeService(service)` for each + b. Collects all children instances and ObjectValue Ref carriers + c. Serializes all children via `SerializationService:SerializeInstancesAsync()` into a single buffer + d. Destroys temporary ObjectValue carriers + e. POSTs to `http://host:port/api/syncback` (MessagePack): `{protocolVersion, serverVersion, placeId, data, services}` +4. Shows success/error notification + +**`encodeService(service)`** (`ChangeBatcher/encodeService.lua`): +- Encodes service-level properties via `RbxDom.findClassDescriptor()` +- Skips: `Parent`, `Name`, `Archivable`, `Attributes`, `Tags` (handled separately), class-specific excludes (`Lighting.ClockTime`) +- Ref properties: creates temporary `ObjectValue` carriers (Name = property name, Value = target instance), stores 1-based index in `refs` map +- Returns `(chunk, refTargets)` where `chunk = {className, childCount, refTargetCount, properties?, refs?}` + ## Dependencies ### Vendored Packages (`plugin/Packages/`) diff --git a/.cursor/rules/atlas-project.mdc b/.cursor/rules/atlas-project.mdc index f55bc43e..6cf66c8c 100644 --- a/.cursor/rules/atlas-project.mdc +++ b/.cursor/rules/atlas-project.mdc @@ -191,7 +191,7 @@ Configure `syncbackRules` object for Roblox → filesystem sync: | `ignoreProperties` | object | `{}` | Properties to skip per class: `{"ClassName": ["Prop1", "Prop2"]}` | | `ignoreClasses` | string[] | `[]` | Instance classes to skip entirely | | `syncCurrentCamera` | bool | `false` | Whether to sync `Workspace.CurrentCamera` | -| `syncUnscriptable` | bool | `true` | Whether to sync unscriptable properties | +| `syncUnscriptable` | bool | `false` | Whether to sync unscriptable properties | | `ignoreReferents` | bool | `false` | Whether to skip Ref properties (e.g., `Model.PrimaryPart`) | | `createIgnoreDirPaths` | bool | `false` | Whether `/**` in ignore paths also matches the directory itself | | `ignoreHiddenServices` | bool | — | Override root-level `ignoreHiddenServices` for syncback | @@ -208,7 +208,7 @@ Configure `syncbackRules` object for Roblox → filesystem sync: }, "ignoreClasses": ["Terrain", "Camera"], "syncCurrentCamera": false, - "syncUnscriptable": true, + "syncUnscriptable": false, "ignoreReferents": false, "createIgnoreDirPaths": false, "warnDuplicateNames": false @@ -341,9 +341,15 @@ Uses filesystem-name paths (slugified, dedup'd) to reference target instances: } ``` -**Attribute format:** `Rojo_Ref_ = ""` +**Attribute format:** `Rojo_Ref_ = ""` -Path segments use full filesystem names (slug + extension + dedup suffix). Resolved during forward sync via `get_instance_by_path()` (case-insensitive filesystem name matching with instance name fallback). +Syncback writes **Luau-style relative paths**: +- `@self` / `@self/Child.luau` -- target is same instance or a descendant +- `./Sibling.luau` -- target is a same-parent sibling +- `../Uncle.luau` -- target is a parent's sibling +- `@game/Service/Path` -- cross-service reference + +Forward sync resolves both relative and absolute formats via `resolve_ref_path_to_absolute()` + `get_instance_by_path()`. Path segments use full filesystem names (slug + extension + dedup suffix), matched case-insensitive with instance name fallback. ### ID-Based: `Rojo_Target_*` + `$id` (legacy, still read by forward sync) diff --git a/.cursor/rules/atlas-src.mdc b/.cursor/rules/atlas-src.mdc index 5310808f..a914ecf5 100644 --- a/.cursor/rules/atlas-src.mdc +++ b/.cursor/rules/atlas-src.mdc @@ -19,7 +19,7 @@ Pure functions: filesystem → instance tree → minimal patches → apply chang - `mod.rs` - System overview with detailed explanation - `patch_compute.rs` - Generate diffs (uses matching algorithm) - `patch_apply.rs` - Apply changes (resolves `Rojo_Ref_*` and `Rojo_Target_*` refs) -- `matching.rs` - Forward sync matching algorithm (recursive change-count scoring) +- `matching.rs` - Forward sync matching algorithm (recursive change-count scoring, `MatchingSession` caching) - `instance_snapshot.rs` - Core snapshot type ```rust @@ -79,7 +79,7 @@ Instance names with forbidden filesystem chars (`<>:"/\|?*~`) are handled by: ### 3. Commands (`src/cli/`) -- `serve.rs` - Live sync with WebSocket +- `serve.rs` - Live sync with WebSocket; live syncback via `run_live_syncback()` + `build_dom_from_chunks()` (server restarts after syncback) - `build.rs` - Generate rbxl/rbxm/rbxmx/rbxlx - `upload.rs` - Upload to Roblox.com - `sourcemap.rs` - Generate sourcemaps @@ -92,17 +92,19 @@ Reverse: Roblox file → filesystem - `snapshot.rs` - Convert instances to snapshots - `fs_snapshot.rs` - Filesystem representation -- `matching.rs` - Syncback matching algorithm (recursive change-count scoring) +- `matching.rs` - Syncback matching algorithm (recursive change-count scoring, `MatchingSession` caching) - `dedup_suffix.rs` - Dedup cleanup rules (gap-tolerant, base-name promotion, group-to-1) - `meta.rs` - Meta file read/write helpers (upsert/remove name, update ref paths) -- `ref_properties.rs` - Ref property path linking (`Rojo_Ref_*`, tentative_fs_path) +- `ref_properties.rs` - Ref property path linking (`Rojo_Ref_*`, tentative_fs_path, placeholder system) - `hash/` - Content hashing for change detection -- `property_filter.rs` - Apply syncback rules +- `fs_snapshot.rs` - Filesystem representation, parallel writes (`write_to_vfs_parallel`), ref path post-processing (`fix_ref_paths`) +- `property_filter.rs` - Apply syncback rules (default: `sync_unscriptable = false`) ### 5. Web Server (`src/web/`) -- `api.rs` - HTTP/WebSocket endpoints -- `interface.rs` - Browser UI +- `api.rs` - HTTP/WebSocket endpoints, including `POST /api/syncback` handler (`handle_api_syncback`) +- `interface.rs` - Wire types: patches, `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload`, `ServerExitReason` +- `mod.rs` - `SyncbackSignal` (Mutex + Notify pattern), `LiveServer` (uses `tokio::select!` to wait for connections or syncback signal) - Protocol version enforcement ### 6. Project System (`src/project.rs`) @@ -123,6 +125,8 @@ Parse/validate `.project.json5` (or legacy `.project.json`), handle nested proje **Key:** `src/serve_session.rs` manages active sessions +**Live syncback integration:** The serve loop checks for `ServerExitReason::SyncbackRequested(payload)`. When received, calls `run_live_syncback()` which reconstructs the DOM from the plugin's rbxm blob + service chunks via `build_dom_from_chunks()`, runs `syncback_loop(incremental=false)` (clean mode), writes files via `write_to_vfs_parallel()`, refreshes git index, then restarts the serve loop. + ### `rojo build` 1. Load project @@ -149,7 +153,7 @@ pub struct SyncbackRules { ignore_properties: IndexMap>, // Class → properties ignore_classes: Vec, // Classes to skip entirely sync_current_camera: Option, // Default: false - sync_unscriptable: Option, // Default: true + sync_unscriptable: Option, // Default: false ignore_referents: Option, // Skip Ref properties, default: false } ``` @@ -220,6 +224,7 @@ Review: `cargo insta review` **HTTP:** - `GET /` - Browser UI - `GET /api/rojo` - Server info (version, project name, protocol) +- `POST /api/syncback` - Live syncback: full Roblox → filesystem sync (validates protocol, version, place ID) **WebSocket:** `ws://host:port/api/rojo` @@ -312,6 +317,7 @@ to_writer(&mut buffer, &dom, dom.root())?; **Core:** rbx_binary, rbx_xml, rbx_dom_weak, memofs (local crate) **Async:** tokio, hyper, hyper-tungstenite **Serialization:** serde, json5, toml, yaml-rust2, csv +**Float precision:** lexical-write-float (for roundtrip-safe f32/f64 formatting in JSON) **Utilities:** clap, anyhow, thiserror, crossbeam-channel, rayon, globset ## Module Organization @@ -378,10 +384,11 @@ src/ matching.rs - Syncback matching algorithm dedup_suffix.rs - Dedup cleanup rules meta.rs - Meta file helpers - web/ - HTTP/WebSocket + web/ - HTTP/WebSocket (incl. SyncbackSignal, LiveServer) + interface.rs - Wire types (patches, syncback requests) change_processor.rs - Two-way sync filesystem writer git.rs - Git integration (changed files, SHA1 hashing, auto-staging) - rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex) + rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex, Luau-style relative paths) variant_eq.rs - Property value comparison lib.rs - Public API main.rs - CLI entry diff --git a/.cursor/rules/atlas-syncback.mdc b/.cursor/rules/atlas-syncback.mdc index 726d938a..43607d93 100644 --- a/.cursor/rules/atlas-syncback.mdc +++ b/.cursor/rules/atlas-syncback.mdc @@ -114,6 +114,30 @@ pub fn syncback_loop( **Implementation detail:** `old_ref` is ignored in `SyncbackSnapshot::with_joined_path` when `incremental=false`. +### Live Syncback Mode (via `atlas serve`) + +Triggered via `POST /api/syncback` during an active `atlas serve` session. The plugin encodes the Studio state (service properties + children as rbxm blob) and POSTs directly -- no WebSocket connection required. + +**Server-side flow:** +1. `handle_api_syncback()` validates protocol, version, place ID +2. Fires `SyncbackSignal` → `LiveServer` exits accept loop +3. `run_live_syncback()` in `src/cli/serve.rs`: + a. `build_dom_from_chunks()` reconstructs DOM from rbxm blob + `ServiceChunk` metadata + b. Runs `syncback_loop(incremental=false)` -- always clean mode + c. `write_to_vfs_parallel()` writes files with Windows retry logic + d. `refresh_git_index()` updates git +4. Serve loop restarts (new session) + +**Key differences from CLI syncback:** +- Runs during an active serve session (not standalone) +- Receives data via HTTP (not file-based) +- Always clean mode (replaces everything) +- Server restarts after completion + +**Plugin-side encoding:** `encodeService()` encodes service properties/attributes/refs (ObjectValue carriers for Ref), `SerializationService:SerializeInstancesAsync()` serializes all children into a single rbxm buffer. + +See `atlas-api.mdc` for wire format details (`SyncbackRequest`, `ServiceChunk`). + ## Matching Algorithm (`matching.rs`) Pairs new instances (from Roblox file) with old instances (from filesystem) to determine which files to update vs create. @@ -133,6 +157,8 @@ Pairs new instances (from Roblox file) with old instances (from filesystem) to d **Output:** `MatchResult { matched: Vec<(new_ref, old_ref)>, unmatched_new: Vec, unmatched_old: Vec }` +**Session caching:** `MatchingSession` wraps a `cost_cache: RefCell>` to cache `compute_change_count` results, avoiding redundant recursive scoring when the same pairs are encountered at different tree levels. + **Known limitation:** Greedy assignment is not globally optimal. The Hungarian algorithm (O(N^3)) would give optimal results, but ambiguous groups are typically small (2-10 instances). ## Dedup Suffix Management (`dedup_suffix.rs`) @@ -171,7 +197,7 @@ pub struct SyncbackRules { /// Whether to sync Workspace.CurrentCamera (default: false) pub sync_current_camera: Option, - /// Whether to sync unscriptable properties (default: true) + /// Whether to sync unscriptable properties (default: false) pub sync_unscriptable: Option, /// Whether to skip Ref properties (default: false) @@ -340,10 +366,10 @@ fs_snapshot.merge(other) // Combine snapshots // Apply to filesystem fs_snapshot.write_to_vfs(base_path, vfs)?; -fs_snapshot.write_to_vfs_parallel(base_path, vfs)?; // Parallel writes with retry logic +fs_snapshot.write_to_vfs_parallel(base_path, vfs)?; // Parallel writes (see below) // Post-process ref paths -fs_snapshot.fix_ref_paths(&substitutions); // Replace tentative paths with final dedup'd paths +fs_snapshot.fix_ref_paths(&substitutions); // Replace placeholder paths with final relative paths // Query fs_snapshot.is_empty() @@ -357,18 +383,22 @@ fs_snapshot.removed_paths() **Solution: Path-based `Rojo_Ref_*` attributes (preferred)** -Syncback writes filesystem-name-based paths as `Rojo_Ref_PropertyName` attributes: +Syncback writes **Luau-style relative paths** as `Rojo_Ref_PropertyName` attributes via `compute_relative_ref_path()`: ```json5 { "properties": {}, "attributes": { - "Rojo_Ref_PrimaryPart": "Workspace/Handle.model.json5" + "Rojo_Ref_PrimaryPart": "./Handle.model.json5" } } ``` -Forward sync reads `Rojo_Ref_*` attributes and resolves them to tree instances via `get_instance_by_path()`. +Relative path prefixes: `@self` (same/descendant), `./` (sibling), `../` (parent's sibling), `@game/` (cross-service). + +Forward sync resolves both relative and absolute formats via `resolve_ref_path_to_absolute()` + `get_instance_by_path()`. + +**Placeholder system:** During the syncback walk, before dedup suffixes are finalized, ref paths are written as `__ROJO_REF__TO___` placeholders (encoding both source and target refs). After the walk assigns final filesystem names, `fix_ref_paths()` substitutes placeholders with actual relative paths. Substitutions are sorted by length descending to prevent partial prefix matches. **Legacy: `Rojo_Target_*` / `$id` system (still supported for reading)** @@ -498,12 +528,9 @@ Filesystem: ## Integration with Plugin -**Plugin does NOT handle syncback.** It only handles: -1. Downloading place file from Studio -2. Sending file to server -3. Server runs syncback command +**Live syncback (plugin-initiated):** The plugin encodes the Studio state via `encodeService()` + `SerializationService` and POSTs to `/api/syncback`. The server reconstructs the DOM, runs `syncback_loop(incremental=false)`, writes files, and restarts. Available from the NotConnected page -- no active WebSocket session required. See `atlas-api.mdc` for wire format. -**Two-way sync** is different - see `atlas-api.mdc`. +**Two-way sync** is a separate system for incremental bidirectional changes -- see `atlas-api.mdc`. ## Performance Considerations @@ -511,7 +538,9 @@ Filesystem: **Incremental benefit:** On large projects, incremental mode with hashing can be 10-100x faster than rewriting everything. -**Parallelization:** File writes can be parallelized via `write_to_vfs_parallel()` (includes Windows retry logic). +**Parallelization:** File writes can be parallelized via `write_to_vfs_parallel()`: +- 4-phase approach: create dirs (sequential) → write files (parallel via rayon) → remove files (parallel) → remove dirs (sequential via VFS) +- Windows retry logic: exponential backoff (10ms initial, doubles each retry, max 3 retries) for OS errors 5 (Access denied) and 32 (Sharing violation) ## Debugging @@ -546,7 +575,7 @@ diff place.rbxl rebuilt.rbxl - Better handling of Roblox format changes - Support for partial syncback (only changed instances) - Globally optimal matching (Hungarian algorithm) for large ambiguous groups -- Matching cache (parent_ref -> match_assignments) for avoiding redundant scoring +- ~~Matching cache (parent_ref -> match_assignments) for avoiding redundant scoring~~ (session-level caching now implemented via `MatchingSession`; per-parent invalidation cache still deferred) ## Key Files to Reference @@ -558,5 +587,9 @@ diff place.rbxl rebuilt.rbxl - `src/syncback/meta.rs` - Meta file read/write helpers - `src/syncback/ref_properties.rs` - Ref property path linking - `src/syncback/hash/` - Hashing algorithm +- `src/syncback/fs_snapshot.rs` - Filesystem operations, parallel writes, ref path post-processing - `src/syncback/property_filter.rs` - Property filtering logic - `src/cli/syncback.rs` - CLI command implementation +- `src/cli/serve.rs` - Live syncback handler (`run_live_syncback`, `build_dom_from_chunks`) +- `src/web/interface.rs` - `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload` types +- `src/rojo_ref.rs` - Luau-style relative ref paths (`compute_relative_ref_path`, `resolve_ref_path_to_absolute`) diff --git a/.cursor/rules/atlas.mdc b/.cursor/rules/atlas.mdc index 0b1edfa7..bb00e2b9 100644 --- a/.cursor/rules/atlas.mdc +++ b/.cursor/rules/atlas.mdc @@ -32,6 +32,7 @@ Atlas bridges filesystem and Roblox Studio, enabling professional development to - Build/upload rbxl/rbxm files from command line - Syncback: pull instances from Roblox → filesystem (7.7.0+) - Two-way sync (plugin-based): live bidirectional sync during `atlas serve` sessions +- Live syncback: one-click full sync from Studio to filesystem during `atlas serve` (no WebSocket connection required) ### Two-Way Sync Strategy @@ -206,7 +207,7 @@ pub struct InstanceContext { } ``` -**Filename handling:** Instance names with forbidden filesystem chars are slugified (forbidden chars → `_`), deduplicated (`~1`, `~2`, etc. on collision), and the real name is stored in `.meta.json5`/`.model.json5` `name` fields. See `src/syncback/file_names.rs`. +**Filename handling:** Instance names with forbidden filesystem chars are slugified (forbidden chars → `_`), deduplicated (`~2`, `~3`, etc. on collision), and the real name is stored in `.meta.json5`/`.model.json5` `name` fields. See `src/syncback/file_names.rs`. **VFS passed separately to snapshot functions:** ```rust @@ -322,6 +323,7 @@ src/ - Rust server/CLI dedup_suffix.rs - Dedup cleanup rules meta.rs - Meta file read/write helpers web/ - HTTP/WebSocket server + interface.rs - Wire types (patches, syncback requests) change_processor.rs - Two-way sync filesystem writer git.rs - Git integration (changed files, SHA1 hashing, auto-staging) rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex) @@ -333,6 +335,7 @@ plugin/src/ - Lua Studio plugin matching.lua - Lua matching algorithm trueEquals.lua - Shared value equality module ChangeBatcher/ - Two-way sync patch creation + encodeService.lua - Service-level property encoding for live syncback Config.lua - Version and protocol config PatchTree.lua - Patch visualization tree (git-based default selections) SHA1.luau - SHA1 hash for git blob format comparison diff --git a/.cursor/rules/luau/CNAME b/.cursor/rules/luau-rfcs/CNAME similarity index 100% rename from .cursor/rules/luau/CNAME rename to .cursor/rules/luau-rfcs/CNAME diff --git a/.cursor/rules/luau/_config.yml b/.cursor/rules/luau-rfcs/_config.yml similarity index 100% rename from .cursor/rules/luau/_config.yml rename to .cursor/rules/luau-rfcs/_config.yml diff --git a/.cursor/rules/luau/abstract-module-paths-and-init-dot-luau.md b/.cursor/rules/luau-rfcs/abstract-module-paths-and-init-dot-luau.md similarity index 100% rename from .cursor/rules/luau/abstract-module-paths-and-init-dot-luau.md rename to .cursor/rules/luau-rfcs/abstract-module-paths-and-init-dot-luau.md diff --git a/.cursor/rules/luau/amended-require-resolution.md b/.cursor/rules/luau-rfcs/amended-require-resolution.md similarity index 100% rename from .cursor/rules/luau/amended-require-resolution.md rename to .cursor/rules/luau-rfcs/amended-require-resolution.md diff --git a/.cursor/rules/luau/behavior-eq-metamethod.md b/.cursor/rules/luau-rfcs/behavior-eq-metamethod.md similarity index 100% rename from .cursor/rules/luau/behavior-eq-metamethod.md rename to .cursor/rules/luau-rfcs/behavior-eq-metamethod.md diff --git a/.cursor/rules/luau/behavior-stricter-utf8-library.md b/.cursor/rules/luau-rfcs/behavior-stricter-utf8-library.md similarity index 100% rename from .cursor/rules/luau/behavior-stricter-utf8-library.md rename to .cursor/rules/luau-rfcs/behavior-stricter-utf8-library.md diff --git a/.cursor/rules/luau/change-global-version.md b/.cursor/rules/luau-rfcs/change-global-version.md similarity index 100% rename from .cursor/rules/luau/change-global-version.md rename to .cursor/rules/luau-rfcs/change-global-version.md diff --git a/.cursor/rules/luau/config-luauconfig.md b/.cursor/rules/luau-rfcs/config-luauconfig.md similarity index 100% rename from .cursor/rules/luau/config-luauconfig.md rename to .cursor/rules/luau-rfcs/config-luauconfig.md diff --git a/.cursor/rules/luau/config-luaurc.md b/.cursor/rules/luau-rfcs/config-luaurc.md similarity index 100% rename from .cursor/rules/luau/config-luaurc.md rename to .cursor/rules/luau-rfcs/config-luaurc.md diff --git a/.cursor/rules/luau/deprecate-getfenv-setfenv.md b/.cursor/rules/luau-rfcs/deprecate-getfenv-setfenv.md similarity index 100% rename from .cursor/rules/luau/deprecate-getfenv-setfenv.md rename to .cursor/rules/luau-rfcs/deprecate-getfenv-setfenv.md diff --git a/.cursor/rules/luau/deprecate-table-getn-foreach.md b/.cursor/rules/luau-rfcs/deprecate-table-getn-foreach.md similarity index 100% rename from .cursor/rules/luau/deprecate-table-getn-foreach.md rename to .cursor/rules/luau-rfcs/deprecate-table-getn-foreach.md diff --git a/.cursor/rules/luau/disallow-proposals-leading-to-ambiguity-in-grammar.md b/.cursor/rules/luau-rfcs/disallow-proposals-leading-to-ambiguity-in-grammar.md similarity index 100% rename from .cursor/rules/luau/disallow-proposals-leading-to-ambiguity-in-grammar.md rename to .cursor/rules/luau-rfcs/disallow-proposals-leading-to-ambiguity-in-grammar.md diff --git a/.cursor/rules/luau/explicit-type-parameter-instantiation.md b/.cursor/rules/luau-rfcs/explicit-type-parameter-instantiation.md similarity index 100% rename from .cursor/rules/luau/explicit-type-parameter-instantiation.md rename to .cursor/rules/luau-rfcs/explicit-type-parameter-instantiation.md diff --git a/.cursor/rules/luau/function-bit32-byteswap.md b/.cursor/rules/luau-rfcs/function-bit32-byteswap.md similarity index 100% rename from .cursor/rules/luau/function-bit32-byteswap.md rename to .cursor/rules/luau-rfcs/function-bit32-byteswap.md diff --git a/.cursor/rules/luau/function-bit32-countlz-countrz.md b/.cursor/rules/luau-rfcs/function-bit32-countlz-countrz.md similarity index 100% rename from .cursor/rules/luau/function-bit32-countlz-countrz.md rename to .cursor/rules/luau-rfcs/function-bit32-countlz-countrz.md diff --git a/.cursor/rules/luau/function-buffer-bits.md b/.cursor/rules/luau-rfcs/function-buffer-bits.md similarity index 100% rename from .cursor/rules/luau/function-buffer-bits.md rename to .cursor/rules/luau-rfcs/function-buffer-bits.md diff --git a/.cursor/rules/luau/function-coroutine-close.md b/.cursor/rules/luau-rfcs/function-coroutine-close.md similarity index 100% rename from .cursor/rules/luau/function-coroutine-close.md rename to .cursor/rules/luau-rfcs/function-coroutine-close.md diff --git a/.cursor/rules/luau/function-debug-info.md b/.cursor/rules/luau-rfcs/function-debug-info.md similarity index 100% rename from .cursor/rules/luau/function-debug-info.md rename to .cursor/rules/luau-rfcs/function-debug-info.md diff --git a/.cursor/rules/luau/function-inlining.md b/.cursor/rules/luau-rfcs/function-inlining.md similarity index 100% rename from .cursor/rules/luau/function-inlining.md rename to .cursor/rules/luau-rfcs/function-inlining.md diff --git a/.cursor/rules/luau/function-math-lerp.md b/.cursor/rules/luau-rfcs/function-math-lerp.md similarity index 100% rename from .cursor/rules/luau/function-math-lerp.md rename to .cursor/rules/luau-rfcs/function-math-lerp.md diff --git a/.cursor/rules/luau/function-math-map.md b/.cursor/rules/luau-rfcs/function-math-map.md similarity index 100% rename from .cursor/rules/luau/function-math-map.md rename to .cursor/rules/luau-rfcs/function-math-map.md diff --git a/.cursor/rules/luau/function-string-pack-unpack.md b/.cursor/rules/luau-rfcs/function-string-pack-unpack.md similarity index 100% rename from .cursor/rules/luau/function-string-pack-unpack.md rename to .cursor/rules/luau-rfcs/function-string-pack-unpack.md diff --git a/.cursor/rules/luau/function-table-clear.md b/.cursor/rules/luau-rfcs/function-table-clear.md similarity index 100% rename from .cursor/rules/luau/function-table-clear.md rename to .cursor/rules/luau-rfcs/function-table-clear.md diff --git a/.cursor/rules/luau/function-table-clone.md b/.cursor/rules/luau-rfcs/function-table-clone.md similarity index 100% rename from .cursor/rules/luau/function-table-clone.md rename to .cursor/rules/luau-rfcs/function-table-clone.md diff --git a/.cursor/rules/luau/function-table-create-find.md b/.cursor/rules/luau-rfcs/function-table-create-find.md similarity index 100% rename from .cursor/rules/luau/function-table-create-find.md rename to .cursor/rules/luau-rfcs/function-table-create-find.md diff --git a/.cursor/rules/luau/function-table-freeze.md b/.cursor/rules/luau-rfcs/function-table-freeze.md similarity index 100% rename from .cursor/rules/luau/function-table-freeze.md rename to .cursor/rules/luau-rfcs/function-table-freeze.md diff --git a/.cursor/rules/luau/function-vector-lerp.md b/.cursor/rules/luau-rfcs/function-vector-lerp.md similarity index 100% rename from .cursor/rules/luau/function-vector-lerp.md rename to .cursor/rules/luau-rfcs/function-vector-lerp.md diff --git a/.cursor/rules/luau/generalized-iteration.md b/.cursor/rules/luau-rfcs/generalized-iteration.md similarity index 100% rename from .cursor/rules/luau/generalized-iteration.md rename to .cursor/rules/luau-rfcs/generalized-iteration.md diff --git a/.cursor/rules/luau/generic-function-subtyping.md b/.cursor/rules/luau-rfcs/generic-function-subtyping.md similarity index 100% rename from .cursor/rules/luau/generic-function-subtyping.md rename to .cursor/rules/luau-rfcs/generic-function-subtyping.md diff --git a/.cursor/rules/luau/generic-functions.md b/.cursor/rules/luau-rfcs/generic-functions.md similarity index 100% rename from .cursor/rules/luau/generic-functions.md rename to .cursor/rules/luau-rfcs/generic-functions.md diff --git a/.cursor/rules/luau/index-type-operator.md b/.cursor/rules/luau-rfcs/index-type-operator.md similarity index 100% rename from .cursor/rules/luau/index-type-operator.md rename to .cursor/rules/luau-rfcs/index-type-operator.md diff --git a/.cursor/rules/luau/index.md b/.cursor/rules/luau-rfcs/index.md similarity index 100% rename from .cursor/rules/luau/index.md rename to .cursor/rules/luau-rfcs/index.md diff --git a/.cursor/rules/luau/keyof-type-operator.md b/.cursor/rules/luau-rfcs/keyof-type-operator.md similarity index 100% rename from .cursor/rules/luau/keyof-type-operator.md rename to .cursor/rules/luau-rfcs/keyof-type-operator.md diff --git a/.cursor/rules/luau/len-metamethod-rawlen.md b/.cursor/rules/luau-rfcs/len-metamethod-rawlen.md similarity index 100% rename from .cursor/rules/luau/len-metamethod-rawlen.md rename to .cursor/rules/luau-rfcs/len-metamethod-rawlen.md diff --git a/.cursor/rules/luau/local-type-inference.md b/.cursor/rules/luau-rfcs/local-type-inference.md similarity index 100% rename from .cursor/rules/luau/local-type-inference.md rename to .cursor/rules/luau-rfcs/local-type-inference.md diff --git a/.cursor/rules/luau/lower-bounds-calculation.md b/.cursor/rules/luau-rfcs/lower-bounds-calculation.md similarity index 100% rename from .cursor/rules/luau/lower-bounds-calculation.md rename to .cursor/rules/luau-rfcs/lower-bounds-calculation.md diff --git a/.cursor/rules/luau/luau-mascot.md b/.cursor/rules/luau-rfcs/luau-mascot.md similarity index 100% rename from .cursor/rules/luau/luau-mascot.md rename to .cursor/rules/luau-rfcs/luau-mascot.md diff --git a/.cursor/rules/luau/mascot.png b/.cursor/rules/luau-rfcs/mascot.png similarity index 100% rename from .cursor/rules/luau/mascot.png rename to .cursor/rules/luau-rfcs/mascot.png diff --git a/.cursor/rules/luau/math-constants.md b/.cursor/rules/luau-rfcs/math-constants.md similarity index 100% rename from .cursor/rules/luau/math-constants.md rename to .cursor/rules/luau-rfcs/math-constants.md diff --git a/.cursor/rules/luau/math-isnan-isfinite-isinf.md b/.cursor/rules/luau-rfcs/math-isnan-isfinite-isinf.md similarity index 100% rename from .cursor/rules/luau/math-isnan-isfinite-isinf.md rename to .cursor/rules/luau-rfcs/math-isnan-isfinite-isinf.md diff --git a/.cursor/rules/luau/metatable-type-functions.md b/.cursor/rules/luau-rfcs/metatable-type-functions.md similarity index 100% rename from .cursor/rules/luau/metatable-type-functions.md rename to .cursor/rules/luau-rfcs/metatable-type-functions.md diff --git a/.cursor/rules/luau/method-type-issubtypeof.md b/.cursor/rules/luau-rfcs/method-type-issubtypeof.md similarity index 100% rename from .cursor/rules/luau/method-type-issubtypeof.md rename to .cursor/rules/luau-rfcs/method-type-issubtypeof.md diff --git a/.cursor/rules/luau/negation-types.md b/.cursor/rules/luau-rfcs/negation-types.md similarity index 100% rename from .cursor/rules/luau/negation-types.md rename to .cursor/rules/luau-rfcs/negation-types.md diff --git a/.cursor/rules/luau/never-and-unknown-types.md b/.cursor/rules/luau-rfcs/never-and-unknown-types.md similarity index 100% rename from .cursor/rules/luau/never-and-unknown-types.md rename to .cursor/rules/luau-rfcs/never-and-unknown-types.md diff --git a/.cursor/rules/luau/new-nonstrict.md b/.cursor/rules/luau-rfcs/new-nonstrict.md similarity index 100% rename from .cursor/rules/luau/new-nonstrict.md rename to .cursor/rules/luau-rfcs/new-nonstrict.md diff --git a/.cursor/rules/luau/new-require-by-string-semantics.md b/.cursor/rules/luau-rfcs/new-require-by-string-semantics.md similarity index 100% rename from .cursor/rules/luau/new-require-by-string-semantics.md rename to .cursor/rules/luau-rfcs/new-require-by-string-semantics.md diff --git a/.cursor/rules/luau/property-readonly.md b/.cursor/rules/luau-rfcs/property-readonly.md similarity index 100% rename from .cursor/rules/luau/property-readonly.md rename to .cursor/rules/luau-rfcs/property-readonly.md diff --git a/.cursor/rules/luau/property-writeonly.md b/.cursor/rules/luau-rfcs/property-writeonly.md similarity index 100% rename from .cursor/rules/luau/property-writeonly.md rename to .cursor/rules/luau-rfcs/property-writeonly.md diff --git a/.cursor/rules/luau/rawget-type-operator.md b/.cursor/rules/luau-rfcs/rawget-type-operator.md similarity index 100% rename from .cursor/rules/luau/rawget-type-operator.md rename to .cursor/rules/luau-rfcs/rawget-type-operator.md diff --git a/.cursor/rules/luau/recursive-type-restriction.md b/.cursor/rules/luau-rfcs/recursive-type-restriction.md similarity index 100% rename from .cursor/rules/luau/recursive-type-restriction.md rename to .cursor/rules/luau-rfcs/recursive-type-restriction.md diff --git a/.cursor/rules/luau/relax-recursive-type-restriction.md b/.cursor/rules/luau-rfcs/relax-recursive-type-restriction.md similarity index 100% rename from .cursor/rules/luau/relax-recursive-type-restriction.md rename to .cursor/rules/luau-rfcs/relax-recursive-type-restriction.md diff --git a/.cursor/rules/luau/require-by-string-aliases.md b/.cursor/rules/luau-rfcs/require-by-string-aliases.md similarity index 100% rename from .cursor/rules/luau/require-by-string-aliases.md rename to .cursor/rules/luau-rfcs/require-by-string-aliases.md diff --git a/.cursor/rules/luau/reserve-dollar-sign.md b/.cursor/rules/luau-rfcs/reserve-dollar-sign.md similarity index 100% rename from .cursor/rules/luau/reserve-dollar-sign.md rename to .cursor/rules/luau-rfcs/reserve-dollar-sign.md diff --git a/.cursor/rules/luau/sealed-table-subtyping.md b/.cursor/rules/luau-rfcs/sealed-table-subtyping.md similarity index 100% rename from .cursor/rules/luau/sealed-table-subtyping.md rename to .cursor/rules/luau-rfcs/sealed-table-subtyping.md diff --git a/.cursor/rules/luau/shared-self-types.md b/.cursor/rules/luau-rfcs/shared-self-types.md similarity index 100% rename from .cursor/rules/luau/shared-self-types.md rename to .cursor/rules/luau-rfcs/shared-self-types.md diff --git a/.cursor/rules/luau/support-for-generic-function-types-in-user-defined-type-functions.md b/.cursor/rules/luau-rfcs/support-for-generic-function-types-in-user-defined-type-functions.md similarity index 100% rename from .cursor/rules/luau/support-for-generic-function-types-in-user-defined-type-functions.md rename to .cursor/rules/luau-rfcs/support-for-generic-function-types-in-user-defined-type-functions.md diff --git a/.cursor/rules/luau/support-for-thread-and-buffer-types-in-user-defined-type-functions.md b/.cursor/rules/luau-rfcs/support-for-thread-and-buffer-types-in-user-defined-type-functions.md similarity index 100% rename from .cursor/rules/luau/support-for-thread-and-buffer-types-in-user-defined-type-functions.md rename to .cursor/rules/luau-rfcs/support-for-thread-and-buffer-types-in-user-defined-type-functions.md diff --git a/.cursor/rules/luau/syntax-array-like-table-types.md b/.cursor/rules/luau-rfcs/syntax-array-like-table-types.md similarity index 100% rename from .cursor/rules/luau/syntax-array-like-table-types.md rename to .cursor/rules/luau-rfcs/syntax-array-like-table-types.md diff --git a/.cursor/rules/luau/syntax-attribute-functions-deprecated.md b/.cursor/rules/luau-rfcs/syntax-attribute-functions-deprecated.md similarity index 100% rename from .cursor/rules/luau/syntax-attribute-functions-deprecated.md rename to .cursor/rules/luau-rfcs/syntax-attribute-functions-deprecated.md diff --git a/.cursor/rules/luau/syntax-attribute-functions-native.md b/.cursor/rules/luau-rfcs/syntax-attribute-functions-native.md similarity index 100% rename from .cursor/rules/luau/syntax-attribute-functions-native.md rename to .cursor/rules/luau-rfcs/syntax-attribute-functions-native.md diff --git a/.cursor/rules/luau/syntax-attributes-functions-parameters.md b/.cursor/rules/luau-rfcs/syntax-attributes-functions-parameters.md similarity index 100% rename from .cursor/rules/luau/syntax-attributes-functions-parameters.md rename to .cursor/rules/luau-rfcs/syntax-attributes-functions-parameters.md diff --git a/.cursor/rules/luau/syntax-attributes-functions.md b/.cursor/rules/luau-rfcs/syntax-attributes-functions.md similarity index 100% rename from .cursor/rules/luau/syntax-attributes-functions.md rename to .cursor/rules/luau-rfcs/syntax-attributes-functions.md diff --git a/.cursor/rules/luau/syntax-compound-assignment.md b/.cursor/rules/luau-rfcs/syntax-compound-assignment.md similarity index 100% rename from .cursor/rules/luau/syntax-compound-assignment.md rename to .cursor/rules/luau-rfcs/syntax-compound-assignment.md diff --git a/.cursor/rules/luau/syntax-continue-statement.md b/.cursor/rules/luau-rfcs/syntax-continue-statement.md similarity index 100% rename from .cursor/rules/luau/syntax-continue-statement.md rename to .cursor/rules/luau-rfcs/syntax-continue-statement.md diff --git a/.cursor/rules/luau/syntax-default-type-alias-type-parameters.md b/.cursor/rules/luau-rfcs/syntax-default-type-alias-type-parameters.md similarity index 100% rename from .cursor/rules/luau/syntax-default-type-alias-type-parameters.md rename to .cursor/rules/luau-rfcs/syntax-default-type-alias-type-parameters.md diff --git a/.cursor/rules/luau/syntax-floor-division-operator.md b/.cursor/rules/luau-rfcs/syntax-floor-division-operator.md similarity index 100% rename from .cursor/rules/luau/syntax-floor-division-operator.md rename to .cursor/rules/luau-rfcs/syntax-floor-division-operator.md diff --git a/.cursor/rules/luau/syntax-if-expression.md b/.cursor/rules/luau-rfcs/syntax-if-expression.md similarity index 100% rename from .cursor/rules/luau/syntax-if-expression.md rename to .cursor/rules/luau-rfcs/syntax-if-expression.md diff --git a/.cursor/rules/luau/syntax-leading-bar-and-ampersand.md b/.cursor/rules/luau-rfcs/syntax-leading-bar-and-ampersand.md similarity index 100% rename from .cursor/rules/luau/syntax-leading-bar-and-ampersand.md rename to .cursor/rules/luau-rfcs/syntax-leading-bar-and-ampersand.md diff --git a/.cursor/rules/luau/syntax-named-function-type-args.md b/.cursor/rules/luau-rfcs/syntax-named-function-type-args.md similarity index 100% rename from .cursor/rules/luau/syntax-named-function-type-args.md rename to .cursor/rules/luau-rfcs/syntax-named-function-type-args.md diff --git a/.cursor/rules/luau/syntax-number-literals.md b/.cursor/rules/luau-rfcs/syntax-number-literals.md similarity index 100% rename from .cursor/rules/luau/syntax-number-literals.md rename to .cursor/rules/luau-rfcs/syntax-number-literals.md diff --git a/.cursor/rules/luau/syntax-property-access-modifiers.md b/.cursor/rules/luau-rfcs/syntax-property-access-modifiers.md similarity index 100% rename from .cursor/rules/luau/syntax-property-access-modifiers.md rename to .cursor/rules/luau-rfcs/syntax-property-access-modifiers.md diff --git a/.cursor/rules/luau/syntax-singleton-types.md b/.cursor/rules/luau-rfcs/syntax-singleton-types.md similarity index 100% rename from .cursor/rules/luau/syntax-singleton-types.md rename to .cursor/rules/luau-rfcs/syntax-singleton-types.md diff --git a/.cursor/rules/luau/syntax-string-interpolation.md b/.cursor/rules/luau-rfcs/syntax-string-interpolation.md similarity index 100% rename from .cursor/rules/luau/syntax-string-interpolation.md rename to .cursor/rules/luau-rfcs/syntax-string-interpolation.md diff --git a/.cursor/rules/luau/syntax-type-alias-type-packs.md b/.cursor/rules/luau-rfcs/syntax-type-alias-type-packs.md similarity index 100% rename from .cursor/rules/luau/syntax-type-alias-type-packs.md rename to .cursor/rules/luau-rfcs/syntax-type-alias-type-packs.md diff --git a/.cursor/rules/luau/syntax-type-ascription-bidi.md b/.cursor/rules/luau-rfcs/syntax-type-ascription-bidi.md similarity index 100% rename from .cursor/rules/luau/syntax-type-ascription-bidi.md rename to .cursor/rules/luau-rfcs/syntax-type-ascription-bidi.md diff --git a/.cursor/rules/luau/syntax-type-ascription.md b/.cursor/rules/luau-rfcs/syntax-type-ascription.md similarity index 100% rename from .cursor/rules/luau/syntax-type-ascription.md rename to .cursor/rules/luau-rfcs/syntax-type-ascription.md diff --git a/.cursor/rules/luau/syntax-typed-variadics.md b/.cursor/rules/luau-rfcs/syntax-typed-variadics.md similarity index 100% rename from .cursor/rules/luau/syntax-typed-variadics.md rename to .cursor/rules/luau-rfcs/syntax-typed-variadics.md diff --git a/.cursor/rules/luau/type-ascription-by-inhabitance.md b/.cursor/rules/luau-rfcs/type-ascription-by-inhabitance.md similarity index 100% rename from .cursor/rules/luau/type-ascription-by-inhabitance.md rename to .cursor/rules/luau-rfcs/type-ascription-by-inhabitance.md diff --git a/.cursor/rules/luau/type-byte-buffer.md b/.cursor/rules/luau-rfcs/type-byte-buffer.md similarity index 100% rename from .cursor/rules/luau/type-byte-buffer.md rename to .cursor/rules/luau-rfcs/type-byte-buffer.md diff --git a/.cursor/rules/luau/type-error-suppression.md b/.cursor/rules/luau-rfcs/type-error-suppression.md similarity index 100% rename from .cursor/rules/luau/type-error-suppression.md rename to .cursor/rules/luau-rfcs/type-error-suppression.md diff --git a/.cursor/rules/luau/type-long-integer.md b/.cursor/rules/luau-rfcs/type-long-integer.md similarity index 67% rename from .cursor/rules/luau/type-long-integer.md rename to .cursor/rules/luau-rfcs/type-long-integer.md index a9b6ae8e..de64d62c 100644 --- a/.cursor/rules/luau/type-long-integer.md +++ b/.cursor/rules/luau-rfcs/type-long-integer.md @@ -6,11 +6,11 @@ A builtin 'integer' type to represent 64 bit integer numbers. ## Motivation -Current methods of representing 64-bit integers in Luau with default environment are done with high-low pairs or by splitting them among vectors which leads to poor ergonomics and may not be sufficient for cases where performance is important. +Current methods of representing 64-bit integers in Luau with the default environment are done with high-low pairs or by splitting them among vectors which leads to poor ergonomics and may not be sufficient for cases where performance is important. -In the case of a high-low pair implementation, each individual number takes 16 bytes (or 24) and needs to be be handled together which can be confusing. +In the case of a high-low pair implementation, each individual number takes 16 bytes (or 24) and needs to be handled together which can be confusing. -In the case of vector implementations, ergonomics are improved by storing the entire integer in one value however you have the same issues of implementation complexity and lack of integration with the type system. +In the case of vector implementations, ergonomics are improved by storing the entire integer in one value. However you have the same issues of implementation complexity and lack of integration with the type system. In both cases, performance is lacking compared to what could be provided by a native implementation. @@ -21,11 +21,11 @@ Besides the above solutions in userland, runtimes are also able to define their Further, representing 64-bit numbers as heap-allocated userdata is sufficient for correctness, but also adds a layer of indirection for something that could fit within the existing value size. In domains where the use of these numbers is more common, the performance characteristics of these varying implementation techniques could be problematic or even prohibitive. -Userdata remain the right mechanism for complex or embedder-specific numeric types, but 64-bit integers have a sufficiently large problem domain to justify their inclusion as a core value type, and bring significant benefits for performance and interoperability in the overall Luau ecosystem. +Userdata remains the right mechanism for complex or embedder-specific numeric types, but 64-bit integers have a sufficiently large problem domain to justify their inclusion as a core value type, and bring significant benefits for performance and interoperability in the overall Luau ecosystem. ## Design -This will be implemented a with new type called `integer`. +This will be implemented with a new type called `integer`. An additional character may be specified at the end of numeric literals `i` which will signify an 64 bit integer literal. 64 bit integer literals will support separators, hex, and binary values: @@ -43,10 +43,18 @@ Operations performing a string formatting of an integer should format the number Integer values will have a built-in equality comparison, but will not have any other operators or metamethods defined. +Integers are never automatically converted to numbers or strings, and vice-versa. +Passing an integer to a function expecting a number (or string) will result in a type error. + +While there are no arithmentic operators defined for integers in this RFC, it is still worth noting that operations between `number` and `integer` are also not supported. + Functions for creating and manipulating this type will exist in a new library called 'integer`. ### Library +Unless otherwise specified, all operations interpret integer values as signed two's complement 64 bit numbers. +Operations that treat the value as unsigned are explicitly prefixed with 'u'. + `function integer.create(n: number): integer?` Converts a number to an integer. @@ -64,7 +72,7 @@ String is allowed to have leading and trailing spaces and number can be preceded If base is not specified, number can have a `0x` or `0X` prefix to be converted in base 16, otherwise base 10 is used. When base is specified, it has to be in range between 2 and 36 inclusive. -In base 10 and base 16, number is allowed to have a `0x` or `0X` prefix. +In base 10 and base 16, numbers are allowed to have a `0x` or `0X` prefix. Returns `nil` if the string doesn't contain a number. @@ -102,7 +110,7 @@ Overflow wraps around according to rules of two-complement arithmetic. `function integer.div(a: integer, b: integer): integer` -Performes signed truncated division of `a` by `b`. +Performs signed truncated division of `a` by `b`. If `b` is 0, throws a division by zero error. @@ -118,7 +126,7 @@ If `a` is -2^63 and `b` is -1, result is 0. `function integer.idiv(a: integer, b: integer): integer` -Performes signed floored division of `a` by `b`. +Performs signed floored division of `a` by `b`. If `b` is 0, throws a division by zero error. @@ -126,7 +134,7 @@ If `a` is -2^63 and `b` is -1, throws an overflow error. `function integer.mod(a: integer, b: integer): integer` -Performes signed floored modulus division of `a` by `b`. +Performs signed floored modulus division of `a` by `b`. If `b` is 0, throws a division by zero error. @@ -293,9 +301,9 @@ Integer value representing -2^63 (`-9_223_372_036_854_775_808i`) `string.format` function is updated to support integer arguments. -'d', 'i' and '*' format specifiers will format integer as a signed 64 bit integer number. +'d', 'i' and '*' format specifiers will format an integer as a signed 64 bit integer number. -'o', 'u', 'x' and 'X' format specifiers will format integer as an unsigned 64 bit integer number. +'o', 'u', 'x' and 'X' format specifiers will format an integer as an unsigned 64 bit integer number. ### C API @@ -307,7 +315,7 @@ If `isinteger` is not a null pointer, writes 1 if the value at `idx` was an inte `void lua_pushinteger64(lua_State *L, int64_t n)` -Pushes a integer value on top of the stack. +Pushes an integer value on top of the stack. `int lua_isinteger64(lua_State *L, int idx)` @@ -332,8 +340,54 @@ Included in `luaL_openlibs`. ## Drawbacks -This increases the complexity of the VM as it may need to implement separate paths for mathematical operations. +This introduces a library with a large set of functions which will likely take a large number of fastcall slots. + +The lack of operator overloads causes code with integers to be more verbose, decreasing readability in complex expressions. + +Introduction of a new type will require updates to libraries (serialization, input/output) which try to handle all potential Luau values. + +The suffix 'i' is standard mathematical notation for imaginary numbers. +Adopting it for integers will likely block proposals for a built-in `complex` number type in the future. ## Alternatives Do nothing, the workarounds exist and are in use but this will restrict the areas Luau can be used without implementation burden or reliance on an externally maintained implementation of 64-bit integers. + +### Operator overloads + +This proposal does not introduce overloaded operators aside from equality checks. +This omission is deliberate. + +While the lack of operators makes code more verbose, Luau's primary numeric type remains `number`. +The `integer` type is intended to cover specialized use cases, many of which will specifically prefer explicit library functions over operators for performance. + +Polymorphic overloaded operators present many challenges and can cause unexpected drops of performance. +This is already true when working with `vector` types without type annotations and given the focus of this proposal, we do not want to provide additional pitfalls. +Monomorphic library functions allow compiler and runtime to optimize more aggressively. + +Heavy uses of 64-bit integers often rely on bitwise operations. +Since Luau does not have bitwise operators in its syntax, even with a subset of overloaded operators in place, code will still end up using library functions. + +Integer division has multiple conflicting definitions (truncated vs. floored). +Operators like `/` and `%` would force a specific default behavior that might not match the specific rounding that the user intended. +Default behavior might also be less performant, causing developers to still rely on library methods. + +Having different library methods allows the developers to choose the specific operation semantics they require. + +### Separate signed and unsigned types + +It has been suggested to create two separate types for signed and unsigned integers or have it be a property of the value. +Having distinct types would provide the typechecker with the ability to highlight mixed operations and a runtime check could throw an error. + +As mentioned above, integers are already not expected to be the default value type to be chosen by developers to work with. +Unsigned integer use is even rarer and other programming languages followed the similar path of only supporting auxiliary operations on them. +Other languages with unsigned integer support like C++ recommend limiting their use in their guidelines. +Given this, we have rejected the idea of distinct signed and unsigned integer types. + +Having two distinct integer types increases the API and library surface area and implementation complexity significantly for little gain. + +In two's complement representation, a single data type with default signed interpretation covers many signed and unsigned operations in an identical way. +Only a limited subset of operations interpreting data as unsigned is added in this proposal to cover most needs for unsigned integer numbers. + +Having library methods instead of operators allows the developer to make an explicit choice of the interpretation required (e.g. `lt` vs `ult`). +Reinterpretation of a negative value as a positive unsigned value can be made deliberately by the choice of function, and is not as error-prone as using an operator. diff --git a/.cursor/rules/luau/types-library-optional.md b/.cursor/rules/luau-rfcs/types-library-optional.md similarity index 100% rename from .cursor/rules/luau/types-library-optional.md rename to .cursor/rules/luau-rfcs/types-library-optional.md diff --git a/.cursor/rules/luau/udtf-is-extern.md b/.cursor/rules/luau-rfcs/udtf-is-extern.md similarity index 100% rename from .cursor/rules/luau/udtf-is-extern.md rename to .cursor/rules/luau-rfcs/udtf-is-extern.md diff --git a/.cursor/rules/luau/unsealed-table-assign-optional-property.md b/.cursor/rules/luau-rfcs/unsealed-table-assign-optional-property.md similarity index 100% rename from .cursor/rules/luau/unsealed-table-assign-optional-property.md rename to .cursor/rules/luau-rfcs/unsealed-table-assign-optional-property.md diff --git a/.cursor/rules/luau/unsealed-table-literals.md b/.cursor/rules/luau-rfcs/unsealed-table-literals.md similarity index 100% rename from .cursor/rules/luau/unsealed-table-literals.md rename to .cursor/rules/luau-rfcs/unsealed-table-literals.md diff --git a/.cursor/rules/luau/unsealed-table-subtyping-strips-optional-properties.md b/.cursor/rules/luau-rfcs/unsealed-table-subtyping-strips-optional-properties.md similarity index 100% rename from .cursor/rules/luau/unsealed-table-subtyping-strips-optional-properties.md rename to .cursor/rules/luau-rfcs/unsealed-table-subtyping-strips-optional-properties.md diff --git a/.cursor/rules/luau/user-defined-type-functions.md b/.cursor/rules/luau-rfcs/user-defined-type-functions.md similarity index 100% rename from .cursor/rules/luau/user-defined-type-functions.md rename to .cursor/rules/luau-rfcs/user-defined-type-functions.md diff --git a/.cursor/rules/luau/vector-library-vector2-constructor.md b/.cursor/rules/luau-rfcs/vector-library-vector2-constructor.md similarity index 100% rename from .cursor/rules/luau/vector-library-vector2-constructor.md rename to .cursor/rules/luau-rfcs/vector-library-vector2-constructor.md diff --git a/.cursor/rules/luau/vector-library.md b/.cursor/rules/luau-rfcs/vector-library.md similarity index 100% rename from .cursor/rules/luau/vector-library.md rename to .cursor/rules/luau-rfcs/vector-library.md diff --git a/.cursor/rules/luau/getting-started/compatibility.md b/.cursor/rules/luau/getting-started/compatibility.md new file mode 100644 index 00000000..b48489bc --- /dev/null +++ b/.cursor/rules/luau/getting-started/compatibility.md @@ -0,0 +1,133 @@ +--- +slug: compatibility +title: Compatibility with Lua +toc: true +--- + +Luau is based on Lua 5.1, and as such incorporates all features of 5.1, except for ones that had to be taken out due to sandboxing limitations. Because of backwards compatibility constraints, we don't remove features deprecated by later versions (e.g. we still support `getfenv`/`setfenv`). Later Lua versions introduce new features into the language and new libraries/functions. + +Our overall goal is to incorporate features from the later versions of Lua when it makes sense for us to do so - the motivations behind some newer features are unclear or don't apply to the domain Luau is used in, and many features carry costs that don't always make sense to pay. The rest of this document describes the status of all features of Lua 5.2 and beyond, with the following classification: + +- ✔️ - the feature is available in Luau +- ❌ - the feature is not available in Luau because we don't believe it makes sense to include it +- 😞 - the feature is not available in Luau because of compatibility/sandboxing concerns +- 🔜 - the feature is not available in Luau yet but we'd like to include it and are possibly working on it +- 🤷‍♀️ - the feature is not available in Luau yet; we don't have strong opinions on it so it might make it at some point + +Please note that all of these decisions are not final, they are just our current stance. In some cases evolution of our VM may make a feature that was previously impractical to support due to performance complications feasible. In some cases a feature that didn't have a strong use case gains one, so we can implement it. + +## Implementation limits + +Luau has certain limitations around the number of local variables, registers, upvalues, constants and instructions. These limits are often different from the limits imposed by various versions of Lua, and are documented here without promising that future versions will adhere to these. Note that writing code that is close to any of these limits is dangerous because this code may become invalid as our codegen evolves. + +- Local variables: 200 per function (same as all versions of Lua, this includes function arguments) +- Upvalues: 200 per function (up from 60 in Lua 5.1) +- Registers: 255 per function (same as all versions of Lua, this includes local variables and function arguments) +- Constants: 2^23 per function (up from 2^18 in Lua 5.1) +- Instructions: 2^23 per function (up from 2^17 in Lua 5.1, although in both cases the limit only applies to control flow) +- Nested functions: 2^15 per function (down from 2^18 in Lua 5.1) +- Stack depth: 20000 Lua calls per Lua thread, 200 C calls per C thread (e.g. `coroutine.resume`/`pcall` nesting is limited to 200) + +Note that Lua 5.3 has a larger upvalue limit (255) and a larger constant limit (2^26); existing Luau limits are likely sufficient for reasonable use cases. + +## Lua 5.1 + +Since several features were removed from Lua 5.1 for sandboxing reasons, this table lists them for completeness. + +| feature | notes | +|---------|------| +| `io`, `os`, `package` and `debug` library | note that some functions in `os`/`debug` are still present | +| `loadfile`, `dofile` | removed for sandboxing, no direct file access | +| `loadstring` bytecode and `string.dump` | exposing bytecode is dangerous for sandboxing reasons | +| `newproxy` can only be called with nil or boolean | extra flexibility removed for sandboxing | + +Sandboxing challenges are [covered in the dedicated section](../sandbox). + +## Lua 5.2 + +| feature | status | notes | +|---------|--------|------| +| yieldable pcall/xpcall | ✔️ | | +| yieldable xpcall error handler | ❌ | no strong use cases, VM complexity and performance implications | +| yieldable metamethods | ❌ | significant performance implications | +| ephemeron tables | ❌ | this complicates and slows down the garbage collector esp. for large weak tables | +| emergency garbage collector | 🤷‍ | Luau runs in environments where handling memory exhaustion in emergency situations is not tenable | +| goto statement | ❌ | this complicates the compiler, makes control flow unstructured and doesn't address a significant need | +| finalizers for tables | ❌ | no `__gc` support due to sandboxing and performance/complexity | +| no more fenv for threads or functions | 😞 | we love this, but it breaks compatibility | +| tables honor the `__len` metamethod | ✔️ | | +| hex and `\z` escapes in strings | ✔️ | | +| support for hexadecimal floats | 🤷‍♀️ | no strong use cases | +| order metamethods (`__lt`/`__le`) are called for unrelated metatables | ❌ | no strong use cases and more complicated semantics, compatibility and performance implications | +| empty statement | 🤷‍♀️ | less useful in Lua than in JS/C#/C/C++ | +| `break` statement may appear in the middle of a block | 🤷‍♀️ | we'd like to do it consistently for `break`/`return`/`continue` but there be dragons | +| arguments for function called through `xpcall` | ✔️ | | +| optional base in `math.log` | ✔️ | | +| optional separator in `string.rep` | 🤷‍♀️ | no strong use cases | +| new metamethods `__pairs` and `__ipairs` | ❌ | superseded by `__iter` | +| frontier patterns | ✔️ | | +| `%g` in patterns | ✔️ | | +| `\0` in patterns | ✔️ | | +| `bit32` library | ✔️ | | +| `string.gsub` is stricter about using `%` on special characters only | ✔️ | | +| light C functions | 😞 | this changes semantics of fenv on C functions and has complex implications wrt runtime performance | +| NaN keys are supported for tables with `__newindex` | ✔️ | | + +Two things that are important to call out here are various new metamethods for tables and yielding in metamethods. In both cases, there are performance implications to supporting this - our implementation is *very* highly tuned for performance, so any changes that affect the core fundamentals of how Lua works have a price. To support yielding in metamethods we'd need to make the core of the VM more involved, since almost every single "interesting" opcode would need to learn how to be resumable - which also complicates future JIT/AOT story. Metamethods in general are important for extensibility, but very challenging to deal with in implementation, so we err on the side of not supporting any new metamethods unless a strong need arises. + +For `__pairs`/`__ipairs`, we felt that extending library functions to enable custom containers wasn't the right choice. Instead we revisited iteration design to allow for self-iterating objects via `__iter` metamethod, which results in a cleaner iteration design that also makes it easier to iterate over tables. As such, we have no plans to support `__pairs`/`__ipairs` as all use cases for it can now be solved by `__iter`. + +Ephemeron tables may be implemented at some point since they do have valid uses and they make weak tables semantically cleaner, however the cleanup mechanism for these is expensive and complicated, and as such this can only be considered after the pending GC rework is complete. + +## Lua 5.3 + +| feature | status | notes | +|---------|--------|------| +| `\u` escapes in strings | ✔️ | | +| integers (64-bit by default) | ❌ | backwards compatibility and performance implications | +| bitwise operators | ❌ | `bit32` library covers this in absence of 64-bit integers | +| basic utf-8 support | ✔️ | we include `utf8` library and other UTF8 features | +| functions for packing and unpacking values (string.pack/unpack/packsize) | ✔️ | | +| floor division | ✔️ | | +| `ipairs` and the `table` library respect metamethods | ❌ | no strong use cases, performance implications | +| new function `table.move` | ✔️ | | +| `collectgarbage("count")` now returns only one result | ✔️ | | +| `coroutine.isyieldable` | ✔️ | | +| stricter error checking for `table.insert`/`table.remove` | 😞 | we love this, but it breaks compatibility | +| `__eq` metamethod is called for unrelated metatables | ❌ | backwards compatibility and typechecking implications | + +It's important to highlight integer support and bitwise operators. For Luau, it's rare that a full 64-bit integer type is necessary - double-precision types support integers up to 2^53 (in Lua which is used in embedded space, integers may be more appealing in environments without a native 64-bit FPU). However, there's a *lot* of value in having a single number type, both from performance perspective and for consistency. Notably, Lua doesn't handle integer overflow properly, so using integers also carries compatibility implications. + +If integers are taken out of the equation, bitwise operators make less sense, as integers aren't a first class feature; additionally, `bit32` library is more fully featured (includes commonly used operations such as rotates and arithmetic shift; bit extraction/replacement is also more readable). Adding operators along with metamethods for all of them increases complexity, which means this feature isn't worth it on the balance. Common arguments for this include a more familiar syntax, which, while true, gets more nuanced as `^` isn't available as a xor operator, and arithmetic right shift isn't expressible without yet another operator, and performance, which in Luau is substantially better than in Lua because `bit32` library uses VM builtins instead of expensive function calls. + +## Lua 5.4 + +| feature | status | notes | +|--|--|--| +| new generational mode for garbage collection | 🔜 | we're working on gc optimizations and generational mode is on our radar +| to-be-closed variables | ❌ | the syntax is inconsistent with how we'd like to do attributes long-term; no strong use cases in our domain | +| const variables | ❌ | while there's some demand for const variables, we'd never adopt this syntax | +| new implementation for math.random | ✔️ | our RNG is based on PCG, unlike Lua 5.4 which uses Xoroshiro | +| optional `init` argument to `string.gmatch` | 🤷‍♀️ | no strong use cases | +| new functions `lua_resetthread` and `coroutine.close` | ✔️ || +| coercions string-to-number moved to the string library | 😞 | we love this, but it breaks compatibility | +| new format `%p` in `string.format` | 🤷‍♀️ | no strong use cases | +| `utf8` library accepts codepoints up to 2^31 | 🤷‍♀️ | no strong use cases | +| The use of the `__lt` metamethod to emulate `__le` has been removed | ❌ | breaks compatibility and complicates comparison overloading story | +| When finalizing objects, Lua will call `__gc` metamethods that are not functions | ❌ | no `__gc` support due to sandboxing and performance/complexity | +| function `print` calls `__tostring` instead of `tostring` to format its arguments | ✔️ | | +| decoding functions in the utf8 library do not accept surrogates | ✔️ | | + +Taking syntax aside (which doesn't feel idiomatic or beautiful), `` isn't very useful in Luau - its dominant use case is for code that works with external resources like files or sockets, but we don't provide such APIs - and has a very large complexity cost, evidences by a lot of bug fixes since the initial implementation in 5.4 work versions. `` in Luau doesn't matter for performance - our multi-pass compiler is already able to analyze the usage of the variable to know if it's modified or not and extract all performance gains from it - so the only use here is for code readability, where the `` syntax is... suboptimal. + +If we do end up introducing const variables, it would be through a `const var = value` syntax, which is backwards compatible through a context-sensitive keyword similar to `type`. That said, there's ambiguity wrt whether `const` should simply behave like a read-only variable, ala JavaScript, or if it should represent a stronger contract, for example by limiting the expressions on the right hand side to ones compiler can evaluate ahead of time, or by freezing table values and thus guaranteeing immutability. + +## Differences from Lua + +We have a few behavior deviations from Lua 5.x that come from either a different implementation, or our desire to clean up small inconsistencies in the language/libraries: + +* Tail calls are not supported to simplify implementation, make debugging/stack traces more predictable and allow deep validation of caller identity for security +* Order of table assignment in table literals follows program order in mixed tables (Lua 5.x assigns array elements first in some cases) +* Equality comparisons call `__eq` metamethod even when objects are rawequal (which matches other metamethods like `<=` and facilitates NaN checking) +* `function()` expressions may reuse a previously created closure in certain scenarios (when all upvalues captured are the same) for efficiency, which changes object identity but doesn't change call semantics -- this is different from Lua 5.1 but similar to Lua 5.2/5.3 +* `os.time` returns UTC timestamp when called with a table for consistency diff --git a/.cursor/rules/luau/getting-started/intro.md b/.cursor/rules/luau/getting-started/intro.md new file mode 100644 index 00000000..6e713cb0 --- /dev/null +++ b/.cursor/rules/luau/getting-started/intro.md @@ -0,0 +1,147 @@ +--- +slug: getting-started +title: An introduction to Luau +sidebar: + order: 1 +--- + +Luau is a fast, small, safe, gradually typed embeddable scripting language derived from Lua 5.1. Luau ships as a command line tool for running, analyzing, and linting your Luau scripts, and is also integrated with Roblox Studio. Roblox developers should also visit our [Creator Docs Luau Section](https://create.roblox.com/docs/luau). + +To get started with Luau, you can use the `luau` command line binary to run your code and `luau-analyze` to run static analysis (including type checking and linting). You can download these from [a recent release](https://github.com/luau-lang/luau/releases). + +## Creating a script + +To create your own testing script, create a new file with `.luau` as the extension: + +```luau +--!hidden mode=nonstrict +function ispositive(x) + return x > 0 +end + +print(ispositive(1)) +print(ispositive("2")) + +function isfoo(a) + return a == "foo" +end + +print(isfoo("bar")) +print(isfoo(1)) +``` + +You can now run the file using `luau test.luau` and analyze it using `luau-analyze test.luau`. + +Note that there are no warnings about calling ``ispositive()`` with a string, or calling ``isfoo()`` with a number. This is because Luau's type checking uses non-strict mode by default, which only reports errors if it's certain a program will error at runtime. + +## Type checking + +Now modify the script to include ``--!strict`` at the top: + +```luau +--!strict + +function ispositive(x) + return x > 0 +end + +print(ispositive(1)) +print(ispositive("2")) +``` + +We just mentioned ``nonstrict`` mode, where we only report errors if we can prove that a program will error at runtime. We also have ``strict`` mode, which will report errors if a program might error at runtime. + +In this case, Luau will use the ``return x > 0`` statement to infer that ``ispositive()`` is a function taking a number and returning a boolean. Note that in this case, we were able to determine the type of `ispositive` without the presence of any explicit type annotations. + +Based on Luau's type inference, the analysis tool will now flag the incorrect call to ``ispositive()``: + +``` +$ luau-analyze test.luau +test.luau(7,18): TypeError: Type 'string' could not be converted into 'number' +``` + +## Annotations + +You can add annotations to locals, arguments, and function return types. Among other things, annotations can help enforce that you don't accidentally do something silly. Here's how we would add annotations to ``ispositive()``: + +```luau +--!strict + +function ispositive(x : number) : boolean + return x > 0 +end + +local result : boolean +result = ispositive(1) + +``` + +Now we've explicitly told Luau that ``ispositive()`` accepts a number and returns a boolean. This wasn't strictly (pun intended) necessary in this case, because Luau's inference was able to deduce this already. But even in this case, there are advantages to explicit annotations. Imagine that later we decide to change ``ispositive()`` to return a string value: + +```luau +--!strict + +function ispositive(x : number) : boolean + if x > 0 then + return "yes" + else + return "no" + end +end + +local result : boolean +result = ispositive(1) +``` + +Oops -- we're returning string values, but we forgot to update the function return type. Since we've told Luau that ``ispositive()`` returns a boolean (and that's how we're using it), the call site isn't flagged as an error. But because the annotation doesn't match our code, we get a warning in the function body itself: + +``` +$ luau-analyze test.luau +test.luau(5,9): TypeError: Type 'string' could not be converted into 'boolean' +test.luau(7,9): TypeError: Type 'string' could not be converted into 'boolean' +``` + +The fix is simple; just change the annotation to declare the return type as a string: + +```luau +--!strict + +function ispositive(x : number) : string + if x > 0 then + return "yes" + else + return "no" + end +end + +local result : boolean +result = ispositive(1) +``` + +Well, almost - since we declared ``result`` as a boolean, the call site is now flagged: + +``` +$ luau-analyze test.luau +test.luau(12,10): TypeError: Type 'string' could not be converted into 'boolean' +``` + +If we update the type of the local variable, everything is good. Note that we could also just let Luau infer the type of ``result`` by changing it to the single line version ``local result = ispositive(1)``. + +```luau +--!strict + +function ispositive(x : number) : string + if x > 0 then + return "yes" + else + return "no" + end +end + +local result : string +result = ispositive(1) +``` + +## Conclusions + +This has been a brief tour of the basic functionality of Luau, but there's lots more to explore. If you're interested in reading more, check out our main reference pages for [syntax](/syntax) and [typechecking](/types). diff --git a/.cursor/rules/luau/getting-started/lint.md b/.cursor/rules/luau/getting-started/lint.md new file mode 100644 index 00000000..c9cfcbc8 --- /dev/null +++ b/.cursor/rules/luau/getting-started/lint.md @@ -0,0 +1,387 @@ +--- +slug: lint +title: Luau's linter +sidebar: + order: 4 +--- + +Luau comes with a set of linting passes, that help make sure that the code is correct and consistent. Unlike the type checker, that models the behavior of the code thoroughly and points toward type mismatches that are likely to result in runtime errors, the linter is more opinionated and produces warnings that can often be safely ignored, although it's recommended to keep the code clean of the warnings. + +Linter produces many different types of warnings; many of these are enabled by default, and can be suppressed by declaring `--!nolint NAME` at the top of the file. In dire situations `--!nolint` at the top of the file can be used to completely disable all warnings (note that the type checker is still active, and requires a separate `--!nocheck` declaration). + +The rest of this page documents all warnings produced by the linter; each warning has a name and a numeric code, the latter is used when displaying warnings. Note that the in-browser version of Luau on this website does not currently use the linter. + +## UnknownGlobal (1) + +By default, variables in Luau are global (this is inherited from Lua 5.x and can't be changed because of backwards compatibility). This means that typos in identifiers are invisible to the parser, and often break at runtime. For this reason, the linter considers all globals that aren't part of the builtin global table and aren't explicitly defined in the script "unknown": + +```luau +--!hidden mode=nocheck +local displayName = "Roblox" + +-- Unknown global 'displaName' +print(displaName) +``` + +Note that injecting globals via `setfenv` can produce this warning in correct code; global injection is incompatible with type checking and has performance implications so we recommend against it and in favor of using `require` with correctly scoped identifiers. + +## DeprecatedGlobal (2) + +Some global names exist for compatibility but their use is discouraged. This mostly affects globals introduced by Roblox, and since they can have problematic behavior or can break in the future, this warning highlights their uses: + +```luau +--!hidden mode=nocheck +-- Global 'ypcall' is deprecated, use 'pcall' instead +ypcall(function() + print("hello") +end) +``` + +## GlobalUsedAsLocal (3) + +The UnknownGlobal lint can catch typos in globals that are read, but can't catch them in globals that are assigned to. Because of this, and to discourage the use of globals in general, linter detects cases when a global is only used in one function and can be safely converted to a local variable. Note that in some cases this requires declaring the local variable in the beginning of the function instead of where it's being assigned to. + +```luau +--!hidden mode=nocheck +local function testFunc(a) + if a < 5 then + -- Global 'b' is only used in the enclosing function; consider changing it to local + b = 1 + else + b = 2 + end + print(b) +end +``` + +## LocalShadow (4) + +In Luau, it is valid to shadow locals and globals with a local variable, including doing it in the same function. This can result in subtle bugs, since the shadowing may not be obvious to the reader. This warning detects cases where local variables shadow other local variables in the same function, or global variables used in the script. + +```luau +--!hidden mode=nocheck +local function foo() + for i=1,10 do + -- Variable 'i' shadows previous declaration at line 2 + for i=1,10 do + print(i) + end + end +end +``` + +## SameLineStatement (5) + +Luau doesn't require the use of semicolons and doesn't automatically insert them at line breaks. When used wisely this results in code that is easy to read and understand, however it can cause subtle issues and hard to understand code when abused by using many different statements on the same line. This warning highlights cases where code should either be broken into multiple lines, or use `;` as a visual guide. + +```luau +--!hidden mode=nocheck +-- A new statement is on the same line; add semi-colon on previous statement to silence +if b < 0 then local a = b + 1 print(a, b) end +``` + +## MultiLineStatement (6) + +An opposite problem is having statements that span multiple lines. This is good for readability when the code is indented properly, but when it's not it results in code that's hard to understand, as its easy to confuse the next line for a separate statement. + +```luau +--!hidden mode=nocheck +-- Statement spans multiple lines; use indentation to silence +print(math.max(1, +math.min(2, 3))) +``` + +## LocalUnused (7) + +This warning is one of the few warnings that highlight unused variables. Local variable declarations that aren't used may indicate a bug in the code (for example, there could be a typo in the code that uses the wrong variable) or redundant code that is no longer necessary (for example, calling a function to get its result and never using this result). This warning warns about locals that aren't used; if the locals are not used intentionally they can be prefixed with `_` to silence the warning: + +```luau +--!hidden mode=nocheck +local x = 1 +-- Variable 'y' is never used; prefix with '_' to silence +local y = 2 +print(x, x) +``` + +## FunctionUnused (8) + +While unused local variables could be useful for debugging, unused functions usually contain dead code that was meant to be removed. Unused functions clutter code, can be a result of typos similar to local variables, and can mislead the reader into thinking that some functionality is supported. + +```luau +--!hidden mode=nocheck +-- Function 'bar' is never used; prefix with '_' to silence +local function bar() +end + +local function foo() +end + +return foo() +``` + +## ImportUnused (9) + +In Luau, there's no first-class module system that's part of the syntax, but `require` function acts as an import statement. When a local is initialized with a `require` result, and the local is unused, this is classified as "unused import". Removing unused imports improves code quality because it makes it obvious what the dependencies of the code are: + +```luau +--!hidden mode=nocheck +-- Import 'Roact' is never used; prefix with '_' to silence +local Roact = require("@game/Packages/Roact") +``` + +## BuiltinGlobalWrite (10) + +While the sandboxing model of Luau prevents overwriting built-in globals such as `table` for the entire program, it's still valid to reassign these globals - this results in "global shadowing", where the script's global table contains a custom version of `table` after writing to it. This is problematic because it disables some optimizations, and can result in misleading code. When shadowing built-in globals, use locals instead. + +```luau +-- Built-in global 'math' is overwritten here; consider using a local or changing the name +math = {} +``` + +## PlaceholderRead (11) + +`_` variable name is commonly used as a placeholder to discard function results. The linter follows this convention and doesn't warn about the use of `_` in various cases where a different name would cause a warning otherwise. To make sure the placeholder is only used to write values to it, this warning detects the cases where it's read instead: + +```luau +local _ = 5 +-- Placeholder value '_' is read here; consider using a named variable +return _ +``` + +## UnreachableCode (12) + +In some cases the linter can detect code that is never executed, because all execution paths through the function exit the function or the loop before reaching it. Such code is misleading because it's not always obvious to the reader that it never runs, and as such it should be removed. + +```luau +function cbrt(v) + if v >= 0 then + return v ^ (1/3) + else + error('cbrt expects a non-negative argument') + end + -- Unreachable code (previous statement always returns) + return 0 +end +``` + +## UnknownType (13) + +Luau provides several functions to get the value type as a string (`type`, `typeof`), and some Roblox APIs expose class names through string arguments (`Instance.new`). This warning detects incorrect use of the type names by checking the string literals used in type comparisons and function calls. + +```luau +--!hidden mode=nocheck +-- Unknown type 'String' (expected primitive type) +if type(v) == "String" then + print("v is a string") +end +``` + +## ForRange (14) + +When using a numeric for, it's possible to make various mistakes when specifying the for bounds. For example, to iterate through the table backwards, it's important to specify the negative step size. This warning detects several cases where the numeric for only runs for 0 or 1 iterations, or when the step doesn't divide the size of the range evenly. + +```luau +--!hidden mode=nocheck +-- For loop should iterate backwards; did you forget to specify -1 as step? +for i=#t,1 do +end +``` + +## UnbalancedAssignment (15) + +Assignment statements and local variable declarations in Luau support multiple variables on the left side and multiple values on the right side. The number of values doesn't need to match; when the right side has more values, the extra values are discarded, and then the left side has more variables the extra variables are set to `nil`. However, this can result in subtle bugs where a value is omitted mistakenly. This warning warns about cases like this; if the last expression on the right hand side returns multiple values, the warning is not emitted. + +```luau +-- Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence +local x, y, z = 1, 2 +``` + +## ImplicitReturn (16) + +In Luau, there's a subtle difference between returning no values from a function and returning `nil`. In many contexts these are equivalent, but when the results are passed to a variadic function (perhaps implicitly), the difference can be observed - for example, `print(foo())` prints nothing if `foo` returns no values, and `nil` if it returns `nil`. + +To help write code that has consistent behavior, linter warns about cases when a function implicitly returns no values, if there are cases when it explicitly returns a result. For code like this it's recommended to use explicit `return` or `return nil` at the end of the function (these have different semantics, so the correct version to use depends on the function): + +```luau +--!hidden mode=nocheck +local function find(t, expected) + for k,v in pairs(t) do + if k == expected then + return v + end + end + -- Function 'find' can implicitly return no values even though there's an explicit return at line 4; add explicit return to silence +end +``` + +## DuplicateLocal (17) + +Luau syntax allows to use the same name for different parameters of a function as well as different local variables declared in the same statement. This is error prone, even if it's occasionally useful, so a warning is emitted in cases like this, unless the duplicate name is the placeholder `_`: + +```luau +--!hidden mode=nocheck +function foo(a, b, a) -- Function parameter 'a' already defined on column 14 +end + +function obj:method(self) -- Function parameter 'self' already defined implicitly +end + +local x, y, x = v:GetComponents() -- Variable 'x' already defined on column 7 +``` + +## FormatString (18) + +Luau has several library functions that expect a format string that specifies the behavior for the function. These format strings follow a specific syntax that depends on the question; mistakes in these strings can lead to runtime errors or unexpected behavior of the code. + +To help make sure that the strings used for these functions are correct, linter checks calls to `string.format`, `string.pack`, `string.packsize`, `string.unpack`, `string.match`, `string.gmatch`, `string.find`, `string.gsub` and `os.date` and issues warnings when the call uses a literal string with an incorrect format: + +```luau +--!hidden mode=nocheck +-- Invalid match pattern: invalid capture reference, must refer to a closed capture +local cap = string.match(s, "(%d)%2") + +-- Invalid format string: unfinished format specifier +local str = ("%d %"):format(1, 2) +``` + + Note that with the exception of `string.format` this only works when the function is called via the library, not via the method call (so prefer `string.match(s, "pattern")` to `s:match("pattern")`). + +## TableLiteral (19) + +Table literals are often used in Luau to create objects or specify data. The syntax for table literals is rich but invites mistakes, for example it allows duplicate keys or redundant index specification for values already present in the list form. This warning is emitted for cases where some entries in the table literal are ignored at runtime as they're duplicate: + +```luau +print({ + first = 1, + second = 2, + first = 3, -- Table field 'first' is a duplicate; previously defined at line 2 +}) +``` + +## UninitializedLocal (20) + +It's easy to forget to initialize a local variable and then proceed to use it regardless. This can happen due to a typo, or sometimes it can happen because the original variable definition is shadowed. When a local variable doesn't have an initial value and is used without ever being assigned to, this warning is emitted: + +```luau +local foo + +if foo then -- Variable 'foo' defined at line 1 is never initialized or assigned; initialize with 'nil' to silence + print(foo) +end +``` + +## DuplicateFunction (21) + +This warning is emitted when a script defines two functions with the same name in the same scope. + +The warning is not produced when the functions are defined in different scopes because this is much more likely to be intentional. + +```luau +--!hidden mode=nocheck +function foo() end +function foo() end -- Duplicate function definition: 'foo' also defined on line 1 + +-- OK: the functions are not defined in the same scope. +if x then + function bar() end +else + function bar() end +end +``` + +## DeprecatedApi (22) + +This warning is emitted when a script accesses a method or field that is marked as deprecated. Use of deprecated APIs is discouraged since they may have performance or correctness issues, may result in unexpected behavior, and could be removed in the future. + +```luau +--!hidden mode=nocheck +function getSize(i: Instance) + return i.DataCost -- Member 'Instance.DataCost' is deprecated +end +``` + +## TableOperations (23) + +To manipulate array-like tables, Luau provides a set of functions as part of the standard `table` library. To help use these functions correctly, this warning is emitted when one of these functions is used incorrectly or can be adjusted to be more performant. + +```luau +local t = {} + +table.insert(t, 0, 42) -- table.insert uses index 0 but arrays are 1-based; did you mean 1 instead? +table.insert(t, #t+1, 42) -- table.insert will append the value to the table; consider removing the second argument for efficiency +``` + +In addition, when type information is present, this warning will be emitted when `#` or `ipairs` is used on a table that has no numeric keys or indexers. This helps avoid common bugs like using `#t == 0` to check if a dictionary is empty. + +```luau +local message = { data = { 1, 2, 3 } } + +if #message == 0 then -- Using '#' on a table without an array part is likely a bug +end +``` + +## DuplicateCondition (24) + +When checking multiple conditions via `and/or` or `if/elseif`, a copy & paste error may result in checking the same condition redundantly. This almost always indicates a bug, so a warning is emitted when use of a duplicate condition is detected. + +```luau +--!hidden mode=nocheck +assert(self._adorns[normID1] and self._adorns[normID1]) -- Condition has already been checked on column 8 +``` + +## MisleadingAndOr (25) + +In Lua, there is no first-class ternary operator but it can be emulated via `a and b or c` pattern. However, due to how boolean evaluation works, if `b` is `false` or `nil`, the resulting expression evaluates to `c` regardless of the value of `a`. Luau solves this problem with the `if a then b else c` expression; a warning is emitted for and-or expressions where the first alternative is `false` or `nil` because it's almost always a bug. + +```luau +--!hidden mode=nocheck +-- The and-or expression always evaluates to the second alternative because the first alternative is false; consider using if-then-else expression instead +local x = flag and false or true +``` + +The code above can be rewritten as follows to avoid the warning and the associated bug: + +```luau +--!hidden mode=nocheck +local x = if flag then false else true +``` + +## CommentDirective (26) + +Luau uses comments that start from `!` to control certain aspects of analysis, for example setting type checking mode via `--!strict` or disabling individual lints with `--!nolint`. Unknown directives are ignored, for example `--!nostrict` doesn't have any effect on the type checking process as the correct spelling is `--!nonstrict`. This warning flags comment directives that are ignored during processing: + +```luau +--!nostrict +-- Unknown comment directive 'nostrict'; did you mean 'nonstrict'?" +``` + +## IntegerParsing (27) + +Luau parses hexadecimal and binary literals as 64-bit integers before converting them to Luau numbers. As a result, numbers that exceed 2^64 are silently truncated to 2^64, which can result in unexpected program behavior. This warning flags literals that are truncated: + +```luau +-- Hexadecimal number literal exceeded available precision and was truncated to 2^64 +local x = 0x1111111111111111111111111111111111111 +``` + +Luau numbers are represented as double precision IEEE754 floating point numbers; they can represent integers up to 2^53 exactly, but larger integers may lose precision. This warning also flags literals that are parsed with a precision loss: + +```luau +-- Number literal exceeded available precision and was truncated to closest representable number +local x = 9007199254740993 +``` + +## ComparisonPrecedence (28) + +Because of operator precedence rules, not X == Y parses as (not X) == Y; however, often the intent was to invert the result of the comparison. This warning flags erroneous conditions like that, as well as flagging cases where two comparisons happen in a row without any parentheses: + +```luau +--!hidden mode=nocheck +-- not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence +if not x == 5 then +end + +-- X <= Y <= Z is equivalent to (X <= Y) <= Z; wrap one of the expressions in parentheses to silence +if 1 <= x <= 3 then +end +``` diff --git a/.cursor/rules/luau/getting-started/syntax.md b/.cursor/rules/luau/getting-started/syntax.md new file mode 100644 index 00000000..75019cac --- /dev/null +++ b/.cursor/rules/luau/getting-started/syntax.md @@ -0,0 +1,318 @@ +--- +slug: syntax +title: Luau syntax by example +description: An informal reference for Luau's syntax by example. +sidebar: + order: 3 +--- + +Luau uses the baseline [syntax of Lua 5.1](https://www.lua.org/manual/5.1/manual.html#2). For detailed documentation, please refer to the Lua manual, this is an example: + +```luau +local function tree_insert(tree, x) + local lower, equal, greater = split(tree.root, x) + if not equal then + equal = { + x = x, + y = math.random(0, 2^31-1), + left = nil, + right = nil + } + end + tree.root = merge3(lower, equal, greater) +end +``` + +Note that future versions of Lua extend the Lua 5.1 syntax with more features; Luau does support string literal extensions but does not support other 5.x additions; for details please refer to [compatibility section](../compatibility). + +The rest of this document documents additional syntax used in Luau. + +## String literals + +Luau implements support for hexadecimal (`\x`), Unicode (`\u`) and `\z` escapes for string literals. This syntax follows [Lua 5.3 syntax](https://www.lua.org/manual/5.3/manual.html#3.1): + +- `\xAB` inserts a character with the code 0xAB into the string +- `\u{ABC}` inserts a UTF8 byte sequence that encodes U+0ABC character into the string (note that braces are mandatory) +- `\z` at the end of the line inside a string literal ignores all following whitespace including newlines, which can be helpful for breaking long literals into multiple lines. + +## Number literals + +In addition to basic integer and floating-point decimal numbers, Luau supports: + +- Hexadecimal integer literals, `0xABC` or `0XABC` +- Binary integer literals, `0b01010101` or `0B01010101` +- Decimal separators in all integer literals, using `_` for readability: `1_048_576`, `0xFFFF_FFFF`, `0b_0101_0101` + +Note that Luau only has a single number type, a 64-bit IEEE754 double precision number (which can represent integers up to 2^53 exactly), and larger integer literals are stored with precision loss. + +## Continue statement + +In addition to `break` in all loops, Luau supports `continue` statement. Similar to `break`, `continue` must be the last statement in the block. + +Note that unlike `break`, `continue` is not a keyword. This is required to preserve backwards compatibility with existing code; so this is a `continue` statement: + +```luau +--!hidden mode=nocheck +if x < 0 then + continue +end +``` + +Whereas this is a function call: + +```luau +--!hidden mode=nocheck +if x < 0 then + continue() +end +``` + +When used in `repeat..until` loops, `continue` can not skip the declaration of a local variable if that local variable is used in the loop condition; code like this is invalid and won't compile: + +```luau +repeat + do continue end + local a = 5 +until a > 0 +``` + +## Compound assignments + +Luau supports compound assignments with the following operators: `+=`, `-=`, `*=`, `/=`, `//=`, `%=`, `^=`, `..=`. Just like regular assignments, compound assignments are statements, not expressions: + +```luau +local a = 5 + +-- this works +a += 1 + +-- this doesn't work +print(a += 1) +``` + +Compound assignments only support a single value on the left and right hand side; additionally, the function calls on the left hand side are only evaluated once: + +```luau +function foo() return 2 end +local a = { 1, 2, 3 } + +-- calls foo() twice +a[foo()] = a[foo()] + 1 + +-- calls foo() once +a[foo()] += 1 +``` + +Compound assignments call the arithmetic metamethods (`__add` et al) and table indexing metamethods (`__index` and `__newindex`) as needed - for custom types no extra effort is necessary to support them. + +## Type annotations + +To support gradual typing, Luau supports optional type annotations for variables and functions, as well as declaring type aliases. + +Types can be declared for local variables, function arguments and function return types using `:` as a separator: + +```luau +function foo(x: number, y: string): boolean + local k: string = y:rep(x) + return k == "a" +end +``` + +In addition, the type of any expression can be overridden using a type cast `::`: + +```luau +function foo(x: number, y: unknown): boolean + local k = (y :: string):rep(x) + return k == "a" +end +``` + +There are several simple builtin types: `any` (represents inability of the type checker to reason about the type), `nil`, `boolean`, `number`, `string` and `thread`. + +Function types are specified using the arguments and return types, separated with `->`: + +```luau +local foo: (number, string) -> boolean +``` + +To return no values or more than one, you need to wrap the return type position with parentheses, and then list your types there. + +```luau +local no_returns: (number, string) -> () +local returns_boolean_and_string: (number, string) -> (boolean, string) + +function foo(x: number, y: number): (number, string) + return x + y, tostring(x) .. tostring(y) +end +``` + +Note that function types are specified without the argument names in the examples above, but it's also possible to specify the names (that are not semantically significant but can show up in documentation and autocomplete): + +```luau +local callback: (errorCode: number, errorText: string) -> () +``` + +Table types are specified using the table literal syntax, using `:` to separate keys from values: + +```luau +local array: { [number] : string } +local object: { x: number, y: string } +``` + +When the table consists of values keyed by numbers, it's called an array-like table and has a special short-hand syntax, `{T}` (e.g. `{string}`). + +Additionally, the type syntax supports type intersections (`((number) -> string) & ((boolean) -> string)`) and unions (`(number | boolean) -> string`). An intersection represents a type with values that conform to both sides at the same time, which is useful for overloaded functions; a union represents a type that can store values of either type - `any` is technically a union of all possible types. + +It's common in Lua for function arguments or other values to store either a value of a given type or `nil`; this is represented as a union (`number | nil`), but can be specified using `?` as a shorthand syntax (`number?`). + +In addition to declaring types for a given value, Luau supports declaring type aliases via `type` syntax: + +```luau +type Point = { x: number, y: number } +type Array = { [number]: T } +type Something = typeof(string.gmatch("", "%d")) +``` + +The right hand side of the type alias can be a type definition or a `typeof` expression; `typeof` expression doesn't evaluate its argument at runtime. + +By default type aliases are local to the file they are declared in. To be able to use type aliases in other modules using `require`, they need to be exported: + +```luau +export type Point = { x: number, y: number } +``` + +An exported type can be used in another module by prefixing its name with the require alias that you used to import the module. + +```luau +--!file main.luau +local M = require("./other") + +local a: M.Point = {x=5, y=6} + +--!file other.luau +export type Point = { x: number, y: number } + +return {} +``` + +For more information please refer to [typechecking documentation](../types). + +## If-then-else expressions + +In addition to supporting standard if *statements*, Luau adds support for if *expressions*. Syntactically, `if-then-else` expressions look very similar to if statements. However instead of conditionally executing blocks of code, if expressions conditionally evaluate expressions and return the value produced as a result. Also, unlike if statements, if expressions do not terminate with the `end` keyword. + +Here is a simple example of an `if-then-else` expression: +```luau +local a, b = 6, 7 +local maxValue = if a > b then a else b +``` + +`if-then-else` expressions may occur in any place a regular expression is used. The `if-then-else` expression must match `if then else `; it can also contain an arbitrary number of `elseif` clauses, like `if then elseif then else `. Note that in either case, `else` is mandatory. + +Here's is an example demonstrating `elseif`: +```luau +local x = -21 +local sign = if x < 0 then -1 elseif x > 0 then 1 else 0 +``` + +**Note:** In Luau, the `if-then-else` expression is preferred vs the standard Lua idiom of writing `a and b or c` (which roughly simulates a ternary operator). However, the Lua idiom may return an unexpected result if `b` evaluates to false. The `if-then-else` expression will behave as expected in all situations. + +## Generalized iteration + +Luau uses the standard Lua syntax for iterating through containers, `for vars in values`, but extends the semantics with support for generalized iteration. In Lua, to iterate over a table you need to use an iterator like `next` or a function that returns one like `pairs` or `ipairs`. In Luau, you can simply iterate over a table: + +```luau +for k, v in {1, 4, 9} do + assert(k * k == v) +end +``` + +Further, iteration can be extended for tables or userdata by implementing the `__iter` metamethod which is called before the iteration begins, and should return an iterator function like `next` (or a custom one): + +```luau +local obj = { items = {1, 4, 9} } +setmetatable(obj, { __iter = function(o) return next, o.items end }) + +for k, v in obj do + assert(k * k == v) +end +``` + +The default iteration order for tables is specified to be consecutive for elements `1..#t` and unordered after that, visiting every element; similarly to iteration using `pairs`, modifying the table entries for keys other than the current one results in unspecified behavior. + +## String interpolation + +Luau adds an additional way to define string values that allows you to place runtime expressions directly inside specific spots of the literal. + +This is a more ergonomic alternative over using `string.format` or `("literal"):format`. + +To use string interpolation, use a backtick string literal: + +```luau +local count = 3 +print(`Bob has {count} apple(s)!`) +--> Bob has 3 apple(s)! +``` + +Any expression can be used inside `{}`: + +```luau +local combos = {2, 7, 1, 8, 5} +print(`The lock combination is {table.concat(combos)}.`) +--> The lock combination is 27185. +``` + +Inside backtick string literal, `\` is used to escape `` ` ``, `{`, `\` itself and a newline: + +```luau +print(`Some example escaping the braces \{like so}`) +--> Some example escaping the braces {like so} + +print(`Backslash \ that escapes the space is not a part of the string...`) +--> Backslash that escapes the space is not a part of the string... + +print(`Backslash \\ will escape the second backslash...`) +--> Backslash \ will escape the second backslash... + +print(`Some text that also includes \`...`) +--> Some text that also includes `... + +local name = "Luau" + +print(`Welcome to { + name +}!`) +--> Welcome to Luau! +``` + +### Restrictions and limitations + +The sequence of two opening braces {{"`{{`"}} is rejected with a parse error. +This restriction is made to prevent developers using other programming languages with a similar feature from trying to attempt that as a way to escape a single `{` and getting unexpected results in Luau. + +Luau currently does not support backtick string literals in type annotations, therefore `` type Foo = `Foo` `` is invalid syntax. + +Unlike single and double-quoted string literals, backtick string literals must always be wrapped in parentheses for function calls: +```luau +print "hello" -- valid +print`hello` -- invalid syntax +print(`hello`) -- valid +``` + +## Floor division (`//`) + +Luau supports floor division, including its operator (`//`), its compound assignment operator (`//=`), and overloading metamethod (`__idiv`), as an ergonomic alternative to `math.floor`. + +For numbers, `a // b` is equal to `math.floor(a / b)`: +```luau +local n = 6.72 +print(n // 2) --> 3 +n //= 3 +print(n) --> 2 +``` + +Note that it's possible to get `inf`, `-inf`, or `NaN` with floor division; when `b` is `0`, `a // b` results in positive or negative infinity, and when both `a` and `b` are `0`, `a // b` results in NaN. + +For native vectors, `c // d` applies `math.floor` to each component of the vector `c`. Therefore `c // d` is equivalent to `vector.create(math.floor(c.x / d), math.floor(c.y / b), math.floor(c.z / b))`. + +Floor division syntax and semantics follow from [Lua 5.3](https://www.lua.org/manual/5.3/manual.html#3.4.1) where applicable. diff --git a/.cursor/rules/luau/getting-started/why.md b/.cursor/rules/luau/getting-started/why.md new file mode 100644 index 00000000..752d7828 --- /dev/null +++ b/.cursor/rules/luau/getting-started/why.md @@ -0,0 +1,28 @@ +--- +slug: why +title: Why Luau? +sidebar: + order: 2 +--- + +Around 2006, [Roblox](https://www.roblox.com) started using Lua 5.1 as a scripting language for games. Over the years the runtime had to be tweaked to provide a safe, secure sandboxed environment; we gradually started accumulating small library changes and tweaks. + +Over the course of the last few years, instead of using Web-based stack for our player-facing application, Lua-based in-game UI and Qt-based editor UI, we've started consolidating a lot of the efforts and developing all of these using Roblox engine and Lua as a scripting language. + +Having grown a substantial internal codebase that needed to be correct and performant, and with the focus shifting a bit from novice game developers to professional studios building games on Roblox and our own teams of engineers building applications, there was a need to improve performance and quality of the code we were writing. + +Unlike mainline Lua, we also could not afford to do major breaking changes to the language (hence the 5.1 language baseline that remained unchanged for more than a decade). While faster implementations of Lua 5.1 like LuaJIT were available, they didn't meet our needs in terms of portability, ease of change and they didn't address the problem of developing robust code at scale. + +All of these motivated us to start reshaping Lua 5.1 that we started from into a new, derivative language that we call Luau. Our focus is on making the language more performant and feature-rich, and make it easier to write robust code through a combination of linting and type checking using a gradual type system. + +## Complete rewrite? + +A very large part of Luau codebase is written from scratch. We needed a set of tools to be able to write language analysis tools; Lua has a parser that is integrated with the bytecode compiler, which makes it unsuitable for complex semantic analysis. For bytecode compilation, while a single pass compiler can deliver better compilation throughput and be simpler than a full frontend/backend, it significantly limits the optimizations that can be done at the bytecode level. + +Luau compiler and analysis tools are thus written from scratch, closely following the syntax and semantics of Lua. Our compiler is not single-pass, and instead relies on a set of analysis passes that run over the AST to produce efficient bytecode, followed by some post-process optimizations. + +As for the runtime, we had to rewrite the interpreter from scratch to get substantially faster performance; using a combination of techniques pioneered by LuaJIT and custom optimizations that are able to improve performance by taking control over the entire stack (language, compiler, interpreter, virtual machine), we're able to get close to LuaJIT interpreter performance while using C as an implementation language. + +The garbage collector and the core libraries represent more of an incremental change, where we used Lua 5.1 as a baseline but we're continuing to rewrite these as well with performance in mind. + +While Luau initially didn't include a JIT/AOT, native code generation has since been implemented for x64/arm64 platform as an optional component. diff --git a/.cursor/rules/luau/guides/performance.md b/.cursor/rules/luau/guides/performance.md new file mode 100644 index 00000000..76c86a48 --- /dev/null +++ b/.cursor/rules/luau/guides/performance.md @@ -0,0 +1,212 @@ +--- +slug: performance +title: How we make Luau fast +sidebar: + order: 1 +--- + +One of main goals of Luau is to enable high performance code, with gameplay code being the main use case. This can be viewed as two separate goals: + +- Make idiomatic code that wasn't tuned faster +- Enable even higher performance through careful tuning + +Both of these goals are important - it's insufficient to just focus on the highly tuned code, and all things being equal we prefer to raise all boats by implementing general optimizations. However, in some cases it's important to be aware of optimizations that Luau does and doesn't do. + +Worth noting is that Luau is focused on, first and foremost, stable high performance code in interpreted context. This is because JIT compilation is not available on many platforms Luau runs on, and AOT compilation would only work for code that Roblox ships (and even that does not always work). This is in stark contrast with LuaJIT that, while providing an excellent interpreter as well, focuses a lot of the attention on JIT (with many optimizations unavailable in the interpreter). + +Having said that, Luau has been updated to include an optional JIT component for x64 and arm64 platforms. This component can compile a selected set of functions, including limiting compilation to functions or modules marked explicitly by the user. While functions can be compiled at any time, automated JIT compilation decisions based on statistics/tracing are not performed. Luau JIT takes into account the type annotations present in the source code to specialize code paths and at this time, doesn't include runtime analysis of the types/values flowing through the program. + +The rest of this document goes into some optimizations that Luau employs and how to best leverage them when writing code. The document is not complete - a lot of optimizations are transparent to the user and involve detailed low-level tuning of various parts that is not described here - and all of this is subject to change without notice, as it doesn't affect the semantics of valid code. + +## Fast bytecode interpreter + +Luau features a very highly tuned portable bytecode interpreter. It's similar to Lua interpreter in that it's written in C, but it's highly tuned to yield efficient assembly when compiled with Clang and latest versions of MSVC. On some workloads it can match the performance of LuaJIT interpreter which is written in highly specialized assembly. We are continuing to tune the interpreter and the bytecode format over time; while extra performance can be extracted by rewriting the interpreter in assembly, we're unlikely to ever do that as the extra gains at this point are marginal, and we gain a lot from C in terms of portability and being able to quickly implement new optimizations. + +Of course the interpreter isn't typical C code - it uses many tricks to achieve extreme levels of performance and to coerce the compiler to produce efficient assembly. Due to a better bytecode design and more efficient dispatch loop it's noticeably faster than Lua 5.x (including Lua 5.4 which made some of the changes similar to Luau, but doesn't come close). The bytecode design was partially inspired by excellent LuaJIT interpreter. Most computationally intensive scripts only use the interpreter core loop and builtins, which on x64 compiles into \~16 KB, thus leaving half of the instruction cache for other infrequently called code. + +## Optimizing compiler + +Unlike Lua and LuaJIT, Luau uses a multi-pass compiler with a frontend that parses source into an AST and a backend that generates bytecode from it. This carries a small penalty in terms of compilation time, but results in more flexible code and, crucially, makes it easier to optimize the generated bytecode. + +> Note: Compilation throughput isn't the main focus in Luau, but our compiler is reasonably fast; with all currently implemented optimizations enabled, it compiles 950K lines of Luau code in 1 second on a single core of a desktop Ryzen 5900X CPU, producing bytecode and debug information. + +While bytecode optimizations are limited due to the flexibility of Luau code (e.g. `a * 1` may not be equivalent to `a` if `*` is overloaded through metatables), even in absence of type information Luau compiler can perform some optimizations such as "deep" constant folding across functions and local variables, perform upvalue optimizations for upvalues that aren't mutated, do analysis of builtin function usage, optimize the instruction sequences for multiple variable assignments, and some peephole optimizations on the resulting bytecode. The compiler can also be instructed to use more aggressive optimizations by enabling optimization level 2 (`-O2` in CLI tools), some of which are documented further on this page. + +Most bytecode optimizations are performed on individual statements or functions, however the compiler also does a limited amount of interprocedural optimizations; notably, calls to local functions can be optimized with the knowledge of the argument count or number of return values involved. Interprocedural optimizations are limited to a single module due to the compilation model. + +Luau compiler is also able to use type information to do further optimizations. Because we control the entire stack (unlike e.g. TypeScript where the type information is discarded completely before reaching the VM), we have more flexibility there and can make some tradeoffs during codegen even if the type system isn't completely sound. For example, it might be reasonable to assume that in presence of known types, we can infer absence of side effects for arithmetic operations and builtins - if the runtime types mismatch due to intentional violation of the type safety through global injection, the code will still be safely sandboxed. Type information is currently limited to small peephole optimizations, but it has a potential to unlock optimizations such as common subexpression elimination and allocation hoisting in the future, without having to rely on a JIT. These future optimizations opportunities are speculative pending further research. + +## Epsilon-overhead debugger + +It's important for Luau to have stable and predictable performance. Something that comes up in Lua-based environments often is the use of line hooks to implement debugging (both for breakpoints and for stepping). This is problematic because the support for hooks is typically not free in general, but importantly once the hook is enabled, calling the hook has a considerable overhead, and the hook itself may be very costly to evaluate since it will need to associate the script:line pair with the breakpoint information. + +Luau does not support hooks at all, and relies on first-class support for breakpoints (using bytecode patching) and single-stepping (using a custom interpreter loop) to implement debugging. As a result, the presence of breakpoints doesn't slow the script execution down - the only noticeable discrepancy between running code under a debugger and without a debugger should be in cases where breakpoints are evaluated and skipped based on breakpoint conditions, or when stepping over long-running fragments of code. + +## Inline caching for table and global access + +Table access for field lookup is optimized in Luau using a mechanism that blends inline caching (classically used in Java/JavaScript VMs) and HREFs (implemented in LuaJIT). Compiler can predict the hash slot used by field lookup, and the VM can correct this prediction dynamically. + +As a result, field access can be very fast in Luau, provided that: + +- The field name is known at compile time. To make sure this is the case, `table.field` notation is recommended, although the compiler will also optimize `table["field"]` when the expression is known to be a constant string. +- The field access doesn't use metatables. The fastest way to work with tables in Luau is to store fields directly inside the table, and store methods in the metatable (see below); access to "static" fields in classic OOP designs is best done through `Class.StaticField` instead of `object.StaticField`. +- The object structure is usually uniform. While it's possible to use the same function to access tables of different shape - e.g. `function getX(obj) return obj.x end` can be used on any table that has a field `"x"` - it's best to not vary the keys used in the tables too much, as it defeats this optimization. + +The same optimization is applied to the custom globals declared in the script, although it's best to avoid these altogether by using locals instead. Still, this means that the difference between `function` and `local function` is less pronounced in Luau. + +## Importing global access chains + +While global access for library functions can be optimized in a similar way, this optimization breaks down when the global table is using sandboxing through metatables, and even when globals aren't sandboxed, `math.max` still requires two table accesses. + +It's always possible to "localize" the global accesses by using `local max = math.max`, but this is cumbersome - in practice it's easy to forget to apply this optimization. To avoid relying on programmers remembering to do this, Luau implements a special optimization called "imports", where most global chains such as `math.max` are resolved when the script is loaded instead of when the script is executed. + +This optimization relies on being able to predict the shape of the environment table for a given function; this is possible due to global sandboxing, however this optimization is invalid in some cases: + +- `loadstring` can load additional code that runs in context of the caller's environment +- `getfenv`/`setfenv` can directly modify the environment of any function + +The use of any of these functions performs a dynamic deoptimization, marking the affected environment as "impure". The optimizations are only in effect on functions with "pure" environments - because of this, the use of `loadstring`/`getfenv`/`setfenv` is not recommended. Note that `getfenv` deoptimizes the environment even if it's only used to read values from the environment. + +> Note: Luau still supports these functions as part of our backwards compatibility promise, although we'd love to switch to Lua 5.2's `_ENV` as that mechanism is cleaner and doesn't require costly dynamic deoptimization. + +## Fast method calls + +Luau specializes method calls to improve their performance through a combination of compiler, VM and binding optimizations. Compiler emits a specialized instruction sequence when methods are called through `obj:Method` syntax (while this isn't idiomatic anyway, you should avoid `obj.Method(obj)`). When the object in question is a Lua table, VM performs some voodoo magic based on inline caching to try to quickly discover the implementation of this method through the metatable. + +For this to be effective, it's crucial that `__index` in a metatable points to a table directly. For performance reasons it's strongly recommended to avoid `__index` functions as well as deep `__index` chains; an ideal object in Luau is a table with a metatable that points to itself through `__index`. + +When the object in question is a reflected userdata, a special mechanism called "namecall" is used to minimize the interop cost. In classical Lua binding model, `obj:Method` is called in two steps, retrieving the function object (`obj.Method`) and calling it; both steps are often implemented in C++, and the method retrieval needs to use a method object cache - all of this makes method calls slow. + +Luau can directly call the method by name using the "namecall" extension, and an optimized reflection layer can retrieve the correct method quickly through more voodoo magic based on string interning and custom Luau features that aren't exposed through Luau scripts. + +As a result of both optimizations, common Lua tricks of caching the method in a local variable aren't very productive in Luau and aren't recommended either. + +## Specialized builtin function calls + +Due to global sandboxing and the ability to dynamically deoptimize code running in impure environments, in pure environments we go beyond optimizing the interpreter and optimize many built-in functions through a "fastcall" mechanism. + +For this mechanism to work, function call must be "obvious" to the compiler - it needs to call a builtin function directly, e.g. `math.max(x, 1)`, although it also works if the function is "localized" (`local max = math.max`); this mechanism doesn't work for indirect function calls unless they were inlined during compilation, and doesn't work for method calls (so calling `string.byte` is more efficient than `s:byte`). + +The mechanism works by directly invoking a highly specialized and optimized implementation of a builtin function from the interpreter core loop without setting up a stack frame and omitting other work; additionally, some fastcall specializations are partial in that they don't support all types of arguments, for example all `math` library builtins are only specialized for numeric arguments, so calling `math.abs` with a string argument will fall back to the slower implementation that will do string->number coercion. + +As a result, builtin calls are very fast in Luau - they are still slightly slower than core instructions such as arithmetic operations, but only slightly so. The set of fastcall builtins is slowly expanding over time and as of this writing contains `assert`, `type`, `typeof`, `rawget`/`rawset`/`rawequal`, `getmetatable`/`setmetatable`, `tonumber`/`tostring`, all functions from `math` (except `noise` and `random`/`randomseed`) and `bit32`, and some functions from `string` and `table` library. + +Some builtin functions have partial specializations that reduce the cost of the common case further. Notably: + +- `assert` is specialized for cases when the assertion return value is not used and the condition is truthy; this helps reduce the runtime cost of assertions to the extent possible +- `bit32.extract` is optimized further when field and width selectors are constant +- `select` is optimized when the second argument is `...`; in particular, `select(x, ...)` is O(1) when using the builtin dispatch mechanism even though it's normally O(N) in variadic argument count. + +Some functions from `math` library like `math.floor` can additionally take advantage of advanced SIMD instruction sets like SSE4.1 when available. + +In addition to runtime optimizations for builtin calls, many builtin calls, as well as constants like `math.pi`/`math.huge`, can also be constant-folded by the bytecode compiler when using aggressive optimizations (level 2); this currently applies to most builtin calls with constant arguments and a single return value. For builtin calls that can not be constant folded, compiler assumes knowledge of argument/return count (level 2) to produce more efficient bytecode instructions. + +## Optimized table iteration + +Luau implements a fully generic iteration protocol; however, for iteration through tables in addition to generalized iteration (`for .. in t`) it recognizes three common idioms (`for .. in ipairs(t)`, `for .. in pairs(t)` and `for .. in next, t`) and emits specialized bytecode that is carefully optimized using custom internal iterators. + +As a result, iteration through tables typically doesn't result in function calls for every iteration; the performance of iteration using generalized iteration, `pairs` and `ipairs` is comparable, so generalized iteration (without the use of `pairs`/`ipairs`) is recommended unless the code needs to be compatible with vanilla Lua or the specific semantics of `ipairs` (which stops at the first `nil` element) is required. Additionally, using generalized iteration avoids calling `pairs` when the loop starts which can be noticeable when the table is very short. + +Iterating through array-like tables using `for i=1,#t` tends to be slightly slower because of extra cost incurred when reading elements from the table. + +## Optimized table length + +Luau tables use a hybrid array/hash storage, like in Lua; in some sense "arrays" don't truly exist and are an internal optimization, but some operations, notably `#t` and functions that depend on it, like `table.insert`, are defined by the Luau/Lua language to allow internal optimizations. Luau takes advantage of that fact. + +Unlike Lua, Luau guarantees that the element at index `#t` is stored in the array part of the table. This can accelerate various table operations that use indices limited by `#t`, and this makes `#t` worst-case complexity O(logN), unlike Lua where the worst case complexity is O(N). This also accelerates computation of this value for small tables like `{ [1] = 1 }` since we never need to look at the hash part. + +The "default" implementation of `#t` in both Lua and Luau is a binary search. Luau uses a special branch-free (depending on the compiler...) implementation of the binary search which results in 50+% faster computation of table length when it needs to be computed from scratch. + +Additionally, Luau can cache the length of the table and adjust it following operations like `table.insert`/`table.remove`; this means that in practice, `#t` is almost always a constant time operation. + +## Creating and modifying tables + +Luau implements several optimizations for table creation. When creating object-like tables, it's recommended to use table literals (`{ ... }`) and to specify all table fields in the literal in one go instead of assigning fields later; this triggers an optimization inspired by LuaJIT's "table templates" and results in higher performance when creating objects. When creating array-like tables, if the maximum size of the table is known up front, it's recommended to use `table.create` function which can create an empty table with preallocated storage, and optionally fill it with a given value. + +When the exact table shape isn't known, Luau compiler can still predict the table capacity required in case the table is initialized with an empty literal (`{}`) and filled with fields subsequently. For example, the following code creates a correctly sized table implicitly: + +```luau +local v = {} +v.x = 1 +v.y = 2 +v.z = 3 +return v +``` + +When appending elements to tables, it's recommended to use `table.insert` (which is the fastest method to append an element to a table if the table size is not known). In cases when a table is filled sequentially, however, it can be more efficient to use a known index for insertion - together with preallocating tables using `table.create` this can result in much faster code, for example this is the fastest way to build a table of squares: + +```luau +local t = table.create(N) + +for i=1,N do + t[i] = i * i +end +``` + +## Native vector math + +Luau uses tagged value storage - each value contains a type tag and the data that represents the value of a given type. Because of the need to store 64-bit double precision numbers *and* 64-bit pointers, we don't use NaN tagging and have to pay the cost of 16 bytes per value. + +We take advantage of this to provide a native value type that can store a 32-bit floating point vector with 3 components. This type is fundamental to game computations and as such it's important to optimize the storage and the operations with that type - our VM implements first class support for all math operations and component manipulation, which essentially means we have native 3-wide SIMD support. For code that uses many vector values this results in significantly smaller GC pressure and significantly faster execution, and gives programmers a mechanism to hand-vectorize numeric code if need be. + +## Optimized upvalue storage + +Lua implements upvalues as garbage collected objects that can point directly at the thread's stack or, when the value leaves the stack frame (and is "closed"), store the value inside the object. This representation is necessary when upvalues are mutated, but inefficient when they aren't - and 90% or more of upvalues aren't mutated in typical Lua code. Luau takes advantage of this by reworking upvalue storage to prioritize immutable upvalues - capturing upvalues that don't change doesn't require extra allocations or upvalue closing, resulting in faster closure allocation, faster execution, faster garbage collection and faster upvalue access due to better memory locality. + +Note that "immutable" in this case only refers to the variable itself - if the variable isn't assigned to it can be captured by value, even if it's a table that has its contents change. + +When upvalues are mutable, they do require an extra allocated object; we carefully optimize the memory consumption and access cost for mutable upvalues to reduce the associated overhead. + +## Closure caching + +With optimized upvalue storage, creating new closures (function objects) is more efficient but still requires allocating a new object every time. This can be problematic for cases when functions are passed to algorithms like `table.sort` or functions like `pcall`, as it results in excessive allocation traffic which then leads to more work for garbage collector. + +To make closure creation cheaper, Luau compiler implements closure caching - when multiple executions of the same function expression are guaranteed to result in the function object that is semantically identical, the compiler may cache the closure and always return the same object. This changes the function identity which may affect code that uses function objects as table keys, but preserves the calling semantics - compiler will only do this if calling the original (cached) function behaves the same way as a newly created function would. The heuristics used for this optimization are subject to change; currently, the compiler will cache closures that have no upvalues, or all upvalues are immutable (see previous section) and are declared at the module scope, as the module scope is (almost always) evaluated only once. + +## Fast memory allocator + +Similarly to LuaJIT, but unlike vanilla Lua, Luau implements a custom allocator that is highly specialized and tuned to the common allocation workloads we see. The allocator design is inspired by classic pool allocators as well as the excellent `mimalloc`, but through careful domain-specific tuning it beats all general purpose allocators we've tested, including `rpmalloc`, `mimalloc`, `jemalloc`, `ptmalloc` and `tcmalloc`. + +This doesn't mean that memory allocation in Luau is free - it's carefully optimized, but it still carries a cost, and a high rate of allocations requires more work from the garbage collector. The garbage collector is incremental, so short of some edge cases this rarely results in visible GC pauses, but can impact the throughput since scripts will interrupt to perform "GC assists" (helping clean up the garbage). Thus for high performance Luau code it's recommended to avoid allocating memory in tight loops, by avoiding temporary table and userdata creation. + +In addition to a fast allocator, all frequently used structures in Luau have been optimized for memory consumption, especially on 64-bit platforms, compared to Lua 5.1 baseline. This helps to reduce heap memory footprint and improve performance in some cases by reducing the memory bandwidth impact of garbage collection. + +## Optimized libraries + +While the best performing code in Luau spends most of the time in the interpreter, performance of the standard library functions is critical to some applications. In addition to specializing many small and simple functions using the builtin call mechanism, we spend extra care on optimizing all library functions and providing additional functions beyond the Lua standard library that help achieve good performance with idiomatic code. + +Functions from the `table` library like `insert`, `remove` and `move` have been tuned for performance on array-like tables, achieving 3x and more performance compared to un-tuned versions, and Luau provides additional functions like `table.create` and `table.find` to achieve further speedup when applicable. Our implementation of `table.sort` is using `introsort` algorithm which results in guaranteed worst case `NlogN` complexity regardless of the input, and, together with the array-like specializations, helps achieve ~4x speedup on average. + +For `string` library, we use a carefully tuned dynamic string buffer implementation; it is optimized for smaller strings to reduce garbage created during string manipulation, and for larger strings it allows to produce a large string without extra copies, especially in cases where the resulting size is known ahead of time. Additionally, functions like `format` have been tuned to avoid the overhead of `sprintf` where possible, resulting in further speedups. + +## Improved garbage collector pacing + +Luau uses an incremental garbage collector which does a little bit of work every so often, and at no point does it stop the world to traverse the entire heap. The runtime will make sure that the collector runs interspersed with the program execution as the program allocates additional memory, which is known as "garbage collection assists", and can also run in response to explicit garbage collection invocation via `lua_gc`. In interactive environments such as video game engines it's possible, and even desirable, to request garbage collection every frame to make sure assists are minimized, since that allows scheduling the garbage collection to run concurrently with other engine processing that doesn't involve script execution. + +Inspired by excellent work by Austin Clements on Go's garbage collector pacer, we've implemented a pacing algorithm that uses a proportional–integral–derivative controller to estimate internal garbage collector tunables to reach a target heap size, defined as a percentage of the live heap data (which is more intuitive and actionable than Lua 5.x "GC pause" setting). Luau runtime also estimates the allocation rate making it easy (given uniform allocation rates) to adjust the per-frame garbage collection requests to do most of the required GC work outside of script execution. + +## Reduced garbage collector pauses + +While Luau uses an incremental garbage collector, once per each collector cycle it runs a so-called "atomic" step. While all other GC steps can do very little work by only looking at a few objects at a given time, which means that the collector can have arbitrarily short pauses, the "atomic" step needs to traverse some amount of data that, in some cases, may scale with the application heap. Since atomic step is indivisible, it can result in occasional pauses on the order of tens of milliseconds, which is problematic for interactive applications. We've implemented a series of optimizations to help reduce the atomic step. + +Normally objects that have been modified after the GC marked them in an incremental mark phase need to be rescanned during atomic phase, so frequent modifications of existing tables may result in a slow atomic step. To address this, we run a "remark" step where we traverse objects that have been modified after being marked once more (incrementally); additionally, the write barrier that triggers for object modifications changes the transition logic during remark phase to reduce the probability that the object will need to be rescanned. + +Another source of scalability challenges is coroutines. Writes to coroutine stacks don't use a write barrier, since that's prohibitively expensive as they are too frequent. This means that coroutine stacks need to be traversed during atomic step, so applications with many coroutines suffer large atomic pauses. To address this, we implement incremental marking of coroutines: marking a coroutine makes it "inactive" and resuming a coroutine (or pushing extra objects on the coroutine stack via C API) makes it "active". Atomic step only needs to traverse active coroutines again, which reduces the cost of atomic step by effectively making coroutine collection incremental as well. + +While large tables can be a problem for incremental GC in general since currently marking a single object is indivisible, large weak tables are a unique challenge because they also need to be processed during atomic phase, and the main use case for weak tables - object caches - may result in tables with large capacity but few live objects in long-running applications that exhibit bursts of activity. To address this, weak tables in Luau can be marked as "shrinkable" by including `s` as part of `__mode` string, which results in weak tables being resized to the optimal capacity during GC. This option may result in missing keys during table iteration if the table is resized while iteration is in progress and as such is only recommended for use in specific circumstances. + +## Optimized garbage collector sweeping + +The incremental garbage collector in Luau runs three phases for each cycle: mark, atomic and sweep. Mark incrementally traverses all live objects, atomic finishes various operations that need to happen without mutator intervention (see previous section), and sweep traverses all objects in the heap, reclaiming memory used by dead objects and performing minor fixup for live objects. While objects allocated during the mark phase are traversed in the same cycle and thus may get reclaimed, objects allocated during the sweep phase are considered live. Because of this, the faster the sweep phase completes, the less garbage will accumulate; and, of course, the less time sweeping takes the less overhead there is from this phase of garbage collection on the process. + +Since sweeping traverses the whole heap, we maximize the efficiency of this traversal by allocating garbage-collected objects of the same size in 16 KB pages, and traversing each page at a time, which is otherwise known as a paged sweeper. This ensures good locality of reference as consecutively swept objects are contiguous in memory, and allows us to spend no memory for each object on sweep-related data or allocation metadata, since paged sweeper doesn't need to be able to free objects without knowing which page they are in. Compared to linked list based sweeping that Lua/LuaJIT implement, paged sweeper is 2-3x faster, and saves 16 bytes per object on 64-bit platforms. + +## Function inlining and loop unrolling + +By default, the bytecode compiler performs a series of optimizations that result in faster execution of the code, but they preserve both execution semantics and debuggability. For example, a function call is compiled as a function call, which may be observable via `debug.traceback`; a loop is compiled as a loop, which may be observable via `lua_getlocal`. To help improve performance in cases where these restrictions can be relaxed, the bytecode compiler implements additional optimizations when optimization level 2 is enabled (which requires using `-O2` switch when using Luau CLI), namely function inlining and loop unrolling. + +Only loops with loop bounds known at compile time, such as `for i=1,4 do`, can be unrolled. The loop body must be simple enough for the optimization to be profitable; compiler uses heuristics to estimate the performance benefit and automatically decide if unrolling should be performed. + +Only local functions (defined either as `local function foo` or `local foo = function`) can be inlined. The function body must be simple enough for the optimization to be profitable; compiler uses heuristics to estimate the performance benefit and automatically decide if each call to the function should be inlined instead. Additionally recursive invocations of a function can’t be inlined at this time, and inlining is completely disabled for modules that use `getfenv`/`setfenv` functions. + +In both cases, in addition to removing the overhead associated with function calls or loop iteration, these optimizations can additionally benefit by enabling additional optimizations, such as constant folding of expressions dependent on loop iteration variable or constant function arguments, or using more efficient instructions for certain expressions when the inputs to these instructions are constants. diff --git a/.cursor/rules/luau/guides/profile.md b/.cursor/rules/luau/guides/profile.md new file mode 100644 index 00000000..3d69ecdb --- /dev/null +++ b/.cursor/rules/luau/guides/profile.md @@ -0,0 +1,60 @@ +--- +permalink: /profile +title: Profiling your Luau code +sidebar: + order: 3 +--- + +One of main goals of Luau is to enable high performance code. To help with that goal, we are relentlessly optimizing the compiler and runtime - but ultimately, performance of their +code is in developers' hands, and is a combination of good algorithm design and implementation that adheres to the strengths of the language. To help write efficient code, Luau +provides a built-in profiler that samples the execution of the program and outputs a profiler dump that can be converted to an interactive flamegraph. + +To run the profiler, make sure you have an optimized build of the interpreter (otherwise profiling results are going to be very skewed) and run it with `--profile` argument: + +``` +$ luau --profile tests/chess.lua +OK 8902 rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 +OK 2039 r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 0 +OK 2812 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 0 +OK 9467 r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1 +OK 1486 rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8 +OK 2079 r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10 +Profiler dump written to profile.out (total runtime 2.034 seconds, 20344 samples, 374 stacks) +GC: 0.378 seconds (18.58%), mark 46.80%, remark 3.33%, atomic 1.93%, sweepstring 6.77%, sweep 41.16% +``` + +The resulting `profile.out` file can be converted to an SVG file by running `perfgraph.py` script that is part of Luau repository: + +``` +$ python tools/perfgraph.py profile.out >profile.svg +``` + +This produces an SVG file that can be opened in a browser (the image below is clickable): + +[![profile.svg](/images/chess-profile.svg)](/images/chess-profile.svg) + +In a flame graph visualization, the individual bars represent function calls, the width represents how much of the total program runtime they execute, and the nesting matches the call stack encountered during program execution. This is a fantastic visualization technique that allows you to hone in on the specific bottlenecks affecting +your program performance, optimize those exact bottlenecks, and then re-generate the profile data and visualizer, and look for the next set of true bottlenecks (if any). + +Hovering your mouse cursor over individual sections will display detailed function information in the status bar and in a tooltip. If you want to Search for a specific named +function, use the Search field in the upper right, or press Ctrl+F. + +Notice that some of the bars in the screenshot don't have any text. In some cases, there isn't enough room in the size of the bar to display the name. +You can hover your mouse over those bars to see the name and source location of the function in the tool tip, or double-click to zoom in on that part of the flame graph. + +Some tooltips will have a source location for the function you're hovering over, but no name. Those are anonymous functions, or functions that were not declared in a way that +allows Luau compiler to track the name. To fill in more names, you may want to make these changes to your code: + +`local myFunc = function() --[[ work ]] end` -> `local function myFunc() --[[ work ]] end` + +Even without these changes, you can hover over a given bar with no visible name and see it's source location. + +As any sampling profiler, this profiler relies on gathering enough information for the resulting output to be statistically meaningful. It may miss short functions if they +aren't called often enough. By default the profiler runs at 10 kHz, this can be customized by passing a different parameter to `--profile=`. Note that higher +frequencies result in higher profiling overhead and longer program execution, potentially skewing the results. + +This profiler doesn't track leaf C functions and instead attributes the time spent there to calling Luau functions. As a result, when thinking about why a given function is +slow, consider not just the work it does immediately but also the library functions it calls. + +This profiler tracks time consumed by Luau thread stacks; when a thread calls another thread via `coroutine.resume`, the time spent is not attributed to the parent thread that's +waiting for resume results. This limitation will be removed in the future. diff --git a/.cursor/rules/luau/guides/sandbox.md b/.cursor/rules/luau/guides/sandbox.md new file mode 100644 index 00000000..86734050 --- /dev/null +++ b/.cursor/rules/luau/guides/sandbox.md @@ -0,0 +1,77 @@ +--- +slug: sandbox +title: Embedding a sandboxed Luau virtual machine +sidebar: + order: 2 +--- + +Luau is safe to embed. Broadly speaking, this means that even in the face of untrusted (and in Roblox case, actively malicious) code, the language and the standard library don't allow unsafe access to the underlying system, and don't have known bugs that allow escaping out of the sandbox (e.g. to gain native code execution through ROP gadgets et al). Additionally, the VM provides extra features to implement isolation of privileged code from unprivileged code and protect one from the other; this is important if the embedding environment decides to expose some APIs that may not be safe to call from untrusted code, for example because they do provide controlled access to the underlying system or risk PII exposure through fingerprinting etc. + +This safety is achieved through a combination of removing features from the standard library that are unsafe, adding features to the VM that make it possible to implement sandboxing and isolation, and making sure the implementation is safe from memory safety issues using fuzzing. + +Of course, since the entire stack is implemented in C++, the sandboxing isn't formally proven - in theory, compiler or the standard library can have exploitable vulnerabilities. In practice these are very rare and usually found and fixed quickly. While implementing the stack in a safer language such as Rust would make it easier to provide these guarantees, to our knowledge (based on prior art) this would make it difficult to reach the level of performance required. + +## Library + +Parts of the Lua 5.x standard library are unsafe. Some of the functions provide access to the host operating system, including process execution and file reads. Some functions lack sufficient memory safety checks. Some functions are safe if all code is untrusted, but can break the isolation barrier between trusted and untrusted code. + +The following libraries and global functions have been removed as a result: + +- `io.` library has been removed entirely, as it gives access to files and allows running processes +- `package.` library has been removed entirely, as it gives access to files and allows loading native modules +- `os.` library has been cleaned up from file and environment access functions (`execute`, `exit`, etc.). The only supported functions in the library are `clock`, `date`, `difftime` and `time`. +- `debug.` library has been removed to a large extent, as it has functions that aren't memory safe and other functions break isolation; the only supported functions are `traceback` and `info` (which is similar to `debug.getinfo` but has a slightly different interface). +- `dofile` and `loadfile` allowed access to file system and have been removed. + +To achieve memory safety, access to function bytecode has been removed. Bytecode is hard to validate and using untrusted bytecode may lead to exploits. Thus, `loadstring` doesn't work with bytecode inputs, and `string.dump`/`load` have been removed as they aren't necessary anymore. When embedding Luau, bytecode should be encrypted/signed to prevent MITM attacks as well, as the VM assumes that the bytecode was generated by the Luau compiler (which never produces invalid/unsafe bytecode). + +Finally, to make isolation possible within the same VM, the following global functions have reduced functionality: + +- `collectgarbage` only works with `"count"` argument, as modifying the state of GC can interfere with the expectations of other code running in the process. As such, `collectgarbage()` became an inferior version of `gcinfo()` and is deprecated. +- `newproxy` only works with `true`/`false`/`nil` arguments. +- `module` allowed overriding global packages and was removed as a result. + +> Note: `getfenv`/`setfenv` result in additional isolation challenges, as they allow injecting globals into scripts on the call stack. Ideally, these should be disabled as well, but unfortunately Roblox community relies on these for various reasons. This can be mitigated by limiting interaction between trusted and untrusted code, and/or using separate VMs. + +## Environment + +The modification to the library functions are sufficient to make embedding safe, but aren't sufficient to provide isolation within the same VM. It should be noted that to achieve guaranteed isolation, it's advisable to load trusted and untrusted code into separate VMs; however, even within the same VM Luau provides additional safety features to make isolation cheaper. + +When initializing the default globals table, the tables are protected from modification: + +- All libraries (`string`, `math`, etc.) are marked as readonly +- The string metatable is marked as readonly +- The global table itself is marked as readonly + +This is using the VM feature that is not accessible from scripts, that prevents all writes to the table, including assignments, `rawset` and `setmetatable`. This makes sure that globals can't be monkey-patched in place, and can only be substituted through `setfenv`. + +By itself this would mean that code that runs in Luau can't use globals at all, since assigning globals would fail. While this is feasible, in Roblox we solve this by creating a new global table for each script, that uses `__index` to point to the builtin global table. This safely sandboxes the builtin globals while still allowing writing globals from each script. This also means that short of exposing special shared globals from the host, all scripts are isolated from each other. + +## `__gc` + +Lua 5.1 exposes a `__gc` metamethod for userdata, which can be used on proxies (`newproxy`) to hook into garbage collector. Later versions of Lua extend this mechanism to work on tables. + +This mechanism is bad for performance, memory safety and isolation: + +- In Lua 5.1, `__gc` support requires traversing userdata lists redundantly during garbage collection to filter out finalizable objects +- In later versions of Lua, userdata that implement `__gc` are split into separate lists; however, finalization prolongs the lifetime of the finalized objects which results in less prompt memory reclamation, and two-step destruction results in extra cache misses for userdata +- `__gc` runs during garbage collection in context of an arbitrary thread which makes the thread identity mechanism used in Roblox to support trusted Luau code invalid +- Objects can be removed from weak tables *after* being finalized, which means that accessing these objects can result in memory safety bugs, unless all exposed userdata methods guard against use-after-gc. +- If `__gc` method ever leaks to scripts, they can call it directly on an object and use any method exposed by that object after that. This means that `__gc` and all other exposed methods must support memory safety when called on a destroyed object. + +Because of these issues, Luau does not support `__gc`. Instead it uses tag-based destructors that can perform additional memory cleanup during userdata destruction; crucially, these are only available to the host (so they can never be invoked manually), and they run right before freeing the userdata memory block which is both optimal for performance, and guaranteed to be memory safe. + +For monitoring garbage collector behavior the recommendation is to use weak tables instead. + +## Interrupts + +In addition to preventing API access, it can be important for isolation to limit the memory and CPU usage of code that runs inside the VM. + +By default, no memory limits are imposed on the running code, so it's possible to exhaust the address space of the host; this is easy to configure from the host for Luau allocations, but of course with a rich API surface exposed by the host it's hard to eliminate this as a possibility. Memory exhaustion doesn't result in memory safety issues or any particular risk to the system that's running the host process, other than the host process getting terminated by the OS. + +Limiting CPU usage can be equally challenging with a rich API. However, Luau does provide a VM-level feature to try to contain runaway scripts which makes it possible to terminate any script externally. This works through a global interrupt mechanism, where the host can setup an interrupt handler at any point, and any Luau code is guaranteed to call this handler "eventually" (in practice this can happen at any function call or at any loop iteration). This still leaves the possibility of a very long running script open if the script manages to find a way to call a single C function that takes a lot of time, but short of that the interruption is very prompt. + +Roblox sets up the interrupt handler using a watchdog that: + +- Limits the runtime of any script in Studio to 10 seconds (configurable through Studio settings) +- Upon client shutdown, interrupts execution of every running script 1 second after shutdown diff --git a/.cursor/rules/luau/index.mdx b/.cursor/rules/luau/index.mdx new file mode 100644 index 00000000..a1da377d --- /dev/null +++ b/.cursor/rules/luau/index.mdx @@ -0,0 +1,62 @@ +--- +title: Luau +description: Luau is a small, fast, and embeddable programming language based on Lua with a gradual type system. +template: splash +hero: + tagline: > + Luau (lowercase u, /ˈlu.aʊ/) is a small, fast, and embeddable programming language based on Lua with a gradual type system. + image: + file: ../../../public/images/mascot.png + alt: Hina, a Hawaiian monk seal and the official mascot of the Luau language + actions: + - text: Get Started + link: /getting-started/ + - text: News + link: /news/ + variant: secondary + - text: Try Luau online + link: https://play.luau.org + icon: right-caret + variant: minimal +--- + +import { Card, CardGrid } from '@astrojs/starlight/components'; +import ColoredCard from '../../components/ColoredCard.astro'; +import LuauPlayground from '../../components/LuauPlayground.astro'; + +# Try Luau! + + + +# Why Luau? + + + + Luau is a high-performance managed language with a fast bytecode compiler and interpreter, and an optimized, incremental garbage collector. Its optional JIT compiler runs on x64 and arm64, and supports plugins that extend native code generation to any embedder-defined types. + + + Luau has a state-of-the-art gradual type system with features like [type refinements](./types/type-refinements) and [type functions](./types/type-functions). This system is powered by an ambitious and powerful type inference engine that aims to both rule out program errors, _and_ provide autocomplete with minimal annotations. + + + Luau is designed to be easy-to-learn, and easy-to-use over time. The syntax of the language is small and easy to pick up, and the semantics of the language should be broadly unsurprising to most programmers, with recursive functions, conventional loops and branching, and so forth. + + + Luau aims to be pleasant and productive to use, and to make programming as fun as it can be! Besides the language itself, we're building a full suite of developer tools: package management, linting, code transformation, documentation generation, and servers for editing and debugging code. + + + Luau, like Lua before it, is designed with embedding in mind. The language features an extended version of Lua's C API with better support for coroutines and yielding, code coverage, and more performant, tagged userdata. We've also carefully sandboxed Luau, limiting the exposed API surface by default and enabling embedders to safely support executing untrusted code from their users. + + + Whenever possible, Luau aims to be backwards-compatible with Lua 5.1 and at the same time to incorporate features from later revisions of Lua. However, Luau is not a full superset of later versions of Lua - we do not always agree with Lua design decisions, and have different use cases and constraints. All post-5.1 Lua features, along with their support status in Luau, [are documented here](compatibility). + + diff --git a/.cursor/rules/luau/reference/grammar.md b/.cursor/rules/luau/reference/grammar.md new file mode 100644 index 00000000..c3a783fe --- /dev/null +++ b/.cursor/rules/luau/reference/grammar.md @@ -0,0 +1,109 @@ +--- +slug: grammar +title: Syntax Grammar +sidebar: + order: 3 +--- + +This is the complete syntax grammar for Luau in EBNF. More information about the terminal nodes STRING and NUMBER +is available in the [syntax section](../syntax). + +```ebnf +chunk ::= block +block ::= {stat [';']} [laststat [';']] +stat ::= varlist '=' explist | + var compoundop exp | + functioncall | + 'do' block 'end' | + 'while' exp 'do' block 'end' | + 'repeat' block 'until' exp | + 'if' exp 'then' block {'elseif' exp 'then' block} ['else' block] 'end' | + 'for' binding '=' exp ',' exp [',' exp] 'do' block 'end' | + 'for' bindinglist 'in' explist 'do' block 'end' | + attributes 'function' funcname funcbody | + attributes 'local' 'function' NAME funcbody | + 'local' bindinglist ['=' explist] | + ['export'] 'type' NAME ['<' GenericTypeListWithDefaults '>'] '=' Type | + ['export'] 'type' 'function' NAME funcbody + +laststat ::= 'return' [explist] | 'break' | 'continue' + +funcname ::= NAME {'.' NAME} [':' NAME] +funcbody ::= ['<' GenericTypeList '>'] '(' [parlist] ')' [':' ReturnType] block 'end' +parlist ::= bindinglist [',' '...' [':' (GenericTypePack | Type)]] | '...' [':' (GenericTypePack | Type)] + +explist ::= {exp ','} exp + +binding ::= NAME [':' Type] +bindinglist ::= binding [',' bindinglist] (* equivalent of Lua 5.1 'namelist', except with optional type annotations *) + +var ::= NAME | prefixexp '[' exp ']' | prefixexp '.' NAME +varlist ::= var {',' var} +prefixexp ::= var | functioncall | '(' exp ')' +functioncall ::= prefixexp funcargs | prefixexp ':' NAME funcargs + +exp ::= asexp { binop exp } | unop exp { binop exp } +ifelseexp ::= 'if' exp 'then' exp {'elseif' exp 'then' exp} 'else' exp +asexp ::= simpleexp ['::' Type] +stringinterp ::= INTERP_BEGIN exp { INTERP_MID exp } INTERP_END +simpleexp ::= NUMBER | STRING | 'nil' | 'true' | 'false' | '...' | tableconstructor | attributes 'function' funcbody | prefixexp | ifelseexp | stringinterp +funcargs ::= '(' [explist] ')' | tableconstructor | STRING + +tableconstructor ::= '{' [fieldlist] '}' +fieldlist ::= field {fieldsep field} [fieldsep] +field ::= '[' exp ']' '=' exp | NAME '=' exp | exp +fieldsep ::= ',' | ';' + +compoundop ::= '+=' | '-=' | '*=' | '/=' | '//=' | '%=' | '^=' | '..=' +binop ::= '+' | '-' | '*' | '/' | '//' | '^' | '%' | '..' | '<' | '<=' | '>' | '>=' | '==' | '~=' | 'and' | 'or' +unop ::= '-' | 'not' | '#' + +littable ::= '{' [litfieldlist] '}' +litfieldlist ::= litfield {fieldsep litfield} [fieldsep] +litfield ::= [NAME '='] literal + +literal ::= 'nil' | 'false' | 'true' | NUMBER | STRING | littable +litlist ::= literal {',' literal} + +pars ::= '(' [litlist] ')' | littable | STRING +parattr ::= NAME [pars] +attribute ::= '@' NAME | '@[' parattr {',' parattr} ']' +attributes ::= {attribute} + +SimpleType ::= + 'nil' | + SingletonType | + NAME ['.' NAME] [ '<' [TypeParams] '>' ] | + 'typeof' '(' exp ')' | + TableType | + FunctionType | + '(' Type ')' + +SingletonType ::= STRING | 'true' | 'false' + +Union ::= [SimpleType {'?'}] {'|' SimpleType {'?'}} +Intersection ::= [SimpleType] {'&' SimpleType} +Type ::= Union | Intersection + +GenericTypePackParameter ::= NAME '...' +GenericTypeList ::= NAME [',' GenericTypeList] | GenericTypePackParameter {',' GenericTypePackParameter} + +GenericTypePackParameterWithDefault ::= NAME '...' '=' (TypePack | VariadicTypePack | GenericTypePack) +GenericTypeListWithDefaults ::= + NAME ['=' Type] [',' GenericTypeListWithDefaults] | + GenericTypePackParameterWithDefault {',' GenericTypePackParameterWithDefault} + +TypeList ::= Type [',' TypeList] | '...' Type +BoundTypeList ::= [NAME ':'] Type [',' BoundTypeList] | GenericTypePack | VariadicTypePack +TypeParams ::= (Type | TypePack | VariadicTypePack | GenericTypePack) [',' TypeParams] +TypePack ::= '(' [TypeList] ')' +GenericTypePack ::= NAME '...' +VariadicTypePack ::= '...' Type +ReturnType ::= Type | TypePack | GenericTypePack | VariadicTypePack +TableIndexer ::= ['read' | 'write'] '[' Type ']' ':' Type +TableProp ::= ['read' | 'write'] NAME ':' Type +PropList ::= TableProp [fieldsep PropList] | TableIndexer {fieldsep TableProp} + +TableType ::= '{' Type '}' | '{' [PropList] '}' +FunctionType ::= ['<' GenericTypeList '>'] '(' [BoundTypeList] ')' '->' ReturnType +``` diff --git a/.cursor/rules/luau/reference/library.md b/.cursor/rules/luau/reference/library.md new file mode 100644 index 00000000..36d61af9 --- /dev/null +++ b/.cursor/rules/luau/reference/library.md @@ -0,0 +1,1097 @@ +--- +slug: library +title: Standard Library +description: The official reference for Luau's standard library. +sidebar: + order: 1 +--- + +Luau comes equipped with a standard library of functions designed to manipulate the built-in data types. Note that the library is relatively minimal and doesn't expose ways for +scripts to interact with the host environment - it's expected that embedding applications provide extra functionality on top of this and limit or sandbox the system access +appropriately, if necessary. For example, Roblox provides [a rich API to interact with the 3D environment and limited APIs to interact with external services](https://developer.roblox.com/en-us/api-reference). + +This page documents the available builtin libraries and functions. All of these are accessible by default by any script, assuming the host environment exposes them (which is usually a safe assumption outside of extremely constrained environments). + +## Global functions + +While most library functions are provided as part of a library like `table`, a few global functions are exposed without extra namespacing. + +``` +function assert(value: T, message: string?): T +``` + +`assert` checks if the value is truthy; if it's not (which means it's `false` or `nil`), it raises an error. The error message can be customized with an optional parameter. +Upon success the function returns the `value` argument. + +``` +function error(obj: any, level: number?) +``` + +`error` raises an error with the specified object. Note that errors don't have to be strings, although they often are by convention; various error handling mechanisms like `pcall` +preserve the error type. When `level` is specified, the error raised is turned into a string that contains call frame information for the caller at level `level`, where `1` refers +to the function that called `error`. This can be useful to attribute the errors to callers, for example `error("Expected a valid object", 2)` highlights the caller of the function +that called `error` instead of the function itself in the callstack. + +``` +function gcinfo(): number +``` + +`gcinfo` returns the total heap size in kilobytes, which includes bytecode objects, global tables as well as the script-allocated objects. Note that Luau uses an incremental +garbage collector, and as such at any given point in time the heap may contain both reachable and unreachable objects. The number returned by `gcinfo` reflects the current heap +consumption from the operating system perspective and can fluctuate over time as garbage collector frees objects. + +``` +function getfenv(target: (function | number)?): table +``` + +Returns the environment table for target function; when `target` is not a function, it must be a number corresponding to the caller stack index, where 1 means the function that calls `getfenv`, and the environment table is returned for the corresponding function from the call stack. When `target` is omitted it defaults to `1`, so `getfenv()` returns the environment table for the calling function. + +``` +function getmetatable(obj: any): table? +``` + +Returns the metatable for the specified object; when object is not a table or a userdata, the returned metatable is shared between all objects of the same type. Note that when metatable is protected (has a `__metatable` key), the value corresponding to that key is returned instead and may not be a table. + +``` +function next(t: { [K]: V }, i: K?): (K, V)? +``` + +Given the table `t`, returns the next key-value pair after `i` in the table traversal order, or nothing if `i` is the last key. When `i` is `nil`, returns the first key-value pair instead. + +``` +function newproxy(mt: boolean?): userdata +``` + +Creates a new untyped userdata object; when `mt` is true, the new object has an empty metatable that can be modified using `getmetatable`. + +``` +function print(args: ...any) +``` + +Prints all arguments to the standard output, using Tab as a separator. + +``` +function rawequal(a: any, b: any): boolean +``` + +Returns true iff `a` and `b` have the same type and point to the same object (for garbage collected types) or are equal (for value types). + +``` +function rawget(t: { [K]: V }, k: K): V? +``` + +Performs a table lookup with index `k` and returns the resulting value, if present in the table, or nil. This operation bypasses metatables/`__index`. + +``` +function rawlen(t: { [K]: V } | string): number +``` + +Returns the raw length of the table or string. If it is a string, this operation is identical to `#str` or `string.len(str)`. This operation bypasses metatables/`__len`. + +``` +function rawset(t: { [K] : V }, k: K, v: V) +``` + +Assigns table field `k` to the value `v`. This operation bypasses metatables/`__newindex`. + +``` +function select(i: string, args: ...T): number +function select(i: number, args: ...T): ...T +``` + +When called with `'#'` as the first argument, returns the number of remaining parameters passed. Otherwise, returns the subset of parameters starting with the specified index. +Index can be specified from the start of the arguments (using 1 as the first argument), or from the end (using -1 as the last argument). + +``` +function setfenv(target: function | number, env: table) +``` + +Changes the environment table for target function to `env`; when `target` is not a function, it must be a number corresponding to the caller stack index, where 1 means the function that calls `setfenv`, and the environment table is returned for the corresponding function from the call stack. + +``` +function setmetatable(t: table, mt: table?) +``` + +Changes metatable for the given table. Note that unlike `getmetatable`, this function only works on tables. If the table already has a protected metatable (has a `__metatable` field), this function errors. + +``` +function tonumber(s: string, base: number?): number? +``` + +Converts the input string to the number in base `base` (default 10) and returns the resulting number. If the conversion fails (that is, if the input string doesn't represent a valid number in the specified base), returns `nil` instead. + +``` +function tostring(obj: any): string +``` + +Converts the input object to string and returns the result. If the object has a metatable with `__tostring` field, that method is called to perform the conversion. + +``` +function type(obj: any): string +``` + +Returns the type of the object, which is one of `"nil"`, `"boolean"`, `"number"`, `"vector"`, `"string"`, `"table"`, `"function"`, `"userdata"`, `"thread"`, or `"buffer"`. + +``` +function typeof(obj: any): string +``` + +Returns the type of the object; for userdata objects that have a metatable with the `__type` field *and* are defined by the host (not `newproxy`), returns the value for that key. +For custom userdata objects, such as ones returned by `newproxy`, this function returns `"userdata"` to make sure host-defined types can not be spoofed. + +``` +function ipairs(t: table): +``` + +Returns the triple (generator, state, nil) that can be used to traverse the table using a `for` loop. The traversal results in key-value pairs for the numeric portion of the table; key starts from 1 and increases by 1 on each iteration. The traversal terminates when reaching the first `nil` value (so `ipairs` can't be used to traverse array-like tables with holes). + +``` +function pairs(t: table): +``` + +Returns the triple (generator, state, nil) that can be used to traverse the table using a `for` loop. The traversal results in key-value pairs for all keys in the table, numeric and otherwise, but doesn't have a defined order. + +``` +function pcall(f: function, args: ...any): (boolean, ...any) +``` + +Calls function `f` with parameters `args`. If the function succeeds, returns `true` followed by all return values of `f`. If the function raises an error, returns `false` followed by the error object. +Note that `f` can yield, which results in the entire coroutine yielding as well. + +``` +function xpcall(f: function, e: function, args: ...any): (boolean, ...any) +``` + +Calls function `f` with parameters `args`. If the function succeeds, returns `true` followed by all return values of `f`. If the function raises an error, calls `e` with the error object as an argument, and returns `false` followed by the first return value of `e`. +Note that `f` can yield, which results in the entire coroutine yielding as well. +`e` can neither yield nor error - if it does raise an error, `xpcall` returns with `false` followed by a special error message. + +``` +function unpack(a: {V}, f: number?, t: number?): ...V +``` + +Returns all values of `a` with indices in `[f..t]` range. `f` defaults to 1 and `t` defaults to `#a`. Note that this is equivalent to `table.unpack`. + +## math library + +``` +function math.abs(n: number): number +``` + +Returns the absolute value of `n`. Returns NaN if the input is NaN. + +``` +function math.acos(n: number): number +``` + +Returns the arc cosine of `n`, expressed in radians. Returns a value in `[0, pi]` range. Returns NaN if the input is not in `[-1, +1]` range. + +``` +function math.asin(n: number): number +``` + +Returns the arc sine of `n`, expressed in radians. Returns a value in `[-pi/2, +pi/2]` range. Returns NaN if the input is not in `[-1, +1]` range. + +``` +function math.atan2(y: number, x: number): number +``` + +Returns the arc tangent of `y/x`, expressed in radians. The function takes into account the sign of both arguments in order to determine the quadrant. Returns a value in `[-pi, pi]` range. + +``` +function math.atan(n: number): number +``` + +Returns the arc tangent of `n`, expressed in radians. Returns a value in `[-pi/2, pi-2]` range. + +``` +function math.ceil(n: number): number +``` + +Rounds `n` upwards to the next integer boundary. + +``` +function math.cosh(n: number): number +``` + +Returns the hyperbolic cosine of `n`. + +``` +function math.cos(n: number): number +``` + +Returns the cosine of `n`, which is an angle in radians. Returns a value in `[0, 1]` range. + +``` +function math.deg(n: number): number +``` + +Converts `n` from radians to degrees and returns the result. + +``` +function math.exp(n: number): number +``` + +Returns the base-e exponent of `n`, that is `e^n`. + +``` +function math.floor(n: number): number +``` + +Rounds `n` downwards to previous integer boundary. + +``` +function math.fmod(x: number, y: number): number +``` + +Returns the remainder of `x` modulo `y`, rounded towards zero. Returns NaN if `y` is zero. + +``` +function math.frexp(n: number): (number, number) +``` + +Splits the number into a significand (a number in `[-1, +1]` range) and binary exponent such that `n = s * 2^e`, and returns `s, e`. + +``` +function math.ldexp(s: number, e: number): number +``` + +Given the significand and a binary exponent, returns a number `s * 2^e`. + +``` +function math.lerp(a: number, b: number, t: number): number +``` + +Linearly interpolated between number value `a` and `b` using factor `t`, generally returning the result of `a + (b - a) * t`. +When `t` is exactly `1`, the value of `b` will be returned instead to ensure that when `t` is on the interval `[0, 1]`, the result of `lerp` will be on the interval `[a, b]`. + +``` +math.map(x: number, inmin: number, inmax: number, outmin: number, outmax: number): number +``` + +Returns a value that represents `x` mapped linearly from the input range (`inmin` to `inmax`) to the output range (`outmin` to `outmax`). + +``` +function math.log10(n: number): number +``` + +Returns base-10 logarithm of the input number. Returns NaN if the input is negative, and negative infinity if the input is 0. +Equivalent to `math.log(n, 10)`. + +``` +function math.log(n: number, base: number?): number +``` + +Returns logarithm of the input number in the specified base; base defaults to `e`. Returns NaN if the input is negative, and negative infinity if the input is 0. + +``` +function math.max(list: ...number): number +``` + +Returns the maximum number of the input arguments. The function requires at least one input and will error if zero parameters are passed. If one of the inputs is a NaN, the result may or may not be a NaN. + +``` +function math.min(list: ...number): number +``` + +Returns the minimum number of the input arguments. The function requires at least one input and will error if zero parameters are passed. If one of the inputs is a NaN, the result may or may not be a NaN. + +``` +function math.modf(n: number): (number, number) +``` + +Returns the integer and fractional part of the input number. Both the integer and fractional part have the same sign as the input number, e.g. `math.modf(-1.5)` returns `-1, -0.5`. + +``` +function math.pow(x: number, y: number): number +``` + +Returns `x` raised to the power of `y`. + +``` +function math.rad(n: number): number +``` + +Converts `n` from degrees to radians and returns the result. + +``` +function math.random(): number +function math.random(n: number): number +function math.random(min: number, max: number): number +``` + +Returns a random number using the global random number generator. A zero-argument version returns a number in `[0, 1]` range. A one-argument version returns a number in `[1, n]` range. A two-argument version returns a number in `[min, max]` range. The input arguments are truncated to integers, so `math.random(1.5)` always returns 1. + +``` +function math.randomseed(seed: number) +``` + +Reseeds the global random number generator; subsequent calls to `math.random` will generate a deterministic sequence of numbers that only depends on `seed`. + +``` +function math.sinh(n: number): number +``` + +Returns a hyperbolic sine of `n`. + +``` +function math.sin(n: number): number +``` + +Returns the sine of `n`, which is an angle in radians. Returns a value in `[0, 1]` range. + +``` +function math.sqrt(n: number): number +``` + +Returns the square root of `n`. Returns NaN if the input is negative. + +``` +function math.tanh(n: number): number +``` + +Returns the hyperbolic tangent of `n`. + +``` +function math.tan(n: number): number +``` + +Returns the tangent of `n`, which is an angle in radians. + +``` +function math.noise(x: number, y: number?, z: number?): number +``` + +Returns 3D Perlin noise value for the point `(x, y, z)` (`y` and `z` default to zero if absent). Returns a value in `[-1, 1]` range. + +``` +function math.clamp(n: number, min: number, max: number): number +``` + +Returns `n` if the number is in `[min, max]` range; otherwise, returns `min` when `n < min`, and `max` otherwise. If `n` is NaN, may or may not return NaN. +The function errors if `min > max`. + +``` +function math.sign(n: number): number +``` + +Returns `-1` if `n` is negative, `1` if `n` is positive, and `0` if `n` is zero or NaN. + +``` +function math.round(n: number): number +``` + +Rounds `n` to the nearest integer boundary. If `n` is exactly halfway between two integers, rounds `n` away from 0. + +## table library + +``` +function table.concat(a: {string}, sep: string?, f: number?, t: number?): string +``` + +Concatenate all elements of `a` with indices in range `[f..t]` together, using `sep` as a separator if present. `f` defaults to 1 and `t` defaults to `#a`. + +``` +function table.foreach(t: { [K]: V }, f: (K, V) -> R?): R? +``` + +Iterates over all elements of the table in unspecified order; for each key-value pair, calls `f` and returns the result of `f` if it's non-nil. If all invocations of `f` returned `nil`, returns no values. This function has been deprecated and is not recommended for use in new code; use `for` loop instead. + +``` +function table.foreachi(t: {V}, f: (number, V) -> R?): R? +``` + +Iterates over numeric keys of the table in `[1..#t]` range in order; for each key-value pair, calls `f` and returns the result of `f` if it's non-nil. If all invocations of `f` returned `nil`, returns no values. This function has been deprecated and is not recommended for use in new code; use `for` loop instead. + +``` +function table.getn(t: {V}): number +``` + +Returns the length of table `t`. This function has been deprecated and is not recommended for use in new code; use `#t` instead. + +``` +function table.maxn(t: {V}): number +``` + +Returns the maximum numeric key of table `t`, or zero if the table doesn't have numeric keys. + +``` +function table.insert(t: {V}, v: V) +function table.insert(t: {V}, i: number, v: V) +``` + +When using a two-argument version, appends the value to the array portion of the table (equivalent to `t[#t+1] = v`). +When using a three-argument version, inserts the value at index `i` and shifts values at indices after that by 1. `i` should be in `[1..#t]` range. + +``` +function table.remove(t: {V}, i: number?): V? +``` + +Removes element `i` from the table and shifts values at indices after that by 1. If `i` is not specified, removes the last element of the table. +`i` should be in `[1..#t]` range. +Returns the value of the removed element, or `nil` if no element was removed (e.g. table was empty). + +``` +function table.sort(t: {V}, f: ((V, V) -> boolean)?) +``` + +Sorts the table `t` in ascending order, using `f` as a comparison predicate: `f` should return `true` iff the first parameter should be before the second parameter in the resulting table. When `f` is not specified, builtin less-than comparison is used instead. +The comparison predicate must establish a strict weak ordering - sort results are undefined otherwise. + +``` +function table.pack(args: ...V): { [number]: V, n: number } +``` + +Returns a table that consists of all input arguments as array elements, and `n` field that is set to the number of inputs. + +``` +function table.unpack(a: {V}, f: number?, t: number?): ...V +``` + +Returns all values of `a` with indices in `[f..t]` range. `f` defaults to 1 and `t` defaults to `#a`. +Note that if you want to unpack varargs packed with `table.pack` you have to specify the index fields because `table.unpack` doesn't automatically use the `n` field that `table.pack` creates. Example usage for packed varargs: `table.unpack(args, 1, args.n)` + +``` +function table.move(a: {V}, f: number, t: number, d: number, tt: {V}?) +``` + +Copies elements in range `[f..t]` from table `a` to table `tt` if specified and `a` otherwise, starting from the index `d`. + +``` +function table.create(n: number, v: V?): {V} +``` + +Creates a table with `n` elements; all of them (range `[1..n]`) are set to `v`. When `v` is nil or omitted, the returned table is empty but has preallocated space for `n` elements which can make subsequent insertions faster. +Note that preallocation is only performed for the array portion of the table - using `table.create` on dictionaries is counter-productive. + +``` +function table.find(t: {V}, v: V, init: number?): number? +``` + +Find the first element in the table that is equal to `v` and returns its index; the traversal stops at the first `nil`. If the element is not found, `nil` is returned instead. The traversal starts at index `init` if specified, otherwise 1. + +``` +function table.clear(t: table) +``` + +Removes all elements from the table while preserving the table capacity, so future assignments don't need to reallocate space. + +``` +function table.freeze(t: table): table +``` + +Given a non-frozen table, freezes it such that all subsequent attempts to modify the table or assign its metatable raise an error. If the input table is already frozen or has a protected metatable, the function raises an error; otherwise it returns the input table. +Note that the table is frozen in-place and is not being copied. Additionally, only `t` is frozen, and keys/values/metatable of `t` don't change their state and need to be frozen separately if desired. + +``` +function table.isfrozen(t: table): boolean +``` + +Returns `true` iff the input table is frozen. + +``` +function table.clone(t: table): table +``` + +Returns a copy of the input table that has the same metatable, same keys and values, and is not frozen even if `t` was. +The copy is shallow: implementing a deep recursive copy automatically is challenging, and often only certain keys need to be cloned recursively which can be done after the initial clone by modifying the resulting table. + +## string library + +``` +function string.byte(s: string, f: number?, t: number?): ...number +``` + +Returns the numeric code of every byte in the input string with indices in range `[f..t]`. `f` defaults to 1 and `t` defaults to `f`, so a two-argument version of this function returns a single number. If the function is called with a single argument and the argument is out of range, the function returns no values. + +``` +function string.char(args: ...number): string +``` + +Returns the string that contains a byte for every input number; all inputs must be integers in `[0..255]` range. + +``` +function string.find(s: string, p: string, init: number?, plain: boolean?): (number?, number?, ...string) +``` + +Tries to find an instance of pattern `p` in the string `s`, starting from position `init` (defaults to 1). When `plain` is true, the search is using raw (case-sensitive) string equality, otherwise `p` should be a [string pattern](https://www.lua.org/manual/5.3/manual.html#6.4.1). If a match is found, returns the position of the match and the length of the match, followed by the pattern captures; otherwise returns `nil`. + +``` +function string.format(s: string, args: ...any): string +``` + +Returns a formatted version of the input arguments using a [printf-style format string](https://en.cppreference.com/w/c/io/fprintf) `s`. The following format characters are supported: + +- `c`: expects an integer number and produces a character with the corresponding character code +- `d`, `i`, `u`: expects an integer number and produces the decimal representation of that number +- `o`: expects an integer number and produces the octal representation of that number +- `x`, `X`: expects an integer number and produces the hexadecimal representation of that number, using lower case or upper case hexadecimal characters +- `e`, `E`, `f`, `g`, `G`: expects a number and produces the floating point representation of that number, using scientific or decimal representation +- `q`: expects a string and produces the same string quoted using double quotation marks, with escaped special characters if necessary +- `s`: expects a string and produces the same string verbatim + +The formats support modifiers `-`, `+`, space, `#` and `0`, as well as field width and precision modifiers - with the exception of `*`. + +``` +function string.gmatch(s: string, p: string): +``` + +Produces an iterator function that, when called repeatedly explicitly or via `for` loop, produces matches of string `s` with [string pattern](https://www.lua.org/manual/5.3/manual.html#6.4.1) `p`. For every match, the captures within the pattern are returned if present (if a pattern has no captures, the entire matching substring is returned instead). + +``` +function string.gsub(s: string, p: string, f: function | table | string, maxs: number?): (string, number) +``` + +For every match of [string pattern](https://www.lua.org/manual/5.3/manual.html#6.4.1) `p` in `s`, replace the match according to `f`. The substitutions stop after the limit of `maxs`, and the function returns the resulting string followed by the number of substitutions. + +When `f` is a string, the substitution uses the string as a replacement. When `f` is a table, the substitution uses the table element with key corresponding to the first pattern capture, if present, and entire match otherwise. Finally, when `f` is a function, the substitution uses the result of calling `f` with call pattern captures, or entire matching substring if no captures are present. + +``` +function string.len(s: string): number +``` + +Returns the number of bytes in the string (equivalent to `#s`). + +``` +function string.lower(s: string): string +``` + +Returns a string where each byte corresponds to the lower-case ASCII version of the input byte in the source string. + +``` +function string.match(s: string, p: string, init: number?): ...string? +``` + +Tries to find an instance of pattern `p` in the string `s`, starting from position `init` (defaults to 1). `p` should be a [string pattern](https://www.lua.org/manual/5.3/manual.html#6.4.1). If a match is found, returns all pattern captures, or entire matching substring if no captures are present, otherwise returns `nil`. + +``` +function string.rep(s: string, n: number): string +``` + +Returns the input string `s` repeated `n` times. Returns an empty string if `n` is zero or negative. + +``` +function string.reverse(s: string): string +``` + +Returns the string with the order of bytes reversed compared to the original. Note that this only works if the input is a binary or ASCII string. + +``` +function string.sub(s: string, f: number, t: number?): string +``` + +Returns a substring of the input string with the byte range `[f..t]`; `t` defaults to `#s`, so a two-argument version returns a string suffix. + +``` +function string.upper(s: string): string +``` + +Returns a string where each byte corresponds to the upper-case ASCII version of the input byte in the source string. + +``` +function string.split(s: string, sep: string?): {string} +``` + +Splits the input string using `sep` as a separator (defaults to `","`) and returns the resulting substrings. If separator is empty, the input string is split into separate one-byte strings. + +``` +function string.pack(f: string, args: ...any): string +``` + +Given a [pack format string](https://www.lua.org/manual/5.3/manual.html#6.4.2), encodes all input parameters according to the packing format and returns the resulting string. Note that Luau uses fixed sizes for all types that have platform-dependent size in Lua 5.x: short is 16 bit, long is 64 bit, integer is 32-bit and size_t is 32 bit for the purpose of string packing. + +``` +function string.packsize(f: string): number +``` + +Given a [pack format string](https://www.lua.org/manual/5.3/manual.html#6.4.2), returns the size of the resulting packed representation. The pack format can't use variable-length format specifiers. Note that Luau uses fixed sizes for all types that have platform-dependent size in Lua 5.x: short is 16 bit, long is 64 bit, integer is 32-bit and size_t is 32 bit for the purpose of string packing. + +``` +function string.unpack(f: string, s: string): ...any +``` + +Given a [pack format string](https://www.lua.org/manual/5.3/manual.html#6.4.2), decodes the input string according to the packing format and returns all resulting values. Note that Luau uses fixed sizes for all types that have platform-dependent size in Lua 5.x: short is 16 bit, long is 64 bit, integer is 32-bit and size_t is 32 bit for the purpose of string packing. + +## coroutine library + +``` +function coroutine.create(f: function): thread +``` + +Returns a new coroutine that, when resumed, will run function `f`. + +``` +function coroutine.running(): thread? +``` + +Returns the currently running coroutine, or `nil` if the code is running in the main coroutine (depending on the host environment setup, main coroutine may never be used for running code). + +``` +function coroutine.status(co: thread): string +``` + +Returns the status of the coroutine, which can be `"running"`, `"suspended"`, `"normal"` or `"dead"`. Dead coroutines have finished their execution and can not be resumed, but their state can still be inspected as they are not dead from the garbage collector point of view. + +``` +function coroutine.wrap(f: function): function +``` + +Creates a new coroutine and returns a function that, when called, resumes the coroutine and passes all arguments along to the suspension point. When the coroutine yields or finishes, the wrapped function returns with all values returned at the suspension point. + +``` +function coroutine.yield(args: ...any): ...any +``` + +Yields the currently running coroutine and passes all arguments along to the code that resumed the coroutine. The coroutine becomes suspended; when the coroutine is resumed again, the resumption arguments will be forwarded to `yield` which will behave as if it returned all of them. + +``` +function coroutine.isyieldable(): boolean +``` + +Returns `true` iff the currently running coroutine can yield. Yielding is prohibited when running inside metamethods like `__index` or C functions like `table.foreach` callback, with the exception of `pcall`/`xpcall`. + +``` +function coroutine.resume(co: thread, args: ...any): (boolean, ...any) +``` + +Resumes the coroutine and passes the arguments along to the suspension point. When the coroutine yields or finishes, returns `true` and all values returned at the suspension point. If an error is raised during coroutine resumption, this function returns `false` and the error object, similarly to `pcall`. + +``` +function coroutine.close(co: thread): (boolean, any?) +``` + +Closes the coroutine which puts coroutine in the dead state. The coroutine must be dead or suspended - in particular it can't be currently running. If the coroutine that's being closed was in an error state, returns `false` along with an error object; otherwise returns `true`. After closing, the coroutine can't be resumed and the coroutine stack becomes empty. + +## bit32 library + +All functions in the `bit32` library treat input numbers as 32-bit unsigned integers in `[0..4294967295]` range. The bit positions start at 0 where 0 corresponds to the least significant bit. + +``` +function bit32.arshift(n: number, i: number): number +``` + +Shifts `n` by `i` bits to the right (if `i` is negative, a left shift is performed instead). The most significant bit of `n` is propagated during the shift. When `i` is larger than 31, returns an integer with all bits set to the sign bit of `n`. When `i` is smaller than `-31`, 0 is returned. + +``` +function bit32.band(args: ...number): number +``` + +Performs a bitwise `and` of all input numbers and returns the result. If the function is called with no arguments, an integer with all bits set to 1 is returned. + +``` +function bit32.bnot(n: number): number +``` + +Returns a bitwise negation of the input number. + +``` +function bit32.bor(args: ...number): number +``` + +Performs a bitwise `or` of all input numbers and returns the result. If the function is called with no arguments, zero is returned. + +``` +function bit32.bxor(args: ...number): number +``` + +Performs a bitwise `xor` (exclusive or) of all input numbers and returns the result. If the function is called with no arguments, zero is returned. + +``` +function bit32.btest(args: ...number): boolean +``` + +Perform a bitwise `and` of all input numbers, and return `true` iff the result is not 0. If the function is called with no arguments, `true` is returned. + +``` +function bit32.extract(n: number, f: number, w: number?): number +``` + +Extracts bits of `n` at position `f` with a width of `w`, and returns the resulting integer. `w` defaults to `1`, so a two-argument version of `extract` returns the bit value at position `f`. Bits are indexed starting at 0. Errors if `f` and `f+w-1` are not between 0 and 31. + +``` +function bit32.lrotate(n: number, i: number): number +``` + +Rotates `n` to the left by `i` bits (if `i` is negative, a right rotate is performed instead); the bits that are shifted past the bit width are shifted back from the right. + +``` +function bit32.lshift(n: number, i: number): number +``` + +Shifts `n` to the left by `i` bits (if `i` is negative, a right shift is performed instead). When `i` is outside of `[-31..31]` range, returns 0. + +``` +function bit32.replace(n: number, r: number, f: number, w: number?): number +``` + +Replaces bits of `n` at position `f` and width `w` with `r`, and returns the resulting integer. `w` defaults to `1`, so a three-argument version of `replace` changes one bit at position `f` to `r` (which should be 0 or 1) and returns the result. Bits are indexed starting at 0. Errors if `f` and `f+w-1` are not between 0 and 31. + +``` +function bit32.rrotate(n: number, i: number): number +``` + +Rotates `n` to the right by `i` bits (if `i` is negative, a left rotate is performed instead); the bits that are shifted past the bit width are shifted back from the left. + +``` +function bit32.rshift(n: number, i: number): number +``` + +Shifts `n` to the right by `i` bits (if `i` is negative, a left shift is performed instead). When `i` is outside of `[-31..31]` range, returns 0. + +``` +function bit32.countlz(n: number): number +``` + +Returns the number of consecutive zero bits in the 32-bit representation of `n` starting from the left-most (most significant) bit. Returns 32 if `n` is zero. + +``` +function bit32.countrz(n: number): number +``` + +Returns the number of consecutive zero bits in the 32-bit representation of `n` starting from the right-most (least significant) bit. Returns 32 if `n` is zero. + +``` +function bit32.byteswap(n: number): number +``` + +Returns `n` with the order of the bytes swapped. + +## utf8 library + +Strings in Luau can contain arbitrary bytes; however, in many applications strings representing text contain UTF8 encoded data by convention, that can be inspected and manipulated using `utf8` library. + +``` +function utf8.offset(s: string, n: number, i: number?): number? +``` + +Returns the byte offset of the Unicode codepoint number `n` in the string, starting from the byte position `i`. When the character is not found, returns `nil` instead. + +``` +function utf8.codepoint(s: string, i: number?, j: number?): ...number +``` + +Returns a number for each Unicode codepoint in the string with the starting byte offset in `[i..j]` range. `i` defaults to 1 and `j` defaults to `i`, so a two-argument version of this function returns the Unicode codepoint that starts at byte offset `i`. + +``` +function utf8.char(args: ...number): string +``` + +Creates a string by concatenating Unicode codepoints for each input number. + +``` +function utf8.len(s: string, i: number?, j: number?): number? +``` + +Returns the number of Unicode codepoints with the starting byte offset in `[i..j]` range, or `nil` followed by the first invalid byte position if the input string is malformed. +`i` defaults to 1 and `j` defaults to `#s`, so `utf8.len(s)` returns the number of Unicode codepoints in string `s` or `nil` if the string is malformed. + +``` +function utf8.codes(s: string): +``` + +Returns an iterator that, when used in `for` loop, produces the byte offset and the codepoint for each Unicode codepoints that `s` consists of. + +## os library + +``` +function os.clock(): number +``` + +Returns a high-precision timestamp (in seconds) that doesn't have a defined baseline, but can be used to measure duration with sub-microsecond precision. + +``` +function os.date(s: string?, t: number?): table | string +``` + +Returns the table or string representation of the time specified as `t` (defaults to current time) according to `s` format string. + +When `s` starts with `!`, the result uses UTC, otherwise it uses the current timezone. + +If `s` is equal to `*t` (or `!*t`), a table representation of the date is returned, with keys `sec`/`min`/`hour` for the time (using 24-hour clock), `day`/`month`/`year` for the date, `wday` for week day (1..7), `yday` for year day (1..366) and `isdst` indicating whether the timezone is currently using daylight savings. + +Otherwise, `s` is interpreted as a [date format string](https://www.cplusplus.com/reference/ctime/strftime/), with the valid specifiers including any of `aAbBcdHIjmMpSUwWxXyYzZ` or `%`. `s` defaults to `"%c"` so `os.date()` returns the human-readable representation of the current date in local timezone. + +``` +function os.difftime(a: number, b: number): number +``` + +Calculates the difference in seconds between `a` and `b`; provided for compatibility only. Please use `a - b` instead. + +``` +function os.time(t: table?): number +``` + +When called without arguments, returns the current date/time as a Unix timestamp. When called with an argument, expects it to be a table that contains `sec`/`min`/`hour`/`day`/`month`/`year` keys and returns the Unix timestamp of the specified date/time in UTC. + +## debug library + +``` +function debug.info(co: thread, level: number, s: string): ...any +function debug.info(level: number, s: string): ...any +function debug.info(f: function, s: string): ...any +``` + +Given a stack frame or a function, and a string that specifies the requested information, returns the information about the stack frame or function. + +Each character of `s` results in additional values being returned in the same order as the characters appear in the string: + +- `s` returns source path for the function +- `l` returns the line number for the stack frame or the line where the function is defined when inspecting a function object +- `n` returns the name of the function, or an empty string if the name is not known +- `f` returns the function object itself +- `a` returns the number of arguments that the function expects followed by a boolean indicating whether the function is variadic or not + +For example, `debug.info(2, "sln")` returns source file, current line and function name for the caller of the current function. + +``` +function debug.traceback(co: thread, msg: string?, level: number?): string +function debug.traceback(msg: string?, level: number?): string +``` + +Produces a stringified callstack of the given thread, or the current thread, starting with level `level`. If `msg` is specified, then the resulting callstack includes the string before the callstack output, separated with a newline. The format of the callstack is human-readable and subject to change. + +## buffer library + +Buffer is an object that represents a fixed-size mutable block of memory. + +All operations on a buffer are provided using the 'buffer' library functions. + +Many of the functions accept an offset in bytes from the start of the buffer. +Offset of 0 from the start of the buffer memory block accesses the first byte. + +All offsets, counts and sizes should be non-negative integer numbers. + +If the bytes that are accessed by any read or write operation are outside the buffer memory, an error is thrown. + +``` +function buffer.create(size: number): buffer +``` + +Creates a buffer of the requested size with all bytes initialized to 0. + +Size limit is 1GB or 1,073,741,824 bytes. + +``` +function buffer.fromstring(str: string): buffer +``` + +Creates a buffer initialized to the contents of the string. + +The size of the buffer equals to the length of the string. + + +``` +function buffer.tostring(b: buffer): string +``` + +Returns the buffer data as a string. + +``` +function buffer.len(b: buffer): number +``` + +Returns the size of the buffer in bytes. + +``` +function buffer.readi8(b: buffer, offset: number): number +function buffer.readu8(b: buffer, offset: number): number +function buffer.readi16(b: buffer, offset: number): number +function buffer.readu16(b: buffer, offset: number): number +function buffer.readi32(b: buffer, offset: number): number +function buffer.readu32(b: buffer, offset: number): number +function buffer.readf32(b: buffer, offset: number): number +function buffer.readf64(b: buffer, offset: number): number +``` + +Used to read the data from the buffer by reinterpreting bytes at the offset as the type in the argument and converting it into a number. + +Available types: + +Function | Type | Range | +---------|------|-------| +readi8 | signed 8-bit integer | [-128, 127] +readu8 | unsigned 8-bit integer | [0, 255] +readi16 | signed 16-bit integer | [-32,768, 32,767] +readu16 | unsigned 16-bit integer | [0, 65,535] +readi32 | signed 32-bit integer | [-2,147,483,648, 2,147,483,647] +readu32 | unsigned 32-bit integer | [0, 4,294,967,295] +readf32 | 32-bit floating-point number | Single-precision IEEE 754 number +readf64 | 64-bit floating-point number | Double-precision IEEE 754 number + +Floating-point numbers are read and written using a format specified by IEEE 754. + +If a floating-point value matches any of bit patterns that represent a NaN (not a number), returned value might be converted to a different quiet NaN representation. + +Read and write operations use the little endian byte order. + +Integer numbers are read and written using two's complement representation. + +``` +function buffer.writei8(b: buffer, offset: number, value: number): () +function buffer.writeu8(b: buffer, offset: number, value: number): () +function buffer.writei16(b: buffer, offset: number, value: number): () +function buffer.writeu16(b: buffer, offset: number, value: number): () +function buffer.writei32(b: buffer, offset: number, value: number): () +function buffer.writeu32(b: buffer, offset: number, value: number): () +function buffer.writef32(b: buffer, offset: number, value: number): () +function buffer.writef64(b: buffer, offset: number, value: number): () +``` + +Used to write data to the buffer by converting the number into the type in the argument and reinterpreting it as individual bytes. + +Ranges of acceptable values can be seen in the table above. + +When writing integers, the number is converted using `bit32` library rules. + +Values that are out-of-range will take less significant bits of the full number. +For example, writing 43,981 (0xabcd) using writei8 function will take 0xcd and interpret it as an 8-bit signed number -51. +It is still recommended to keep all numbers in range of the target type. + +Results of converting special number values (inf/nan) to integers are platform-specific. + +``` +function buffer.readstring(b: buffer, offset: number, count: number): string +``` + +Used to read a string of length 'count' from the buffer at specified offset. + +``` +function buffer.writestring(b: buffer, offset: number, value: string, count: number?): () +``` + +Used to write data from a string into the buffer at a specified offset. + +If an optional 'count' is specified, only 'count' bytes are taken from the string. + +Count cannot be larger than the string length. + +``` +buffer.readbits(b: buffer, bitOffset: number, bitCount: number): number +``` + +Used to read a range of `bitCount` bits from the buffer, at specified offset `bitOffset`, into an unsigned integer. + +`bitCount` must be in `[0, 32]` range. + +``` +buffer.writebits(b: buffer, bitOffset: number, bitCount: number, value: number): () +``` + +Used to write `bitCount` bits from `value` into the buffer at specified offset `bitOffset`. + +`bitCount` must be in `[0, 32]` range. + +``` +function buffer.copy(target: buffer, targetOffset: number, source: buffer, sourceOffset: number?, count: number?): () +``` + +Copy 'count' bytes from 'source' starting at offset 'sourceOffset' into the 'target' at 'targetOffset'. + +It is possible for 'source' and 'target' to be the same. Copying an overlapping region inside the same buffer acts as if the source region is copied into a temporary buffer and then that buffer is copied over to the target. + +If 'sourceOffset' is nil or is omitted, it defaults to 0. + +If 'count' is 'nil' or is omitted, the whole 'source' data starting from 'sourceOffset' is copied. + +``` +function buffer.fill(b: buffer, offset: number, value: number, count: number?): () +``` + +Sets the 'count' bytes in the buffer starting at the specified 'offset' to the 'value'. + +If 'count' is 'nil' or is omitted, all bytes from the specified offset until the end of the buffer are set. + +## vector library + +This library implements functionality for the vector type in addition to the built-in primitive operator support. +Default configuration uses vectors with 3 components (`x`, `y`, and `z`). +If the _4-wide mode_ is enabled by setting the `LUA_VECTOR_SIZE` VM configuration to 4, vectors get an additional `w` component. + +Individual vector components can be accessed using the fields `x` or `X`, `y` or `Y`, `z` or `Z`, and `w` or `W` in 4-wide mode. +Since vector values are immutable, writes to individual components are not supported. + +``` +vector.zero +vector.one +``` + +Constant vectors with all components set to 0 and 1 respectively. Includes the fourth component in _4-wide mode_. + +``` +vector.create(x: number, y: number, z: number): vector +vector.create(x: number, y: number, z: number, w: number): vector +``` + +Creates a new vector with the given component values. The first constructor sets the fourth (`w`) component to 0.0 in _4-wide mode_. + +``` +vector.magnitude(vec: vector): number +``` + +Calculates the magnitude of a given vector. Includes the fourth component in _4-wide mode_. + +``` +vector.normalize(vec: vector): vector +``` + +Computes the normalized version (unit vector) of a given vector. Includes the fourth component in _4-wide mode_. + +``` +vector.cross(vec1: vector, vec2: vector): vector +``` + +Computes the cross product of two vectors. Ignores the fourth component in _4-wide mode_ and returns the 3-dimensional cross product. + +``` +vector.dot(vec1: vector, vec2: vector): number +``` + +Computes the dot product of two vectors. Includes the fourth component in _4-wide mode_. + +``` +vector.angle(vec1: vector, vec2: vector, axis: vector?): number +``` + +Computes the angle between two vectors in radians. The axis, if specified, is used to determine the sign of the angle. Ignores the fourth component in _4-wide mode_ and returns the 3-dimensional angle. + +``` +vector.floor(vec: vector): vector +``` + +Applies `math.floor` to every component of the input vector. Includes the fourth component in _4-wide mode_. + +``` +vector.ceil(vec: vector): vector +``` + +Applies `math.ceil` to every component of the input vector. Includes the fourth component in _4-wide mode_. + +``` +vector.abs(vec: vector): vector +``` + +Applies `math.abs` to every component of the input vector. Includes the fourth component in _4-wide mode_. + +``` +vector.sign(vec: vector): vector +``` + +Applies `math.sign` to every component of the input vector. Includes the fourth component in _4-wide mode_. + +``` +vector.clamp(vec: vector, min: vector, max: vector): vector +``` + +Applies `math.clamp` to every component of the input vector. Includes the fourth component in _4-wide mode_. + +``` +vector.max(...: vector): vector +``` + +Applies `math.max` to the corresponding components of the input vectors. Includes the fourth component in _4-wide mode_. Equivalent to `vector.create(math.max((...).x), math.max((...).y), math.max((...).z), math.max((...).w))`. + +``` +vector.min(...: vector): vector +``` + +Applies `math.min` to the corresponding components of the input vectors. Includes the fourth component in _4-wide mode_. Equivalent to `vector.create(math.min((...).x), math.min((...).y), math.min((...).z), math.min((...).w))`. diff --git a/.cursor/rules/luau/reference/types-library.md b/.cursor/rules/luau/reference/types-library.md new file mode 100644 index 00000000..b3dcacd8 --- /dev/null +++ b/.cursor/rules/luau/reference/types-library.md @@ -0,0 +1,368 @@ +--- +slug: types-library +title: Type Function Library +sidebar: + order: 2 +--- + +The `types` library is used to create and transform types, and can only be used within [type functions](../types/type-functions). + +### `types` library properties + +``` +types.any +``` + +The [any](../types/basic-types#any-type) `type`. + +``` +types.unknown +``` + +The [unknown](../types/basic-types#unknown-type) `type`. + +``` +types.never +``` + +The [never](../types/basic-types#never-type) `type`. + +``` +types.boolean +``` + +The boolean `type`. + +``` +types.buffer +``` + +The [buffer](../library#buffer-library) `type`. + +``` +types.number +``` + +The number `type`. + +``` +types.string +``` + +The string `type`. + +``` +types.thread +``` + +The thread `type`. + +## `types` library functions + +``` +types.singleton(arg: string | boolean | nil): type +``` + +Returns the [singleton](../types/basic-types#singleton-types-aka-literal-types) type of the argument. + +``` +types.negationof(arg: type): type +``` + +Returns an immutable negation of the argument type. + +``` +types.optional(arg: type): type +``` + +Returns a version of the given type that is now optional. + +- If the given type is a [union type](../types/unions-and-intersections#union-types), `nil` will be added unconditionally as a component. +- Otherwise, the result will be a union of the given type and the `nil` type. + +``` +types.unionof(first: type, second: type, ...: type): type +``` + +Returns an immutable [union](../types/unions-and-intersections#union-types) of two or more arguments. + +``` +types.intersectionof(first: type, second: type, ...: type): type +``` + +Returns an immutable [intersection](../types/unions-and-intersections#intersection-types) of two or more arguments. + +``` +types.newtable(props: { [type]: type | { read: type?, write: type? } }?, indexer: { index: type, readresult: type, writeresult: type? }?, metatable: type?): type +``` + +Returns a fresh, mutable table `type`. Property keys must be string singleton `type`s. The table's metatable is set if one is provided. + +``` +types.newfunction(parameters: { head: {type}?, tail: type? }, returns: { head: {type}?, tail: type? }?, generics: {type}?): type +``` + +Returns a fresh, mutable function `type`, using the ordered parameters of `head` and the variadic tail of `tail`. + +``` +types.copy(arg: type): type +``` + +Returns a deep copy of the argument type. + +``` +types.generic(name: string?, ispack: boolean?): type +``` + +Creates a [generic](../types/generics#generic-functions) named `name`. If `ispack` is `true`, the result is a [generic pack](../types/basic-types#type-packs). + +### `type` instance + +`type` instances can have extra properties and methods described in subsections depending on its tag. + +``` +type.tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "class" | "thread" | "buffer" +``` + +An immutable property holding the type's tag. + +``` +__eq(arg: type): boolean +``` + +Overrides the `==` operator to return `true` if `self` is syntactically equal to `arg`. This excludes semantically equivalent types, `true | false` is unequal to `boolean`. + +``` +type:is(arg: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "class" | "thread" | "buffer") +``` + +Returns `true` if `self` has the argument as its tag. + +### Singleton `type` instance + +``` +singletontype:value(): boolean | nil | "string" +``` + +Returns the singleton's actual value, like `true` for `types.singleton(true)`. + +### Generic `type` instance + +``` +generictype:name(): string? +``` + +Returns the name of the [generic](../types/generics#generic-functions) or `nil` if it has no name. + +``` +generictype:ispack(): boolean +``` + +Returns `true` if the [generic](../types/generics#generic-functions) is a [pack](../types/basic-types#type-packs), or `false` otherwise. + +### Table `type` instance + +``` +tabletype:setproperty(key: type, value: type?) +``` + +Sets the type of the property for the given `key`, using the same type for both reading from and writing to the table. + +- `key` is expected to be a string singleton type, naming the property. +- `value` will be set as both the `read type` and `write type` of the property. +- If `value` is `nil`, the property is removed. + +``` +tabletype:setreadproperty(key: type, value: type?) +``` + +Sets the type for reading from the property named by `key`, leaving the type for writing this property as-is. + +- `key` is expected to be a string singleton type, naming the property. +- `value` will be set as the `read type`, the `write type` will be unchanged. +- If `key` is not already present, only a `read type` will be set, making the property read-only. +- If `value` is `nil`, the property is removed. + +``` +tabletype:setwriteproperty(key: type, value: type?) +``` + + +Sets the type for writing to the property named by `key`, leaving the type for reading this property as-is. + +- `key` is expected to be a string singleton type, naming the property. +- `value` will be set as the `write type`, the `read type` will be unchanged. +- If `key` is not already present, only a `write type` will be set, making the property write-only. +- If `value` is `nil`, the property is removed. + +``` +tabletype:readproperty(key: type): type? +``` + +Returns the type used for reading values from this property, or `nil` if the property doesn't exist. + +``` +tabletype:writeproperty(key: type): type? +``` + +Returns the type used for writing values to this property, or `nil` if the property doesn't exist. + +``` +tabletype:properties(): { [type]: { read: type?, write: type? } } +``` + +Returns a table mapping property keys to their read and write types. + +``` +tabletype:setindexer(index: type, result: type) +``` + +Sets the table's indexer, using the same type for reads and writes. + +``` +tabletype:setreadindexer(index: type, result: type) +``` + +Sets the type resulting from reading from this table via indexing. + +``` +tabletype:setwriteindexer(index: type, result: type) +``` + +Sets the type for writing to this table via indexing. + +``` +tabletype:indexer(): { index: type, readresult: type, writeresult: type } +``` + +Returns the table's indexer as a table, or `nil` if it doesn't exist. + +``` +tabletype:readindexer(): { index: type, result: type }? +``` + +Returns the table's indexer using the result's read type, or `nil` if it doesn't exist. + +``` +tabletype:writeindexer() +``` + +Returns the table's indexer using the result's write type, or `nil` if it doesn't exist. + +``` +tabletype:setmetatable(arg: type) +``` + +Sets the table's metatable. + +``` +tabletype:metatable(): type? +``` + +Gets the table's metatable, or `nil` if it doesn't exist. + +### Function `type` instance + +``` +functiontype:setparameters(head: {type}?, tail: type?) +``` + +Sets the function's parameters, with the ordered parameters in `head` and the variadic tail in `tail`. + +``` +functiontype:parameters(): { head: {type}?, tail: type? } +``` + +Returns the function's parameters, with the ordered parameters in `head` and the variadic tail in `tail`. + +``` +functiontype:setreturns(head: {type}?, tail: type?) +``` + +Sets the function's return types, with the ordered parameters in `head` and the variadic tail in `tail`. + +``` +functiontype:returns(): { head: {type}?, tail: type? } +``` + +Returns the function's return types, with the ordered parameters in `head` and the variadic tail in `tail`. + +``` +functiontype:generics(): {type} +``` + +Returns an array of the function's [generic](../types/generics#generic-functions) `type`s. + +``` +functiontype:setgenerics(generics: {type}?) +``` + +Sets the function's [generic](../types/generics#generic-functions) `type`s. + +### Negation `type` instance + +``` +type:inner(): type +``` + +Returns the `type` being negated. + +### Union `type` instance + +``` +uniontype:components(): {type} +``` + +Returns an array of the [unioned](../types/unions-and-intersections#union-types) types. + +### Intersection `type` instance + +``` +intersectiontype:components() +``` + +Returns an array of the [intersected](../types/unions-and-intersections#intersection-types) types. + +### Class `type` instance + +``` +classtype:properties(): { [type]: { read: type?, write: type? } } +``` + +Returns the properties of the class with their respective `read` and `write` types. + +``` +classtype:readparent(): type? +``` + +Returns the type of reading this class' parent, or returns `nil` if the parent class doesn't exist. + +``` +classtype:writeparent(): type? +``` + +Returns the type for writing to this class' parent, or returns `nil` if the parent class doesn't exist. + +``` +classtype:metatable(): type? +``` + +Returns the class' metatable, or `nil` if it doesn't exist. + +``` +classtype:indexer(): { index: type, readresult: type, writeresult: type }? +``` + +Returns the class' indexer, or `nil` if it doesn't exist. + +``` +classtype:readindexer(): { index: type, result: type }? +``` + +Returns result type of reading from the class via indexing, or `nil` if it doesn't exist. + +``` +classtype:writeindexer(): { index: type, result: type }? +``` + +Returns the type for writing to the class via indexing, or `nil` if it doesn't exist. diff --git a/.cursor/rules/luau/types/basic-types.md b/.cursor/rules/luau/types/basic-types.md new file mode 100644 index 00000000..2e65c804 --- /dev/null +++ b/.cursor/rules/luau/types/basic-types.md @@ -0,0 +1,240 @@ +--- +slug: types/basic-types +title: Primitives and Simple Types +sidebar: + order: 2 +--- + +## Builtin types + +The Luau VM supports 10 primitive types: + +1. `nil` +2. `string` +3. `number` +4. `boolean` +5. `table` +6. `function` +7. `thread` +8. `userdata` +9. `vector` +10. `buffer` + +Most of these can be specified by their name and written directly in type annotations: + +```luau +local message: string = "Hello" +local count: number = 42 +local isReady: boolean = true +local co: thread = coroutine.running() +``` + +Some types have special syntax: +* `table` and `function` are not represented by name, but have their dedicated syntax as covered in this [syntax document](../../syntax) +* `userdata` is represented by [concrete types](../roblox-types), and +* `vector` is not representable by name at all + +The type checker also provides the builtin types [`unknown`](#unknown-type), [`never`](#never-type), and [`any`](#any-type). + +```luau +local s = "foo" +local n = 1 +local b = true +local t = coroutine.running() + +local a: any = 1 +print(a.x) -- Type checker believes this to be ok, but crashes at runtime. +``` + +#### Special behavior with `nil` + +There's a special case where we intentionally avoid inferring `nil` for local variables. This allows you to declare variables first and assign values to them later — if we inferred `nil`, you wouldn't be able to assign other types to these variables. + +```luau +local a +local b = nil +``` + +### `unknown` type + +`unknown` is also said to be the _top_ type, that is it's a union of all types. + +```luau +local a: unknown = "hello world!" +local b: unknown = 5 +local c: unknown = function() return 5 end +``` + +Unlike `any`, `unknown` will not allow itself to be used as a different type! + +```luau +local function unknown(): unknown + return if math.random() > 0.5 then "hello world!" else 5 +end + +local a: string = unknown() -- not ok +local b: number = unknown() -- not ok +local c: string | number = unknown() -- not ok +``` + +In order to turn a variable of type `unknown` into a different type, you must apply [type refinements](../types/refinements.md) on that variable. + +```luau +local function unknown(): unknown return 5 end +local x = unknown() +if typeof(x) == "number" then + print(x) -- x : number +end +``` + +### `never` type + +`never` is also said to be the _bottom_ type, meaning there doesn't exist a value that inhabits the type `never`. In fact, it is the _dual_ of `unknown`. `never` is useful in many scenarios, and one such use case is when type refinements proves it impossible: + +```luau +local function unknown(): unknown return 5 end +local x = unknown() +if typeof(x) == "number" and typeof(x) == "string" then + print(x) -- x : never +end +``` + +### `any` type + +`any` is just like `unknown`, except that it allows itself to be used as an arbitrary type without further checks or annotations. Essentially, it's an opt-out from the type system entirely. + +```luau +local x: any = 5 +local y: string = x -- no type errors here! +``` + +## Function types + +Let's start with something simple. + +```luau +local function f(x) return x end + +local a: number = f(1) -- ok +local b: string = f("foo") -- ok +local c: string = f(true) -- not ok +``` + +In strict mode, the inferred type of this function `f` is `(A) -> A` (take a look at [generics](../generics)), whereas in nonstrict we infer `(any) -> any`. We know this is true because `f` can take anything and then return that. If we used `x` with another concrete type, then we would end up inferring that. + +Similarly, we can infer the types of the parameters with ease. By passing a parameter into *anything* that also has a type, we are saying "this and that has the same type." + +```luau +local function greetingsHelper(name: string) + return "Hello, " .. name +end + +local function greetings(name) + return greetingsHelper(name) +end + +print(greetings("Alexander")) -- ok +print(greetings({name = "Alexander"})) -- not ok +``` + +## Variadic types + +Luau permits assigning a type to the `...` variadic symbol like any other parameter: + +```luau +local function f(...: number) +end + +f(1, 2, 3) -- ok +f(1, "string") -- not ok +``` + +`f` accepts any number of `number` values. + +In type annotations, this is written as `...T`: + +```luau +type F = (...number) -> ...string +``` + +## Type packs + +Multiple function return values as well as the function variadic parameter use a type pack to represent a list of types. + +When a type alias is defined, generic type pack parameters can be used after the type parameters: + +```luau +type Signal = { f: (T, U...) -> (), data: T } +``` + +> Keep in mind that `...T` is a variadic type pack (many elements of the same type `T`), while `U...` is a generic type pack that can contain zero or more types and they don't have to be the same. + +It is also possible for a generic function to reference a generic type pack from the generics list: + +```luau +type Signal = { f: (T, U...) -> (), data: T } + +local function call(s: Signal, ...: U...) + s.f(s.data, ...) +end +``` + +Generic types with type packs can be instantiated by providing a type pack: + +```luau +type Signal = { f: (T, U...) -> (), data: T } +local function call(s: Signal, ...: U...) end + +local signal: Signal = {} :: any + +call(signal, 1, 2, false) +``` + +There are also other ways to instantiate types with generic type pack parameters: + +```luau +type A = (T) -> U... + +type B = A -- with a variadic type pack +type C = A -- with a generic type pack +type D = A -- with an empty type pack +``` + +Trailing type pack argument can also be provided without parentheses by specifying variadic type arguments: + +```luau +type List = (Head, Rest...) -> () + +type B = List -- Rest... is () +type C = List -- Rest is (string, boolean) + +type Returns = () -> T... + +-- When there are no type parameters, the list can be left empty +type D = Returns<> -- T... is () +``` + +Type pack parameters are not limited to a single one, as many as required can be specified: + +```luau +type Callback = { f: (Args...) -> Rets... } + +type A = Callback<(number, string), ...number> +``` + +## Singleton types (aka literal types) + +Luau's type system also supports singleton types, which means it's a type that represents one single value at runtime. At this time, both string and booleans are representable in types. + +> We do not currently support numbers as types. For now, this is intentional. + +```luau +local foo: "Foo" = "Foo" -- ok +local bar: "Bar" = foo -- not ok +local baz: string = foo -- ok + +local t: true = true -- ok +local f: false = false -- ok +``` + +This happens all the time, especially through [type refinements](../type-refinements) and is also incredibly useful when you want to enforce program invariants in the type system! See [tagged unions](../unions-and-intersections/#tagged-unions) for more information. diff --git a/.cursor/rules/luau/types/considerations.md b/.cursor/rules/luau/types/considerations.md new file mode 100644 index 00000000..33c01b3d --- /dev/null +++ b/.cursor/rules/luau/types/considerations.md @@ -0,0 +1,42 @@ +--- +slug: types/considerations +title: Additional Considerations +sidebar: + order: 10 +--- + +## Module interactions + +Let's say that we have two modules, `Foo` and `Bar`. Luau will try to resolve the paths if it can find any `require` in any scripts. In this case, when you say `./bar`, Luau will resolve it as: relative to this script, go to my sibling script named Bar. + +```luau +--!file foo.luau +local Bar = require("./bar") + +local baz1: Bar.Baz = 1 -- not ok +local baz2: Bar.Baz = "foo" -- ok + +print(Bar.Quux) -- ok +print(Bar.FakeProperty) -- not ok + +Bar.NewProperty = true -- not ok + +--!file bar.luau +export type Baz = string + +local module = {} + +module.Quux = "Hello, world!" + +return module +``` + +There are some caveats here though. For instance, the require path must be resolvable statically, otherwise Luau cannot accurately type check it. + +### Cyclic module dependencies + +Cyclic module dependencies can cause problems for the type checker. In order to break a module dependency cycle a typecast of the module to `any` may be used: +```luau +--!hidden mode=nocheck +local myModule = require(MyModule) :: any +``` diff --git a/.cursor/rules/luau/types/generics.md b/.cursor/rules/luau/types/generics.md new file mode 100644 index 00000000..0adb651d --- /dev/null +++ b/.cursor/rules/luau/types/generics.md @@ -0,0 +1,71 @@ +--- +slug: types/generics +title: Generics and Polymorphism +sidebar: + order: 5 +--- + +The type inference engine was built from the ground up to recognize generics. A generic is simply a type parameter in which another type could be slotted in. It's extremely useful because it allows the type inference engine to remember what the type actually is, unlike `any`. + +```luau +type Pair = {first: T, second: T } +-- generics can also have defaults! +type PairWithDefault = Pair + +local strings: Pair = { first="Hello", second="World" } +local numbers: Pair = { first=1, second=2 } +-- can just treat PairWithDefault as a type that doesnt have generics because it has all of its generics assigned a default! +local more_strings: PairWithDefault = { first = "meow", second = "mrrp" } +``` + +## Generic functions + +As well as generic type aliases like `Pair`, Luau supports generic functions. These are functions that, as well as their regular data parameters, take type parameters. For example, a function which reverses an array is: +```luau +function reverse(a) + local result = {} + for i = #a, 1, -1 do + table.insert(result, a[i]) + end + return result +end +``` +The type of this function is that it can reverse an array, and return an array of the same type. Luau can infer this type, but if you want to be explicit, you can declare the type parameter `T`, for example: +```luau +function reverse(a: {T}): {T} + local result: {T} = {} + for i = #a, 1, -1 do + table.insert(result, a[i]) + end + return result +end +``` +When a generic function is called, Luau infers type arguments, for example +```luau +--!file main.luau +local reverse = require("./reverse") +local x: {number} = reverse({1, 2, 3}) +local y: {string} = reverse({"a", "b", "c"}) + +--!file reverse.luau +function reverse(a: {T}): {T} + local result: {T} = {} + for i = #a, 1, -1 do + table.insert(result, a[i]) + end + return result +end + +return reverse +``` +Generic types are used for built-in functions as well as user functions, +for example the type of two-argument `table.insert` is: +``` +({T}, T) -> () +``` +Note: Functions don't support having defaults assigned to generics, meaning the following is invalid +```luau +function meow(mrrp: T) + print(mrrp .. " :3") +end +``` diff --git a/.cursor/rules/luau/types/object-oriented-programs.md b/.cursor/rules/luau/types/object-oriented-programs.md new file mode 100644 index 00000000..d749c667 --- /dev/null +++ b/.cursor/rules/luau/types/object-oriented-programs.md @@ -0,0 +1,84 @@ +--- +slug: types/object-oriented-programs +title: Object-Oriented Programming +sidebar: + order: 7 +--- + +## Adding types for faux object oriented programs + +One common pattern we see with existing Lua/Luau code is the following object-oriented code. While Luau is capable of inferring a decent chunk of this code, it cannot pin down on the types of `self` when it spans multiple methods. + +```luau +local Account = {} +Account.__index = Account + +function Account.new(name, balance) + local self = {} + self.name = name + self.balance = balance + + return setmetatable(self, Account) +end + +-- The `self` type is different from the type returned by `Account.new` +function Account:deposit(credit) + self.balance += credit +end + +-- The `self` type is different from the type returned by `Account.new` +function Account:withdraw(debit) + self.balance -= debit +end + +local account = Account.new("Alexander", 500) +``` + +For example, the type of `Account.new` is `(name: a, balance: b) -> { ..., name: a, balance: b, ... }` (snipping out the metatable). For better or worse, this means you are allowed to call `Account.new(5, "hello")` as well as `Account.new({}, {})`. In this case, this is quite unfortunate, so your first attempt may be to add type annotations to the parameters `name` and `balance`. + +There's the next problem: the type of `self` is not shared across methods of `Account`, this is because you are allowed to explicitly opt for a different value to pass as `self` by writing `account.deposit(another_account, 50)`. As a result, the type of `Account:deposit` is `(self: { balance: a }, credit: b) -> ()`. Consequently, Luau cannot infer the result of the `+` operation from `a` and `b`, so a type error is reported. + +We can see there's a lot of problems happening here. This is a case where you'll have to provide some guidance to Luau in the form of annotations today, but the process is straightforward and without repetition. You first specify the type of _data_ you want your class to have, and then you define the class type separately with `setmetatable` (either via `typeof`, or in the New Type Solver, the `setmetatable` type function). +From then on, you can explicitly annotate the `self` type of each method with your class type! Note that while the definition is written e.g. `Account.deposit`, you can still call it as `account:deposit(...)`. + +```luau +local Account = {} +Account.__index = Account + +type AccountData = { + name: string, + balance: number, +} + +export type Account = typeof(setmetatable({} :: AccountData, Account)) +-- or alternatively, in the new type solver... +-- export type Account = setmetatable + + +-- this return annotation is not required, but ensures that you cannot +-- accidentally make the constructor incompatible with the methods +function Account.new(name, balance): Account + local self = {} + self.name = name + self.balance = balance + + return setmetatable(self, Account) +end + +-- this annotation on `self` is currently a _required_ annotation. +function Account.deposit(self: Account, credit: number) + -- autocomplete on `self` works here! + self.balance += credit +end + +-- this annotation on `self` is currently a _required_ annotation. +function Account.withdraw(self: Account, debit: number) + -- autocomplete on `self` works here! + self.balance -= debit +end + +local account = Account.new("Hina", 500) +account:deposit(20) -- this still works, and we had autocomplete after hitting `:`! +``` + +Based on feedback, we plan to restrict the types of all functions defined with `:` syntax to [share their self types](https://rfcs.luau.org/shared-self-types.html). This will enable future versions of this code to work without any explicit `self` annotations because it amounts to having type inference make precisely the assumptions we are encoding with annotations here --- namely, that the type of the constructors and the method definitions is intended by the developer to be the same. diff --git a/.cursor/rules/luau/types/overview.md b/.cursor/rules/luau/types/overview.md new file mode 100644 index 00000000..3d4226ba --- /dev/null +++ b/.cursor/rules/luau/types/overview.md @@ -0,0 +1,160 @@ +--- +slug: types +title: An introduction to Luau types +sidebar: + order: 1 +--- + +Luau supports a gradual type system through the use of type annotations and type inference. These types are used to provide warnings, errors, and suggestions for our developers. Type checking helps you find bugs early - while you're writing code - rather than discovering when your program crashes at runtime. + +## Type inference modes + +Luau offers three different modes that control how strictly it checks your types. You can set the mode by adding one of the following to the top of your file: + +* `--!nocheck`, +* `--!nonstrict` (default), and +* `--!strict` + +##### `--!nocheck` + +`nocheck` mode completely disables the type inference engine for the file. + +With this mode enabled, we don't provide any feedback on the types, so scripts with errors (like following example that tries to add a `string` and a `number`) will not raise a typecheck error, even though the program errors when executed. + +Try pressing the `Check` and `Run` buttons to try out this snippet: + +```luau +--!nocheck + +local foo = 1 +local x = "hello " + foo -- No error, type checking is disabled +print(foo) +``` + +If, instead, you want to catch these kinds of errors before running your code, you can enable type checking with the `--!nonstrict` or `--!strict` modes. Both of these modes analyze your types and provide helpful feedback, but they differ slightly in how strictly they enforce type safety. + +##### `--!nonstrict` + +In `nonstrict` mode, the type checker is more forgiving: if we can't figure out what type something is early on, we infer the type could be anything (the `any` type) and let you proceed without errors. + +This means that if we define a variable without initializing it or specifying its type: + +```luau +--!nonstrict + +local foo +foo = 1 +foo = "hello " + foo -- Still no error, foo is 'any' +``` + +The type checker can't tell what type `foo` should be when it's first declared, so in `nonstrict` mode it infers `any` - allowing the addition to proceed without warnings, even though it will error at runtime. + +##### `--!strict` + +In `strict` mode, Luau is smarter about tracking types across statements. Given the previous example now in `strict` mode: + +```luau +--!strict + +local foo +foo = 1 +foo = "hello " + foo +``` + +We see an error! The type checker sees `foo = 1` on line 4 and infers that `foo` must be a `number`. Then, when you try to add `"hello "` and `foo`, `strict` mode catches our error of trying to add a `string` and a `number`, which isn't allowed. (tip: hover over the erroneous lines to see the error message!) + +## Structural type system + +Luau's type system is structural by default, which is to say that we inspect the shape of two tables to see if they are similar enough. This was chosen because Lua 5.1 is inherently structural. + +Take, for example, these two tables `A` and `B`: + +```luau +--!hidden FIXME(LUAU): there's a bug in the new type solver unfortunately +--!hidden solver=old +type A = {x: number, y: number, z: number?} +type B = {x: number, y: number, z: number} + +local a1: A = {x = 1, y = 2} -- ok +local b1: B = {x = 1, y = 2, z = 3} -- ok + +local a2: A = b1 -- ok +local b2: B = a1 -- not ok +``` + +## Type annotations + +While Luau's type system can infer many types, you can also annotate types using a colon (`:`) followed by the type: + +```luau +-- Variable annotations +local age: number = 25 +local names: {string} = {"Alice", "Bob"} + +-- Function annotations +function greet(name: string): string + return "Hello, " .. name +end + +-- Table type annotations +type Person = { + name: string, + age: number, + email: string? +} + +local user: Person = { + name = "Alice", + age = 30 +} + +print(greet(user.name)) +``` + +## Type casts + +Sometimes, you might want to specify the type of an expression when the automatically inferred type is too generic. In these cases, you can use a type cast with the `::` operator to tell Luau what type something should be. + +For example, consider the following table constructor where the intent is to store a table of names: + +```luau +local myTable = {names = {}} +table.insert(myTable.names, 42) +``` + +Inserting a number into `names` ought to cause a type error, but doesn't. + +In order to specify the type of the `names` table, we can use a typecast: + +```luau +local myTable = {names = {} :: {string}} +table.insert(myTable.names, 42) +``` + +Now, inserting a number raises a type error, informing the user that there is an `invalid 'number' to 'string' conversion`. + +### Type cast rules + +Type casts themselves are also type checked to ensure that one of the conversion operands is the subtype of the other or `any`: + +```luau +local numericValue = 1 +local value = numericValue :: any -- ok, all expressions may be cast to 'any' +local flag = numericValue :: boolean -- not ok, invalid 'number' to 'boolean' conversion +``` + +This prevents unsafe casts between incompatible types. + +#### Type casts with multiple return values + +When typecasting a variadic or the result of a function with multiple returns, only the first value will be preserved, and the rest discarded: + +```luau +function returnsMultiple(...): (number, number, number) + print(... :: string) -- "x" + return 1, 2, 3 +end + +print(returnsMultiple("x", "y", "z")) -- 1, 2, 3 +print(returnsMultiple("x", "y", "z") :: number) -- 1 +``` diff --git a/.cursor/rules/luau/types/refinements.md b/.cursor/rules/luau/types/refinements.md new file mode 100644 index 00000000..66e755e0 --- /dev/null +++ b/.cursor/rules/luau/types/refinements.md @@ -0,0 +1,82 @@ +--- +slug: types/type-refinements +title: Type Refinements +sidebar: + order: 6 +--- + +When we check the type of any lvalue (a global, a local, or a property), what we're doing is we're refining the type, hence "type refinement." The support for this is arbitrarily complex, so go at it! + +Here are all the ways you can refine: +1. Truthy test: `if x then` will refine `x` to be truthy. +2. Type guards: `if type(x) == "number" then` will refine `x` to be `number`. +3. Equality: `if x == "hello" then` will refine `x` to be a singleton type `"hello"`. + +And they can be composed with many of `and`/`or`/`not`. `not`, just like `~=`, will flip the resulting refinements, that is `not x` will refine `x` to be falsy. + +The `assert(..)` function may also be used to refine types instead of `if/then`. + +Using truthy test: +```luau +local maybeString: string? = nil + +if maybeString then + local onlyString: string = maybeString -- ok + local onlyNil: nil = maybeString -- not ok +end + +if not maybeString then + local onlyString: string = maybeString -- not ok + local onlyNil: nil = maybeString -- ok +end +``` + +Using `type` test: +```luau +local stringOrNumber: string | number = "foo" + +if type(stringOrNumber) == "string" then + local onlyString: string = stringOrNumber -- ok + local onlyNumber: number = stringOrNumber -- not ok +end + +if type(stringOrNumber) ~= "string" then + local onlyString: string = stringOrNumber -- not ok + local onlyNumber: number = stringOrNumber -- ok +end +``` + +Using equality test: +```luau +function f() return "meow" end +local myString: string = f() + +if myString == "hello" then + local hello: "hello" = myString -- ok because it is absolutely "hello"! + local copy: string = myString -- ok +end +``` + +And as said earlier, we can compose as many of `and`/`or`/`not` as we wish with these refinements: +```luau +local function f(x: any, y: any) + if (x == "hello" or x == "bye") and type(y) == "string" then + -- x is of type "hello" | "bye" + -- y is of type string + end + + if not (x ~= "hi") then + -- x is of type "hi" + end +end +``` + +`assert` can also be used to refine in all the same ways: +```luau +local stringOrNumber: string | number = "foo" + +assert(type(stringOrNumber) == "string") + +local onlyString: string = stringOrNumber -- ok +local onlyNumber: number = stringOrNumber -- not ok +``` diff --git a/.cursor/rules/luau/types/roblox-types.md b/.cursor/rules/luau/types/roblox-types.md new file mode 100644 index 00000000..3430d618 --- /dev/null +++ b/.cursor/rules/luau/types/roblox-types.md @@ -0,0 +1,34 @@ +--- +slug: types/roblox-types +title: Foreign Types from the Embedder +sidebar: + order: 9 +--- + +Roblox supports a rich set of classes and data types, [documented here](https://developer.roblox.com/en-us/api-reference). All of them are readily available for the type checker to use by their name (e.g. `Part` or `RaycastResult`). + +When one type inherits from another type, the type checker models this relationship and allows to cast a subclass to the parent class implicitly, so you can pass a `Part` to a function that expects an `Instance`. + +All enums are also available to use by their name as part of the `Enum` type library, e.g. `local m: Enum.Material = part.Material`. + +We can automatically deduce what calls like `Instance.new` and `game:GetService` are supposed to return: + +```luau +--!hidden mode=nocheck +local part = Instance.new("Part") +local basePart: BasePart = part +``` + +Finally, Roblox types can be refined using `IsA`: + +```luau +--!hidden mode=nocheck +local function getText(x : Instance) : string + if x:IsA("TextLabel") or x:IsA("TextButton") or x:IsA("TextBox") then + return child.Text + end + return "" +end +``` + +Note that many of these types provide some properties and methods in both lowerCase and UpperCase; the lowerCase variants are deprecated, and the type system will ask you to use the UpperCase variants instead. diff --git a/.cursor/rules/luau/types/tables.md b/.cursor/rules/luau/types/tables.md new file mode 100644 index 00000000..11aa8b36 --- /dev/null +++ b/.cursor/rules/luau/types/tables.md @@ -0,0 +1,87 @@ +--- +slug: types/tables +title: Table Types +sidebar: + order: 3 +--- + +From the type checker perspective, each table can be in one of three states. They are: `unsealed table`, `sealed table`, and `generic table`. This is intended to represent how the table's type is allowed to change. + +### Unsealed tables + +An unsealed table is a table which supports adding new properties, which updates the tables type. Unsealed tables are created using table literals. This is one way to accumulate knowledge of the shape of this table. + +```luau +local t = {x = 1} -- {x: number} +t.y = 2 -- {x: number, y: number} +t.z = 3 -- {x: number, y: number, z: number} +``` + +However, if this local were written as `local t: { x: number } = { x = 1 }`, it ends up sealing the table, so the two assignments henceforth will not be ok. + +Furthermore, once we exit the scope where this unsealed table was created in, we seal it. + +```luau +local function vec2(x, y) + local t = {} + t.x = x + t.y = y + return t +end + +local v2 = vec2(1, 2) +v2.z = 3 -- not ok +``` + +Unsealed tables are *exact* in that any property of the table must be named by the type. Since Luau treats missing properties as having value `nil`, this means that we can treat an unsealed table which does not mention a property as if it mentioned the property, as long as that property is optional. + +```luau +local t = {x = 1} +local u : { x : number, y : number? } = t -- ok because y is optional +local v : { x : number, z : number } = t -- not ok because z is not optional +``` + +### Sealed tables + +A sealed table is a table that is now locked down. This occurs when the table type is spelled out explicitly via a type annotation, or if it is returned from a function. + +```luau +local t : { x: number } = {x = 1} +t.y = 2 -- not ok +``` + +Sealed tables are *inexact* in that the table may have properties which are not mentioned in the type. +As a result, sealed tables support *width subtyping*, which allows a table with more properties to be used as a table with fewer properties. + +```luau +type Point1D = { x : number } +type Point2D = { x : number, y : number } +local p : Point2D = { x = 5, y = 37 } +local q : Point1D = p -- ok because Point2D has more properties than Point1D +``` + +### Generic tables + +This typically occurs when the symbol does not have any annotated types or were not inferred anything concrete. In this case, when you index on a parameter, you're requesting that there is a table with a matching interface. + +```luau +local function f(t) + return t.x + t.y + --^ --^ {x: _, y: _} +end + +f({x = 1, y = 2}) -- ok +f({x = 1, y = 2, z = 3}) -- ok +f({x = 1}) -- not ok +``` + +## Table indexers + +These are particularly useful for when your table is used similarly to an array. + +```luau +local t = {"Hello", "world!"} -- {[number]: string} +print(table.concat(t, ", ")) +``` + +Luau supports a concise declaration for array-like tables, `{T}` (for example, `{string}` is equivalent to `{[number]: string}`); the more explicit definition of an indexer is still useful when the key isn't a number, or when the table has other fields like `{ [number]: string, n: number }`. diff --git a/.cursor/rules/luau/types/type-functions.md b/.cursor/rules/luau/types/type-functions.md new file mode 100644 index 00000000..3da01d23 --- /dev/null +++ b/.cursor/rules/luau/types/type-functions.md @@ -0,0 +1,53 @@ +--- +slug: types/type-functions +title: Type Functions +sidebar: + order: 8 +--- + +Type functions are functions that run during analysis time and operate on types, instead of runtime values. They can use the [types](../../types-library) library to transform existing types or create new ones. + + +Here's a simplified implementation of the builtin type function `keyof`. It takes a table type and returns its property names as a [union](../unions-and-intersections#union-types) of [singletons](../basic-types#singleton-types-aka-literal-types). + +```luau +type function simple_keyof(ty) + -- Ignoring unions or intersections of tables for simplicity. + if not ty:is("table") then + error("Can only call keyof on tables.") + end + + local union = nil + + for property in ty:properties() do + union = if union then types.unionof(union, property) else property + end + + return if union then union else types.singleton(nil) +end + +type person = { + name: string, + age: number, +} +--- keys = "age" | "name" +type keys = simple_keyof +``` + +### Type function environment + +In addition to the [types](../../types-library) library, type functions have access to: + +* `assert`, `error`, `print` +* `next`, `ipairs`, `pairs` +* `select`, `unpack` +* `getmetatable`, `setmetatable` +* `rawget`, `rawset`, `rawlen`, `raweq` +* `tonumber`, `tostring` +* `type`, `typeof` +* `math` library +* `table` library +* `string` library +* `bit32` library +* `utf8` library +* `buffer` library diff --git a/.cursor/rules/luau/types/unions-and-intersections.md b/.cursor/rules/luau/types/unions-and-intersections.md new file mode 100644 index 00000000..22bef3c5 --- /dev/null +++ b/.cursor/rules/luau/types/unions-and-intersections.md @@ -0,0 +1,92 @@ +--- +slug: types/unions-and-intersections +title: Union and Intersection Types +sidebar: + order: 4 +--- + +## Union types + +A union type represents *one of* the types in this set. If you try to pass a union onto another thing that expects a *more specific* type, it will fail. + +For example, what if this `string | number` was passed into something that expects `number`, but the passed in value was actually a `string`? + +```luau +local stringOrNumber: string | number = "foo" + +local onlyString: string = stringOrNumber -- not ok +local onlyNumber: number = stringOrNumber -- not ok +``` + +Note: it's impossible to be able to call a function if there are two or more function types in this union. + +### Tagged unions + +Tagged unions are just union types! In particular, they're union types of tables where they have at least _some_ common properties but the structure of the tables are different enough. Here's one example: + +```luau +type Ok = { type: "ok", value: T } +type Err = { type: "err", error: E } +type Result = Ok | Err +``` + +This `Result` type can be discriminated by using type refinements on the property `type`, like so: + +```luau +--!file main.luau +local res = require("./result") +local result: res.Result = res.something() + +if result.type == "ok" then + -- result is known to be Ok + -- and attempting to index for error here will fail + print(result.value) +elseif result.type == "err" then + -- result is known to be Err + -- and attempting to index for value here will fail + print(result.error) +end + +--!file result.luau +export type Ok = { type: "ok", value: T } +export type Err = { type: "err", error: E } +export type Result = Ok | Err + +return { + something = function(): any return {} end +} +``` + +Which works out because `value: T` exists only when `type` is in actual fact `"ok"`, and `error: E` exists only when `type` is in actual fact `"err"`. + + +## Intersection types + +An intersection type represents *all of* the types in this set. It's useful for two main things: to join multiple tables together, or to specify overloadable functions. + +```luau +type XCoord = {x: number} +type YCoord = {y: number} +type ZCoord = {z: number} + +type Vector2 = XCoord & YCoord +type Vector3 = XCoord & YCoord & ZCoord + +local vec2: Vector2 = {x = 1, y = 2} -- ok +local vec3: Vector3 = {x = 1, y = 2, z = 3} -- ok +``` + +```luau +type SimpleOverloadedFunction = ((string) -> number) & ((number) -> string) + +local f: SimpleOverloadedFunction + +local r1: number = f("foo") -- ok +local r2: number = f(12345) -- not ok +local r3: string = f("foo") -- not ok +local r4: string = f(12345) -- ok +``` + +Note: it's impossible to create an intersection type of some primitive types, e.g. `string & number`, or `string & boolean`, or other variations thereof. + +Note: Luau still does not support user-defined overloaded functions. Some of Roblox and Lua 5.1 functions have different function signature, so inherently requires overloaded functions. diff --git a/.cursor/rules/roblox/common/navigation/platform.yaml b/.cursor/rules/roblox/common/navigation/platform.yaml index a72017cc..faaa7de4 100644 --- a/.cursor/rules/roblox/common/navigation/platform.yaml +++ b/.cursor/rules/roblox/common/navigation/platform.yaml @@ -29,6 +29,8 @@ navigation: path: /safety - title: Content maturity path: /production/promotion/content-maturity + - title: Regional content availability + path: /production/promotion/regional-content-availability - title: GDPR and CCPA path: /production/publishing/about-GDPR-and-CCPA - title: DMCA diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Female-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Female-Voice.wav new file mode 100644 index 00000000..c4507997 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Female-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e0921ee71090a803beb7eabe18adca534ff0dc92b0ba2cb2568cc404a36800e +size 175148 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Male-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Male-Voice.wav new file mode 100644 index 00000000..75139d96 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/French-Male-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1b38f276f16df08f3422d976a76359e0cab2d78fa6483df161051c593cf21ea +size 175148 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Female-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Female-Voice.wav new file mode 100644 index 00000000..c95fbb5f --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Female-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ac6e95ad2327743f7637c2a4bc1cfbfb7a166084b6f9cd303342bba2fd54bf8 +size 144940 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Male-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Male-Voice.wav new file mode 100644 index 00000000..c7482bb2 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/German-Male-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a0b87d69c69545a1a2ef7cd657429f4831a3e5a5d1cd86b881659856fce792e +size 121900 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Female-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Female-Voice.wav new file mode 100644 index 00000000..cf2c0d07 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Female-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2ea8e0ffa988b70b9b61fa631dfd528794f83478c2691e1295ea4c042173db6 +size 118828 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Male-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Male-Voice.wav new file mode 100644 index 00000000..472dac1b --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Italian-Male-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa9a65c96fa51876e32adb2a87d2a209f4fd51929402fccd56cc531fc7180eca +size 94252 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Narrator-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Narrator-Voice.wav new file mode 100644 index 00000000..300c9e2b --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Narrator-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02205bdcf3da43616fec1ae4e4d584110f7671dbfac524336a878154ed391210 +size 182316 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Female-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Female-Voice.wav new file mode 100644 index 00000000..64da9cf7 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Female-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d56a65949dbb7dc2be7b10a945076ac64f47f9d645d42a5d62056251fe61910 +size 186412 diff --git a/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Male-Voice.wav b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Male-Voice.wav new file mode 100644 index 00000000..efcadcea --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/audio/audio-objects/Spanish-Male-Voice.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fb305e6b4bd54763aecc43b79abf20b9679bbd02a485bc169e1c7174bdc8ebd +size 143404 diff --git a/.cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Extended.png b/.cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Extended.png new file mode 100644 index 00000000..bacbeebe --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Extended.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19b1f77eddc0d4055712a37aeb0e7d41cbe388e8b979a4920b79943e736eb494 +size 89138 diff --git a/.cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Mono.png b/.cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Mono.png new file mode 100644 index 00000000..9c5df783 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/engine-api/datatypes/Font/Builder-Mono.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6275a0279861f5edecabd85d6f1017ac7b302ab3117028fe9781184b60b296ee +size 59182 diff --git a/.cursor/rules/roblox/en-us/assets/physics/constraints/AnimationConstraint-IsKinematic.mp4 b/.cursor/rules/roblox/en-us/assets/physics/constraints/AnimationConstraint-IsKinematic.mp4 new file mode 100644 index 00000000..838214d7 --- /dev/null +++ b/.cursor/rules/roblox/en-us/assets/physics/constraints/AnimationConstraint-IsKinematic.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d108ca2ff0e99dd882551f519141c2150ac4859872779574a99aeec858af7001 +size 3278127 diff --git a/.cursor/rules/roblox/en-us/audio/objects.md b/.cursor/rules/roblox/en-us/audio/objects.md index 1ffb2a40..8d7096cf 100644 --- a/.cursor/rules/roblox/en-us/audio/objects.md +++ b/.cursor/rules/roblox/en-us/audio/objects.md @@ -223,7 +223,52 @@ To play 2D text-to-speech audio: 10 Retro voice #2 - + + + 11 + Host voice + + + + 101 + Spanish male + + + + 102 + Spanish female + + + + 201 + German male + + + + 202 + German female + + + + 301 + Italian male + + + + 302 + Italian female + + + + 401 + French male + + + + 402 + French female + + diff --git a/.cursor/rules/roblox/en-us/get-started/monetization.md b/.cursor/rules/roblox/en-us/get-started/monetization.md index 1d6ced53..4dc8ef85 100644 --- a/.cursor/rules/roblox/en-us/get-started/monetization.md +++ b/.cursor/rules/roblox/en-us/get-started/monetization.md @@ -88,16 +88,15 @@ fullWidth: true

Daily engagement

- Earn a fixed payout of 5 Robux when a user who has spent at least $9.99 on the platform in the last 60 days (an "Active Spender") plays your game for 10 minutes or more. This reward applies to the first three experiences they play per day. * + Earn a fixed payout of 5 Robux when a user who has spent at least $9.99 on the platform in the last 60 days (an "Active Spender") plays your game for 10 minutes or more. This reward applies to the first three experiences they play per day.

Audience expansion

- Earn a 35% revenue share on the first $100 in Robux purchases made by a new or returning user who finds your experience through a direct link or search, and plays for at least 10 minutes. _ + Earn a 35% revenue share on the first $100 in Robux purchases made by a new or returning user who finds your experience through a direct link or search, and plays for at least 10 minutes.1



1 Less qualifying purchases they have made in the 60 days after each time (if any) they have previously reactivated. Additional restrictions apply.
- _ Less Qualifying Purchases they have made in the 60 days after each time (if any) they have previously reactivated). Additional restrictions apply. diff --git a/.cursor/rules/roblox/en-us/get-started/why-build-on-roblox.md b/.cursor/rules/roblox/en-us/get-started/why-build-on-roblox.md index 8d624296..bb268944 100644 --- a/.cursor/rules/roblox/en-us/get-started/why-build-on-roblox.md +++ b/.cursor/rules/roblox/en-us/get-started/why-build-on-roblox.md @@ -5,40 +5,6 @@ hideInPageNavigation: true fullWidth: true --- -

Where imagination meets a global audience

- - - - -

Massive global reach

- Tap into a platform with 111.8 million daily active users (DAU), spanning 180 countries and 17 languages.

- *As of Q1 2025 -
-
- - -

Deep user engagement

- Capture attention on a platform where users spend an average of 2.7 hours per day, totaling 27.4 billion hours of engagement.

- *As of Q1 2025 -
-
- - -

Proven developer success

- On average, the top 10,000 developers earned over $97,000 in 2024. -
-
-
- - - DAU by Region (in millions) - -
-

Robust tools with no upfront costs

diff --git a/.cursor/rules/roblox/en-us/parts/model-generation.md b/.cursor/rules/roblox/en-us/parts/model-generation.md index 77353e18..8d79664b 100644 --- a/.cursor/rules/roblox/en-us/parts/model-generation.md +++ b/.cursor/rules/roblox/en-us/parts/model-generation.md @@ -116,7 +116,7 @@ style FunctionalModel color:white, fill:SeaGreen To test retargetable scripts: -1. Download the
`Behaviors.rbxm` file (right‑click and "save as" to your local system). +1. Download the `Behaviors.rbxm` file. 2. In Studio's [Explorer](../studio/explorer.md) hierarchy, right‑click `Class.ReplicatedStorage`, select **Insert** ⟩ **Import Roblox Model**, and choose the downloaded file. The model unpacks into several folders. diff --git a/.cursor/rules/roblox/en-us/parts/textures-decals.md b/.cursor/rules/roblox/en-us/parts/textures-decals.md index a46a5b33..849cf803 100644 --- a/.cursor/rules/roblox/en-us/parts/textures-decals.md +++ b/.cursor/rules/roblox/en-us/parts/textures-decals.md @@ -21,7 +21,7 @@ add a `Class.Texture` or `Class.Decal` object to a part or union, you can:
- The same blue texture repeated eight times on a block part. + The same texture repeated eight times on a block part.
Texture applied to a part (repeating)
@@ -31,13 +31,13 @@ add a `Class.Texture` or `Class.Decal` object to a part or union, you can:
An example decal image of a light purple hexagon on top of a dark purple background. -
Texture image
+
Decal image
- The same blue texture repeated eight times on a block part. -
Texture applied to a part (repeating)
+ The same decal stretched on a block part. +
Decal applied to a part (stretched)
diff --git a/.cursor/rules/roblox/en-us/physics/constraints/animation.md b/.cursor/rules/roblox/en-us/physics/constraints/animation.md index 39ac3690..b7de66fa 100644 --- a/.cursor/rules/roblox/en-us/physics/constraints/animation.md +++ b/.cursor/rules/roblox/en-us/physics/constraints/animation.md @@ -23,6 +23,10 @@ The constraint's `Class.AnimationConstraint.Transform|Transform` is the internal When the constraint's `Class.AnimationConstraint.IsKinematic|IsKinematic` property is `true`, the connected parts snap to the [transform](#transform) instantly and precisely without participating in physics simulation. When `false`, the connected parts follow the trajectory using [force and torque](#force-and-torque). +
+ +
+ ## Force and torque -If the constraint is not [kinematic](#kinematic), the connected parts follow a trajectory toward the [transform](#transform) using a maximum force/torque limited by `Class.AnimationConstraint.MaxForce|MaxForce` and `Class.AnimationConstraint.MaxTorque|MaxTorque` respectively. +If the constraint is **not** set to [kinematic](#kinematic), the connected parts follow a trajectory toward the [transform](#transform) using a maximum force/torque limited by `Class.AnimationConstraint.MaxForce|MaxForce` and `Class.AnimationConstraint.MaxTorque|MaxTorque` respectively. diff --git a/.cursor/rules/roblox/en-us/production/creator-store.md b/.cursor/rules/roblox/en-us/production/creator-store.md index 754a7980..e0fd5710 100644 --- a/.cursor/rules/roblox/en-us/production/creator-store.md +++ b/.cursor/rules/roblox/en-us/production/creator-store.md @@ -50,6 +50,8 @@ In addition to being able to distribute more of each asset type, verifying your For information on how to verify your account either through a government-issued ID or through a phone number, see [account verification](../production/publishing/account-verification.md). +Verification status for group-owned assets is based on the user that last updated the asset. + **Phone number verification is not sufficient** to sell priced assets on the Creator Store. To sell priced assets, you need to verify with a [government ID](./publishing/account-verification.md#verify-through-government-id) and create a seller account. For more information, see [Sell on the Creator Store](./sell-on-creator-store.md). diff --git a/.cursor/rules/roblox/en-us/production/monetization/immersive-ads.md b/.cursor/rules/roblox/en-us/production/monetization/immersive-ads.md index bf18358c..c1b4eb12 100644 --- a/.cursor/rules/roblox/en-us/production/monetization/immersive-ads.md +++ b/.cursor/rules/roblox/en-us/production/monetization/immersive-ads.md @@ -78,27 +78,28 @@ Roblox pays out earnings on the 25th of the following month from when you insert ## Publisher eligibility -While anyone can insert ad units into their experiences, Roblox only serves ads into ad units if the experience or group owner and experience is eligible to serve ads. Eligibility does not guarantee Roblox will serve ads in your experience and nothing obligates Roblox to serve ads into your experience. +While anyone can insert ad units into their experiences, Roblox only serves ads into ad units if the experience is eligible to serve ads. Eligibility does not guarantee Roblox will serve ads in your experience, and nothing obligates Roblox to serve ads into your experience. -Experiences must meet the following criteria to be eligible to be an ad publisher: +For your experience to be eligible to serve ads, your account and experience must meet the following criteria: -- Your account must have **2-factor authentication (2FA)** enabled. - - For group-owned experiences, this requirement applies to the owner of the group. - - If a publisher disables 2FA, they will lose eligibility. +- Your experience must be **public**. - You must be **13+ years of age**. - For group-owned experiences, this requirement applies to the owner of the group. -- Your account must be [ID-verified](../publishing/account-verification.md). +- Your account must be [ID-verified](../publishing/account-verification.md) and have **2-factor authentication (2FA)** enabled. - For group-owned experiences, this requirement applies to the owner of the group. - - This requirement is persistent. If a publisher disables identity verification, they lose eligibility. -- Your experience must be **Public**. If a publisher makes the experience Private, the experience loses eligibility. -- You must complete the **Maturity & Compliance Questionnaire** for your experience. + - This requirement is persistent. If you disable identity verification or 2FA, your experience loses eligibility. +- You must complete the **Maturity and Compliance Questionnaire** for your experience. + - The questionnaire must be reviewed and approved by Roblox before the experience is eligible to serve ads. If the review status is **Pending**, you can request a re-review from the experience's advertising eligibility page under **Monetization** ⟩ **Ads** ⟩ **Eligiblity**. You'll receive a response within 24 hours. - Your experience must maintain **2,000 unique visitors per month**. - - This is calculated and updated monthly, based on visitor data. - - Bots can't be included in visitor counts. -- You must comply with the [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use), the [Community Standards](https://en.help.roblox.com/hc/en-us/articles/203313410-Roblox-Community-Standards), and the [Advertising Standards](https://en.help.roblox.com/hc/en-us/articles/13722260778260-Advertising-Standards#publisher-integrity). + - This is calculated and updated monthly, based on visitor data. Bots are not included in visitor counts. +- You must comply with the [Roblox terms of use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use), [community standards](https://en.help.roblox.com/hc/en-us/articles/203313410-Roblox-Community-Standards), and [publisher integrity requirements](https://en.help.roblox.com/hc/en-us/articles/13722260778260-Advertising-Standards#publisher-integrity). + + + Resubmitting your **Maturity and Compliance Questionnaire** will temporarily remove your ads eligibility while the questionnaire is under review. To avoid repeated interruptions, only resubmit the questionnaire when you publish an experience update that changes your previous responses. + - Publishers who don't comply with these requirements might lose their eligibility temporarily or permanently. In some cases, they might lose some or all ad payouts and might have their content or account suspended. + If you don't comply with these requirements, you might lose your publisher eligibility temporarily or permanently. In some cases, you might lose some or all ad payouts and might have your content or account suspended. ## User eligibility diff --git a/.cursor/rules/roblox/en-us/production/promotion/regional-content-availability.md b/.cursor/rules/roblox/en-us/production/promotion/regional-content-availability.md new file mode 100644 index 00000000..be3a79a4 --- /dev/null +++ b/.cursor/rules/roblox/en-us/production/promotion/regional-content-availability.md @@ -0,0 +1,23 @@ +--- +title: Regional Content Availability +description: Explains why experiences or assets may not be available in every country. +--- + +## Our approach to global compliance + +As a global platform, we are committed to respecting the local requirements of the countries in which we operate. This commitment is a core part of our long-term strategy to maintain a presence in nearly every country, allowing creators to reach the widest possible audience. + +## Why is my content unavailable in certain regions? + +Content availability can change based on regional requirements. At times, we may receive requests to restrict certain types of content in a specific country, as well as requests to restrict a particular experience or avatar item in a specific country. + +## What does this mean for my content? + +When content is restricted in a specific country, the following changes occur: + +- **Experiences**: Impacted experiences will be neither discoverable nor playable by users in that country. +- **Avatar items**: Impacted avatar items will be undiscoverable and taken off-sale for users in that country. + +Your content’s status in other countries remains unchanged, and your global engagement and analytics in other regions will continue as usual. + +Our approach to content availability is not static; it is subject to the ongoing evolution of requirements worldwide. Content that was once restricted may become available, and new requirements may be introduced. diff --git a/.cursor/rules/roblox/en-us/production/promotion/rewarded-video-ads.md b/.cursor/rules/roblox/en-us/production/promotion/rewarded-video-ads.md index fe27a316..02a3eb88 100644 --- a/.cursor/rules/roblox/en-us/production/promotion/rewarded-video-ads.md +++ b/.cursor/rules/roblox/en-us/production/promotion/rewarded-video-ads.md @@ -334,7 +334,7 @@ To prevent abuse of the rewarded ad system and to provide the best user experien **Experiences** - Experiences must:

  • Be public and unrestricted.
  • Offer no free-form user creation or AI interaction.
  • Have a complete [Maturity & Compliance questionnaire](../promotion/content-maturity.md).
  • Have an average of at least **2 thousand unique visitors per month**.
  • Comply with the Roblox [terms of use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use), [community standards](https://en.help.roblox.com/hc/en-us/articles/203313410-Roblox-Community-Standards), and [publisher integrity requirements](https://en.help.roblox.com/hc/en-us/articles/13722260778260-Advertising-Standards#publisher-integrity).
+ Experiences must:

  • Be public and unrestricted.
  • Offer no free-form user creation.
  • Have a complete **Maturity and Compliance Questionnaire** that has been reviewed and approved by Roblox. If the review status is **Pending**, you can request a re-review from the experience's advertising eligibility page under **Monetization** ⟩ **Ads** ⟩ **Eligiblity**. You'll receive a response within 24 hours.
  • Have an average of at least **2,000 unique visitors per month**.
  • Comply with the [Roblox terms of use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use), [community standards](https://en.help.roblox.com/hc/en-us/articles/203313410-Roblox-Community-Standards), and [publisher integrity requirements](https://en.help.roblox.com/hc/en-us/articles/13722260778260-Advertising-Standards#publisher-integrity).
**Publishers** @@ -347,7 +347,13 @@ To prevent abuse of the rewarded ad system and to provide the best user experien -Any violation of the eligilibity requirements can result in account suspension, removal of content, revocation of ad payouts, or loss of eligibility to earn from ads. + + Resubmitting your **Maturity and Compliance Questionnaire** will temporarily remove your ads eligibility while the questionnaire is under review. To avoid repeated interruptions, only resubmit the questionnaire when you publish an experience update that changes your previous responses. + + + + Any violation of the eligilibity requirements can result in account suspension, removal of content, revocation of ad payouts, or loss of eligibility to earn from ads. + ## Best practices diff --git a/.cursor/rules/roblox/en-us/production/publishing/adaptive-design.md b/.cursor/rules/roblox/en-us/production/publishing/adaptive-design.md index 783afa1f..849eb30c 100644 --- a/.cursor/rules/roblox/en-us/production/publishing/adaptive-design.md +++ b/.cursor/rules/roblox/en-us/production/publishing/adaptive-design.md @@ -29,13 +29,19 @@ Ensure your experience feels natural on various input devices, including gamepad +
+
+
+
+
+
@@ -48,10 +54,14 @@ The layout of your experience UI should adapt to different screen sizes and orie +
+
+
+
@@ -64,13 +74,19 @@ This is about the **size** of your UI elements, not just their position. Your UI +
+
- +
+ +
+
+
diff --git a/.cursor/rules/roblox/en-us/reference/cloud/openapi.json b/.cursor/rules/roblox/en-us/reference/cloud/openapi.json index 855f6b7f..bdfc4dac 100644 --- a/.cursor/rules/roblox/en-us/reference/cloud/openapi.json +++ b/.cursor/rules/roblox/en-us/reference/cloud/openapi.json @@ -53769,6 +53769,7 @@ "url": "https://thumbnails.roblox.com" } ], + "x-roblox-stability": "BETA", "x-roblox-engine-usability": { "apiKeyWithHttpService": false }, @@ -54803,6 +54804,7 @@ "url": "https://thumbnails.roblox.com" } ], + "x-roblox-stability": "BETA", "x-roblox-engine-usability": { "apiKeyWithHttpService": false }, @@ -55057,6 +55059,7 @@ "url": "https://thumbnails.roblox.com" } ], + "x-roblox-stability": "BETA", "x-roblox-engine-usability": { "apiKeyWithHttpService": false }, @@ -85237,10 +85240,6 @@ "isDelayedUiEnabled": { "description": "Whether the delayed UI is enabled.", "type": "boolean" - }, - "is2svRecoveryEnabled": { - "description": "Whether to expose 2SV recovery through the 2SV challenge modal.", - "type": "boolean" } }, "additionalProperties": false @@ -85958,6 +85957,12 @@ "format": "date-time", "nullable": true }, + "creatingUniverseId": { + "type": "integer", + "description": "Universe ID if the asset was created within a specific universe.", + "format": "int64", + "nullable": true + }, "categoryPath": { "type": "string", "description": "Category Path of the asset, if it exists.\r\ne.g. \"3d__characters\"", @@ -86112,6 +86117,17 @@ "type": "boolean", "description": "Whether the creator is verified.", "nullable": true + }, + "latestGroupUpdaterUserId": { + "type": "integer", + "description": "This value only exists for group-based creators and represents the User ID of the creator who most recently updated the asset.", + "format": "int64", + "nullable": true + }, + "latestGroupUpdaterUserName": { + "type": "string", + "description": "This value only exists for group-based creators and represents the name of the creator who most recently updated the asset.", + "nullable": true } }, "additionalProperties": false, diff --git a/.cursor/rules/roblox/en-us/reference/engine/STUDIO_VERSION b/.cursor/rules/roblox/en-us/reference/engine/STUDIO_VERSION index 5dcd9fa1..6002c259 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/STUDIO_VERSION +++ b/.cursor/rules/roblox/en-us/reference/engine/STUDIO_VERSION @@ -1,2 +1,2 @@ -0.708.0.7080878 -version-6df69e4610344376 \ No newline at end of file +0.709.0.7090870 +version-bb326014c89c4d27 \ No newline at end of file diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/AnalyticsService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/AnalyticsService.yaml index 097a5702..dfbe5542 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/AnalyticsService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/AnalyticsService.yaml @@ -121,8 +121,7 @@ methods: summary: '' tags: - Deprecated - deprecation_message: | - This deprecated property is a variant of `Class.AnalyticsService:LogCustomEvent()` which should be used instead. + deprecation_message: '' security: None thread_safety: Unsafe capabilities: @@ -180,9 +179,10 @@ methods: This function has been deprecated in favor of more descriptive methods, including: - - `Class.AnalyticsService:LogCustomEvent()` - - `Class.AnalyticsService:LogEconomyEvent()` - - `Class.AnalyticsService:LogProgressionEvent()` + - `Class.AnalyticsService:FireCustomEvent()` + - `Class.AnalyticsService:FireInGameEconomyEvent()` + - `Class.AnalyticsService:FireLogEvent()` + - `Class.AnalyticsService:FirePlayerProgressionEvent()` security: None thread_safety: Unsafe capabilities: @@ -302,8 +302,7 @@ methods: No return. tags: - Deprecated - deprecation_message: | - This deprecated property is a variant of `Class.AnalyticsService:LogEconomyEvent()` which should be used instead. + deprecation_message: '' security: None thread_safety: Unsafe capabilities: @@ -541,8 +540,7 @@ methods: No return. tags: - Deprecated - deprecation_message: | - This deprecated property is a variant of `Class.AnalyticsService:LogProgressionEvent()` which should be used instead. + deprecation_message: '' security: None thread_safety: Unsafe capabilities: diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/AudioPlayer.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/AudioPlayer.yaml index 89a300ed..3e671894 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/AudioPlayer.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/AudioPlayer.yaml @@ -71,7 +71,8 @@ properties: description: '' code_samples: [] type: Content - tags: [] + tags: + - Hidden deprecation_message: '' security: read: None diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/AudioSpeechToText.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/AudioSpeechToText.yaml index 1334ab3f..05c354da 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/AudioSpeechToText.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/AudioSpeechToText.yaml @@ -14,7 +14,8 @@ description: | Roblox uses the following formula to throttle requests for this API based on the number of players in your experience: - `max requests per minute per experience = 1 + (5 * number_of_concurrent_users)`. You can purchase additional usage using + `max requests per minute per experience = 1 + (5 * number_of_concurrent_users)`. + You can purchase additional usage using [Extended Services](../../../cloud-services/extended-services.md). For more information, see diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarCreationService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarCreationService.yaml index 7205e2d1..b6e89249 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarCreationService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarCreationService.yaml @@ -953,7 +953,8 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - DynamicGeneration - name: AvatarCreationService.AvatarModerationCompleted summary: | Fires when an in-experience-created avatar's moderation status has been diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarEditorService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarEditorService.yaml index 85b72b25..5ccae4f2 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarEditorService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/AvatarEditorService.yaml @@ -1348,7 +1348,9 @@ methods: deprecation_message: '' security: None thread_safety: Unsafe - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing - name: AvatarEditorService:PromptSetFavorite summary: | Prompts the `Class.Players.LocalPlayer` to favorite or unfavorite the @@ -1512,7 +1514,9 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing - name: AvatarEditorService.PromptDeleteOutfitCompleted summary: | Fires when the PromptDeleteOutfit operation is completed. @@ -1530,7 +1534,9 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing - name: AvatarEditorService.PromptRenameOutfitCompleted summary: | Fires when the PromptRenameOutfit operation is completed. @@ -1548,7 +1554,9 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing - name: AvatarEditorService.PromptSaveAvatarCompleted summary: | Fires when the `Class.AvatarEditorService:PromptSaveAvatar()` operation is @@ -1572,7 +1580,9 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing - name: AvatarEditorService.PromptSetFavoriteCompleted summary: | Fires when the `Class.AvatarEditorService:PromptSetFavorite()` operation @@ -1592,7 +1602,9 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing - name: AvatarEditorService.PromptUpdateOutfitCompleted summary: | Fires when the `Class.AvatarEditorService:PromptUpdateOutfit()` operation @@ -1612,5 +1624,7 @@ events: tags: [] deprecation_message: '' security: None - capabilities: [] + capabilities: + - Players + - PlatformAvatarEditing callbacks: [] diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/ChangeHistoryService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/ChangeHistoryService.yaml index e2d90363..4d50d108 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/ChangeHistoryService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/ChangeHistoryService.yaml @@ -46,7 +46,7 @@ methods: Identifies the recording from the previous call to `Class.ChangeHistoryService:TryBeginRecording()|TryBeginRecording()`. If the operation is - `Enum.FinishRecordingOperation.Cancel`, this + `Enum.ChangeHistoryService.FinishRecordingOperation.Cancel`, this value is ignored, and the recording is determined by context. - name: operation type: FinishRecordingOperation diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/ContentProvider.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/ContentProvider.yaml index 8dc4dc1c..f5ef466c 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/ContentProvider.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/ContentProvider.yaml @@ -230,6 +230,13 @@ methods: individual textures. Calling it on a `Class.SurfaceAppearance` instance will not do anything, but the associated textures will still be streamed in during runtime. + + If PreloadAsync is called on `Class.Instance|Instances` that are not + currently visible, such as a `Class.Decal` or an `Class.ImageLabel`, the + Engine will download and store textures used by those Instances in its + disk cache. Because the `Class.Instance|Instances` are not visible in + these cases, the Engine may reduce memory consumption by unloading the + textures after preloading them. code_samples: - ContentProvider-PreloadAsync1 parameters: diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/GeometryService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/GeometryService.yaml index e9ca83b1..42753da3 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/GeometryService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/GeometryService.yaml @@ -168,6 +168,34 @@ methods: thread_safety: Unsafe capabilities: - CSG + - name: GeometryService:FragmentAsync + summary: | + This API has not been released. + description: '' + code_samples: [] + parameters: + - name: part + type: BasePart + default: + summary: '' + - name: sites + type: Array + default: + summary: '' + - name: options + type: Dictionary + default: nil + summary: '' + returns: + - type: Array + summary: '' + tags: + - Yields + deprecation_message: '' + security: None + thread_safety: Unsafe + capabilities: + - CSG - name: GeometryService:IntersectAsync summary: | Creates one or more `Class.PartOperation|PartOperations` from the @@ -432,6 +460,34 @@ methods: thread_safety: Unsafe capabilities: - CSG + - name: GeometryService:SweepPartAsync + summary: | + This API has not been released. + description: '' + code_samples: [] + parameters: + - name: part + type: BasePart + default: + summary: '' + - name: cframes + type: Array + default: + summary: '' + - name: options + type: Dictionary + default: nil + summary: '' + returns: + - type: MeshPart + summary: '' + tags: + - Yields + deprecation_message: '' + security: None + thread_safety: Unsafe + capabilities: + - CSG - name: GeometryService:UnionAsync summary: | Creates one or more `Class.PartOperation|PartOperations` from one part diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/HttpService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/HttpService.yaml index 58241c29..498c63d2 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/HttpService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/HttpService.yaml @@ -433,7 +433,7 @@ methods: Timeout - Integer + Integer no An optional timeout value in seconds to make requests time out more quickly. Values must be greater than zero and no greater than the default request timeout. This can be useful when debugging hanging requests. diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/Sound.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/Sound.yaml index f5607c30..06e46954 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/Sound.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/Sound.yaml @@ -33,11 +33,14 @@ tags: [] deprecation_message: '' properties: - name: Sound.AudioContent - summary: '' - description: '' + summary: | + A reference to an audio asset. + description: | + This property is a reference to an audio asset. code_samples: [] type: Content - tags: [] + tags: + - Hidden deprecation_message: '' security: read: None diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/TeleportService.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/TeleportService.yaml index 681ea169..ae4e65d1 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/TeleportService.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/TeleportService.yaml @@ -6,10 +6,8 @@ name: TeleportService type: class memory_category: Instances summary: | - Enables transporting `Class.Player|Players` between places and servers. - - For more information on how to teleport players between servers, see - + Enables transporting `Class.Player|Players` between places and servers. For + more information on how to teleport players between servers, see [Teleport between places](../../../projects/teleport.md). description: | **TeleportService** is responsible for transporting `Class.Player|Players` diff --git a/.cursor/rules/roblox/en-us/reference/engine/classes/VideoSampler.yaml b/.cursor/rules/roblox/en-us/reference/engine/classes/VideoSampler.yaml index d7607f66..3cbf5f38 100644 --- a/.cursor/rules/roblox/en-us/reference/engine/classes/VideoSampler.yaml +++ b/.cursor/rules/roblox/en-us/reference/engine/classes/VideoSampler.yaml @@ -17,6 +17,7 @@ inherits: descendants: [] tags: - NotCreatable + - NotReplicated deprecation_message: '' properties: - name: VideoSampler.TimeLength diff --git a/.cursor/rules/roblox/en-us/scripting/security/access-control.md b/.cursor/rules/roblox/en-us/scripting/security/access-control.md index 2994dea6..d9c7a5d6 100644 --- a/.cursor/rules/roblox/en-us/scripting/security/access-control.md +++ b/.cursor/rules/roblox/en-us/scripting/security/access-control.md @@ -13,20 +13,30 @@ Understanding what information and assets are visible to clients is critical for Once inside a universe, clients can teleport to any place within that universe, potentially bypassing any intended access restrictions or progression gates. This can also lead to the unintended leak of unreleased content, for example from a development or staging experience. It is important to understand that: -- Disabling "Direct Access" only removes the Join button from subplaces on the website - it does not prevent client-initiated teleports +- Disabling "Direct Access" only removes the Join button from subplaces on the website — it does not prevent client-initiated teleports - Assume exploiters will discover the existence of all subplaces within your universe - A client can teleport to any subplace if they can access a single place, such as the root place, regardless of your intended design flow -Many developers attempt to prevent access by kicking unauthorized users from a subplace. This may work for blocking gameplay, but it does **not** prevent content from replicating to the client. For places intended to be private: +Many developers attempt to prevent access by kicking unauthorized users from a subplace. This may work for blocking gameplay, but it does **not** prevent content from replicating to the client, since replication begins as soon as the player joins and before server-side kick logic can execute. -- **Keep development and test places in separate, private universe**s - this is the only reliable way to ensure confidentiality - Never ship confidential event assets, scripts, or UI elements to the production environment before they're intended to be active +### Use secure teleports -For universes requiring a public place but restricted access, combine multiple layers: +The most effective way to prevent unauthorized access via teleportation is to set **Access Control for Places** to **Secure within universe only** in the Creator Dashboard. This restricts all non-start places to server-initiated teleports only, blocking client-initiated teleports at the platform level before the player ever joins the subplace. Because the player never joins, no content replicates to them. -- If feasible, use streaming to limit how much of the world will replicate to a new player. + +Enabling **Secure within universe only** is the recommended setting for any experience with restricted subplaces, progression-gated areas, or test places that players should not have access to. + + +For configuration steps and migration guidance for existing experiences that use client-initiated teleports, see [Teleport between places](../../projects/teleport.md#configure-secure-teleportation). + +### Defense-in-depth for restricted places + +For universes that cannot use secure teleports, or as additional layers of protection alongside them, combine the following measures: + +- **Keep development and test places in separate, private universes** — this is the only reliable way to ensure confidentiality for unreleased content. Never ship confidential event assets, scripts, or UI elements to the production environment before they are intended to be active. +- If feasible, use streaming to limit how much of the world replicates to a new player. - Add server-side group role or badge verification and verify player state or progression requirements. -- By default, disallow incoming players. This ensures no player will be allowed even if the verification process is inconclusive, such as in the case of an exception thrown from the engine. +- By default, disallow incoming players. This ensures no player will be allowed in even if the verification process is inconclusive, such as in the case of an exception thrown from the engine. - If practical, use the `Class.Players.BanAsync|Ban API` for players that fail validation. This prevents players from continuously attempting to join on the same account. ## Replication @@ -114,4 +124,4 @@ Confidentiality breaches can have significant consequences, such as: - **Content leaks**: Unreleased items, maps, or features may be discovered and shared publicly before official announcements - **Competitive disadvantage**: Mechanics, algorithms, or upcoming features may be reverse-engineered by competitors -- **Exploit Development**: Exposed server logic makes it faster and easier for exploiters to identify vulnerabilities and develop targeted attacks +- **Exploit development**: Exposed server logic makes it faster and easier for exploiters to identify vulnerabilities and develop targeted attacks diff --git a/.cursor/rules/roblox/en-us/scripting/sync.md b/.cursor/rules/roblox/en-us/scripting/sync.md index 79348887..422fd868 100644 --- a/.cursor/rules/roblox/en-us/scripting/sync.md +++ b/.cursor/rules/roblox/en-us/scripting/sync.md @@ -1,6 +1,6 @@ --- title: Script Sync -description: Use Studio's Script Sync feature to edit your Luau scripts in an external text editor. +description: Use Studio's Script Sync feature to edit your Luau scripts on your local disk using an external text editor or your favorite integrated development environment (IDE). --- import BetaAlert from '../includes/beta-features/beta-alert.md' @@ -9,7 +9,7 @@ import BetaAlert from '../includes/beta-features/beta-alert.md' Roblox Studio has a built-in [script editor](../studio/script-editor.md) for editing the code in your Roblox project. The editor includes key features like linting, type checking, autocomplete and collaborative editing. -Many developers, however, have a preferred text editor, such as [Visual Studio Code](https://code.visualstudio.com), [Cursor](https://cursor.com), [Sublime Text](https://www.sublimetext.com), or [Notepad++](https://notepad-plus-plus.org). Popular development environments often have features or plugins that don't exist in Studio. To use an external text editor with Studio, enable the **Script Sync** feature. +Many developers, however, have a preferred text editor or integrated development environment (IDE), such as [Visual Studio Code](https://code.visualstudio.com), [Cursor](https://cursor.com), [Sublime Text](https://www.sublimetext.com), or [Notepad++](https://notepad-plus-plus.org). Popular development environments often have features or plugins that don't exist in Studio. To use an external editor or IDE with Studio, enable the **Script Sync** feature. Script Sync works by synchronizing the scripts in your [data model](../projects/data-model.md) to text files on your local disk. Changes made to the Luau files on disk are applied to the scripts in Studio, and vice versa. This synchronization means that you can edit a script in an external editor like VS Code and immediately see the change in Studio. diff --git a/.cursor/rules/roblox/en-us/studio/ui-overview.md b/.cursor/rules/roblox/en-us/studio/ui-overview.md index 2c302286..9f1490a8 100644 --- a/.cursor/rules/roblox/en-us/studio/ui-overview.md +++ b/.cursor/rules/roblox/en-us/studio/ui-overview.md @@ -83,10 +83,6 @@ You can customize the appearance of the toolbar by right-clicking in an empty ar Beyond the default tool tabs, you can add **custom tabs** for your specific needs, either through Studio's interface or by editing local `.json` files. - -The tab configuration system is intended for customizing the Studio toolbar using existing controls/widgets and plugins. It's not meant to replace Studio [plugins](./plugins.md) or interactive [widgets](./build-studio-widgets.md). - - The easiest way to create custom tabs is through Studio's workflow. @@ -116,12 +112,14 @@ The easiest way to create custom tabs is through Studio's workflow. Custom tab configurations are saved **locally** on each machine in `.json` format, allowing you to build customized tabs and share them with your team or the community. -1. Open the custom tabs folder which is stored in a location unaffected by Studio updates. +1. To modify `.json` from a default tab toolset, begin by right‑clicking an existing tab and selecting **Duplicate tab**. + +2. Open the custom tabs folder which is stored in a location unaffected by Studio updates. - **Windows** — `%LOCALAPPDATA%\Roblox\\CustomRibbonTabs` - **Mac** — `~/Documents/Roblox//CustomRibbonTabs` -2. In the `CustomRibbonTabs` directory are `.json` files which define the toolbar's structure: +3. In the `CustomRibbonTabs` directory are `.json` files which define the toolbar's structure: - The `RibbonLayout.json` file controls the overall ordering and visibility of tabs. @@ -149,7 +147,7 @@ Custom tab configurations are saved **locally** on each machine in `.json` forma } ``` -3. In the `Controls` array for a tab file, insert various buttons, submenus, and more. Each control's `Id` value must be **unique** within the file. +4. In the `Controls` array for a tab file, insert various buttons, submenus, and more. Each control's `Id` value must be **unique** within the file. The following examples illustrate common elements in a custom tab: @@ -297,7 +295,7 @@ Custom tab configurations are saved **locally** on each machine in `.json` forma Beyond the above examples, you can right-click any default tab and select **Duplicate tab**. This will create a new custom tab and corresponding `.json` file in your local `CustomRibbonTabs` directory which you can inspect to explore how various default tools and menus are constructed.
-4. Save the `.json` file and then reload Studio's plugins: +5. Save the `.json` file and then reload Studio's plugins: 1. Right-click anywhere in the toolbar or mezzanine and select **Manage tabs**. 2. Access the options menu from the popup's upper‑right corner and select **Reload custom tabs**. diff --git a/plugin/src/ChangeBatcher/encodeInstance.lua b/plugin/src/ChangeBatcher/encodeInstance.lua index 51c7ae49..a41f41c0 100644 --- a/plugin/src/ChangeBatcher/encodeInstance.lua +++ b/plugin/src/ChangeBatcher/encodeInstance.lua @@ -221,7 +221,9 @@ encodeInstance = function(instance, parentId, _skipPathCheck) -- Only encode properties that: -- 1. Are readable (ReadWrite or Read scriptability) -- 2. Actually serialize (not "DoesNotSerialize") - local isReadable = propertyMeta.scriptability == "ReadWrite" or propertyMeta.scriptability == "Read" + local isReadable = propertyMeta.scriptability == "ReadWrite" + or propertyMeta.scriptability == "Read" + or propertyMeta.scriptability == "Custom" local doesSerialize = propertyMeta.serialization ~= "DoesNotSerialize" if isReadable and doesSerialize then diff --git a/plugin/src/ChangeBatcher/encodeService.lua b/plugin/src/ChangeBatcher/encodeService.lua index 1f7c3f99..b7ab63f9 100644 --- a/plugin/src/ChangeBatcher/encodeService.lua +++ b/plugin/src/ChangeBatcher/encodeService.lua @@ -34,7 +34,9 @@ return function(service: Instance) continue end - local isReadable = propertyMeta.scriptability == "ReadWrite" or propertyMeta.scriptability == "Read" + local isReadable = propertyMeta.scriptability == "ReadWrite" + or propertyMeta.scriptability == "Read" + or propertyMeta.scriptability == "Custom" local doesSerialize = propertyMeta.serialization ~= "DoesNotSerialize" if isReadable and doesSerialize then diff --git a/plugin/src/PatchTree.lua b/plugin/src/PatchTree.lua index 681e5e32..9a6c5700 100644 --- a/plugin/src/PatchTree.lua +++ b/plugin/src/PatchTree.lua @@ -4,6 +4,7 @@ ]] local HttpService = game:GetService("HttpService") +local ScriptEditorService = game:GetService("ScriptEditorService") local Rojo = script:FindFirstAncestor("Rojo") local Plugin = Rojo.Plugin @@ -367,6 +368,20 @@ function PatchTree.build(patch, instanceMap, changeListHeaders, gitMetadata) break end end + + if defaultSelection == nil then + local ok, draft = pcall(ScriptEditorService.GetEditorSource, ScriptEditorService, instance) + if ok and draft ~= source then + local draftBlob = "blob " .. tostring(#draft) .. "\0" .. draft + local draftHash = SHA1(buffer.fromstring(draftBlob)) + for _, hash in hashes do + if draftHash == hash then + defaultSelection = "push" + break + end + end + end + end end end end diff --git a/plugin/src/testUtils/MockApiContext.lua b/plugin/src/testUtils/MockApiContext.lua index a0d70f44..e54c09a6 100644 --- a/plugin/src/testUtils/MockApiContext.lua +++ b/plugin/src/testUtils/MockApiContext.lua @@ -10,6 +10,41 @@ local HttpService = game:GetService("HttpService") +local MockPromise = {} +MockPromise.__index = MockPromise + +function MockPromise.resolve(value) + return setmetatable({ _value = value, _rejected = false }, MockPromise) +end + +function MockPromise.reject(err) + return setmetatable({ _value = err, _rejected = true }, MockPromise) +end + +function MockPromise:andThen(callback) + if not self._rejected then + local ok, result = pcall(callback, self._value) + if ok then + return MockPromise.resolve(result) + else + return MockPromise.reject(result) + end + end + return self +end + +function MockPromise:catch(callback) + if self._rejected then + local ok, result = pcall(callback, self._value) + if ok then + return MockPromise.resolve(result) + else + return MockPromise.reject(result) + end + end + return self +end + local MockApiContext = {} MockApiContext.__index = MockApiContext @@ -155,22 +190,15 @@ function MockApiContext:write(patch) self:_maybeDelay() if self.failOnWrite then - return false, "Mock write failure" + return MockPromise.reject("Mock write failure") end if self.onWrite then - local result = self.onWrite(self, patch) - if result ~= nil then - return result - end + self.onWrite(self, patch) end - if self.writeResponse then - return true, self.writeResponse - end - - -- Default success response - return true, { success = true } + local response = self.writeResponse or { success = true } + return MockPromise.resolve(response) end --[[ diff --git a/src/syncback/property_filter.rs b/src/syncback/property_filter.rs index b8de1d49..1f622f47 100644 --- a/src/syncback/property_filter.rs +++ b/src/syncback/property_filter.rs @@ -116,12 +116,6 @@ pub fn should_property_serialize_with_stats( prop_name: &str, stats: Option<&SyncbackStats>, ) -> bool { - // Live syncback service chunks cannot provide this hidden property, so - // dedicated syncback must also skip it to preserve parity. - if class_name == "Workspace" && prop_name == "WorldPivotData" { - return false; - } - let database = rbx_reflection_database::get().unwrap(); let mut current_class_name = class_name; @@ -165,18 +159,3 @@ pub fn should_property_serialize_with_stats( true } - -#[cfg(test)] -mod tests { - use super::should_property_serialize; - - #[test] - fn workspace_world_pivot_data_is_not_serialized() { - assert!(!should_property_serialize("Workspace", "WorldPivotData")); - } - - #[test] - fn model_world_pivot_data_still_serializes() { - assert!(should_property_serialize("Model", "WorldPivotData")); - } -}