Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions docs/improving-type-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Type Validation and Structural Comparison Utility

## Summary

Introduce a built-in method for `Type` objects in Type Function library (e.g., "Type:verifyFields" or "Type:matches") that validates whether a given object conforms to a target type definition, returning detailed error metadata.

## Motivation

Currently, implementing structural validation in Luau Type Functions requires manually iterating through properties using `Type:properties()`, checking existence via `readproperty`, and comparing types using `Type:is()`.

As shown in my example, validating a simple Interface requires over 20 lines of repetitive imperative code. This is:
1. Error-prone: Developers must manually handle edge cases like optional fields and indexers.
2. Verbose: Common patterns (like ensuring a table matches an interface) are rewritten constantly.
3. Inconsistent: Different developers will return different error formats, making library interoperability difficult.
4. Needs a Luau table to mimic a type definiton and it's fields to check against.
5. Becomes more complex the bigger your type becomes.
6. Having the type to be an actual type, not a mimic can still be crucial to have exported, it can be bad when you end up having a mimic and a real type.

A built-in utility would allow for "Type-Driven Validation" where the type system itself acts as the schema validator for runtime-adjacent logic.

## Design

I propose adding a method to the `Type` object accessible within type functions.

API Signature

```lua
type TypeValidatorErrorKind = 'type-mismatch' | 'field-missing' | 'unexpected'
type ValidationError = {
kind: TypeValidatorErrorKind,
field: string,
targetType: Type,
}

type ValidationOptions = {
strict: boolean
}

-- Within a type function context:
function Type:matches(target: table, options: ValidationOptions): (boolean, ValidationError?)
```

Behavior

**target**: The object to be validated against `self` type
**options.strict**: If `true`, the presence of fields in `target` that are not defined in `self` (the schema) will trigger an `unexpected` error. If `false` (default), extra fields are ignored.

Error Mapping
1. `field-missing`: A property exists in the type definition but is absent in the target.
2. `type-mismatch`: A property exists in both, but `:is()` returns false.
3. `unexpected`: (Strict mode only) A property exists in the target but not in the definition.

Examples

Current Approach
```lua
type function Person(obj)
if not obj:is("table") then
error("Object must be a table")
end

local properties = obj:properties()

local PersonType = {
Name = "string"
}

for Field in PersonType do
Field = obj:readproperty(types.singleton(Field))

if not Field then
error(`Person is missing {Field}`)
end
end

for k, v in properties do
local value = v.read
local key = k:value()

local targetType = PersonType[key]

if targetType and not value:is(targetType) then
error(`Person {key} must be {targetType}`)
end
end

return obj
end

local function CreatePerson<T>(person: T): Person<T>
return person
end

local Peter = CreatePerson({
Name = true --// Person Name must be string
})
```

Proposed Approach

```lua
local Person = {
name = "john"
}

type PersonType = {
Name: string
}

local Success, Error = PersonType:matches(Person, { strict = false })

if not Success then
if Error.kind == Enum.TypeValidatorErrorKind.Mismatch then
error(`Person field {Error.field} expected to be {Error.targetType}`)
elseif Error.kind == Enum.TypeValidatorErrorKind.FieldMissing then
error(`Required field {Error.field} is missing!`)
end
end
```

## Drawbacks

Must handle recursive types and unions carefullly to avoid infinite loops or performance degradation during analysis.

## Alternatives

Write a runtime object schema validator.

## Unresolved Questions

1. How should this behave with `indexers` (dictionary types), especially with `strict` mode?
94 changes: 94 additions & 0 deletions docs/local-table-destructuring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Local Table Destructuring

## Summary
Add syntax support for flat table destructuring in local and constant declarations to unpack table fields into discrete local variables in a single statement.

```luau
local foo, { yes, no }, bar = 5, getOptions(), 1
const foo, { yes, no }, bar = 5, getOptions(), 1
```

## Motivation
Extracting multiple fields from tables, especially configuration structures, module returns, or UI state tables currently requires verbose, repetitive assignment boilerplate:

```luau
local myObject = getObject()
local yes = myObject.yes
local no = myObject.no
```

This proposal provides a clean, declarative syntax to extract variables, matching modern ergonomics found in languages like TypeScript, without breaking Luau's existing visual style or type systems.

## Design

## Syntax
The grammar for `local` and `const` statements is extended to allow a brace-enclosed list of identifiers inside the binding list.

```
binding ::= Name | table_destructure
table_destructure ::= '{' [Name {',' Name}] '}'
bindinglist ::= binding {',' binding}
```

## Scope Restriction (Type Solver Safety)
To ensure absolute safety and stability for Luau's strict type-inference engine, **destructuring is strictly confined to `local` and `const` declaration statements.**

By avoiding function signatures and loop constructs in this phase, we prevent complex type solver bugs that arise from trying to infer or annotate destructuring patterns inside continuous execution blocks (like `for` loops or unannotated function parameter boundaries)

## Parser Changes (`Parser.cpp`)
The core parsing logic for variable declarations resides in `parseBindingList`. We will modify its behavior using an internal flag:

1. Add a boolean flag `allowDestructuring` (defaults to `false`) to the signature of `parseBindingList`.

2. `parseLocal` will invoke `parseBindingList` with `allowDestructuring = true`.

3. Inside `parseBindingList`, if `allowDestructuring` is true and the current token is '{', the parser branches into a new internal helper function `parseTableDestructure()`.

4. `parseTableDestructure()` captures the list of identifiers enclosed in the braces and returns a compound AST node representing the pattern block.

*Note: For loops (`parseFor`) and function parameters, we will leave `allowDestructuring = false` to enforce the safety scope.*

## Compiler Lowering (`Compiler.cpp`)
Destructuring is implemented purely as syntactic sugar within the compiler. The AST transformation lowers the destructuring pattern into standard local assignment via hidden, compiler-generated temporary variables.

Given the source code:
```luau
local foo, bar, { yes, no } = 0, 1, getOptions()
```

The compiler lowers this into the bytecode equivalent of:
```luau
local foo, bar, _temp1 = 0, 1 getOptions()
local yes, no = _temp1.yes, _temp1.no
```

## Expression alignment and Nil Safety
• Left-hand side patterns map 1:1 with right-hand side expressions based on their list position.

• The expression at the pattern's index is assigned to the temporary variable (`_temp1`).

• If fewer expressions are provided on the right-hand side than bindings on the left-hand side, the temporary variable resolves to `nil`, safely causing the extracted properties also resolve to `nil`.

## Non-Goals

## Destructuring in Function Parameters and For-Loops
As stated in the design section, expanding destructuring to `function foo({ x, y })` or `for { id, name } in items do` is an explicit non-goal for this initial implementation. This isolates the change from Luau's type solver codebase.

## Nested Destructuring
This proposal explicitly excludes nested table destructuring (e.g, `local { a = { b } } = object`). While nesting is structurally possible, deep nesting drastically reduces code readability and introduces complex runtime `nil`-indexing risks.

## Key Aliasing / Renaming
Renaming keys during extraction (e.g `local { yes = customName }`) is excluded from this initial proposal to keep the parser and AST footprint simple.

## Drawbacks
Potential ambiguity with function call sugar:
```lua
const {a, b}
```

Can already be interpreted as:
```lua
const({a, b})
```

Optimal way to distinguish destructuring syntax from function call now is to peek after `}` token and look for `=` token.