Skip to content

Commit 7ebf00d

Browse files
committed
WIP
1 parent 4a98537 commit 7ebf00d

23 files changed

Lines changed: 1951 additions & 69 deletions

examples/new_metatable_type_system.lua

Lines changed: 944 additions & 0 deletions
Large diffs are not rendered by default.

nattlua/analyzer/operators/newindex.lua

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ return {
145145
val:GetInputSignature():Set(1, Union({Any(), obj}))
146146
self:AddToUnreachableCodeAnalysis(val)
147147
end
148+
elseif
149+
node and
150+
not node.self_call and
151+
obj.Type == "table" and
152+
obj:GetNewMetaTable() and
153+
not self:IsTypesystem()
154+
then
155+
-- For @NewMetaTable tables, non-self-call functions assigned to the table
156+
-- (e.g. META["SetX"] = function(self, val) ... end) should have their first
157+
-- untyped parameter treated as the self type, so that return self works properly.
158+
local arg = val:GetInputSignature():GetWithNumber(1)
159+
160+
if arg and arg.Type == "any" then
161+
val:SetCalled(true)
162+
val = val:Copy()
163+
val:SetCalled(false)
164+
val:GetInputSignature():Set(1, Union({Any(), obj}))
165+
self:AddToUnreachableCodeAnalysis(val)
166+
end
148167
end
149168
end
150169

nattlua/definitions/lua/globals.nlua

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,34 @@ analyzer function setmetatable(tbl: Table, meta: Table | nil)
503503
end
504504

505505
if meta.Type == "table" then
506-
if meta.Self then
506+
if meta.NewMetaTable and not meta.Self then
507+
-- @NewMetaTable mode: build an inferred Self from tbl's fields
508+
-- This is informational (no contract enforcement)
509+
local inferred_self = types.Table()
510+
for _, kv in ipairs(tbl:GetData()) do
511+
local key = kv.key
512+
local val = kv.val
513+
-- Widen literal values to their base types for the inferred self
514+
if val:IsLiteral() and val.Type ~= "function" and val.Type ~= "table" and val.Widen then
515+
val = val:Widen()
516+
end
517+
inferred_self:Set(key, val)
518+
end
519+
inferred_self:SetMetaTable(meta)
520+
meta.Self = inferred_self
521+
elseif meta.NewMetaTable and meta.Self then
522+
-- Subsequent setmetatable calls: merge additional fields
523+
for _, kv in ipairs(tbl:GetData()) do
524+
local key = kv.key
525+
local val = kv.val
526+
if val:IsLiteral() and val.Type ~= "function" and val.Type ~= "table" and val.Widen then
527+
val = val:Widen()
528+
end
529+
if not meta.Self:HasKey(key) then
530+
meta.Self:Set(key, val)
531+
end
532+
end
533+
elseif meta.Self then
507534
analyzer:ErrorIfFalse(tbl:FollowsContract(meta.Self))
508535
tbl:CopyLiteralness2(meta.Self)
509536
tbl:SetContract(meta.Self)

nattlua/types/table.lua

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ META:GetSet("BaseTable", nil--[[# as TTable | false]])
3838
META:GetSet("ReferenceId", nil--[[# as string | false]])
3939
META:GetSet("Self", nil--[[# as false | TTable]])
4040
META:GetSet("Self2", nil--[[# as false | TTable]])
41+
META:GetSet("NewMetaTable", false--[[# as boolean]])
4142
META:GetSet("Contracts", nil--[[# as List<|TTable|>]])
4243
META:GetSet("CreationScope", nil--[[# as any]])
4344
META:GetSet("AnalyzerEnvironment", false--[[# as false | "runtime" | "typesystem"]])
@@ -85,6 +86,14 @@ function META:SetSelf(tbl--[[#: TTable]])
8586
self.Self = tbl
8687
end
8788

89+
function META:SetNewMetaTable(val)
90+
if val and val.Type == "symbol" and val:IsTrue() then
91+
self.NewMetaTable = true
92+
else
93+
self.NewMetaTable = false
94+
end
95+
end
96+
8897
function META.Equal(
8998
a--[[#: TTable]],
9099
b--[[#: TBaseType]],
@@ -1099,6 +1108,7 @@ function META:Copy(map--[[#: Map<|any, any|> | nil]], copy_tables)
10991108
end
11001109

11011110
copy.Self2 = self.Self2
1111+
copy.NewMetaTable = self.NewMetaTable
11021112
copy.MetaTable = self.MetaTable --copy_val(self.MetaTable, map, copy_tables)
11031113
copy.Contract = self:GetContract() --copy_val(self.Contract, map, copy_tables)
11041114
copy:SetAnalyzerEnvironment(self:GetAnalyzerEnvironment())
@@ -1465,6 +1475,7 @@ function META.New()
14651475
Name = false,
14661476
Self = false,
14671477
Self2 = false,
1478+
NewMetaTable = false,
14681479
literal_data_cache = {},
14691480
Contracts = {},
14701481
TypeOverride = false,

notes/new_metatable.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# NewMetaTable - Redesigning Metatable/Self Typing
2+
3+
## Goal
4+
5+
Redesign how metatables and `self` typing work. The current system has three overlapping mechanisms (`Self` via `@Self`, `Self2` via `@Self2`, and `potential_self`) that are unsatisfying. The new approach should infer the `self` type automatically from `setmetatable()` calls rather than requiring explicit `type META.@Self = {fields...}` declarations.
6+
7+
## Instructions
8+
9+
- Start with an explicit opt-in annotation `type META.@NewMetaTable = true` to activate the new system
10+
- Once working in tests, try making it the default by auto-detecting the `META.__index = META` pattern
11+
- The new system should be **informational only** (no contract enforcement at `setmetatable()` time, unlike `@Self`)
12+
- The goal is to eventually **deprecate/remove `@Self`** -- it should become unnecessary
13+
- Support **both** inheritance patterns: `setmetatable(Child, {__index=Parent})` and manual `function META:__index(key)` dispatch
14+
- Run `luajit nattlua.lua test` often to check for regressions
15+
- Run individual test files with `luajit nattlua.lua test path/to/test/file.lua`
16+
17+
## Architecture of the Current System
18+
19+
### 1. `Self` (via `@Self`)
20+
Set by `SetSelf()` in `table.lua:83-87`. Creates a contract, sets metatable relationship, enforces structural typing. Used in `setmetatable()` to validate tables. Heavy -- calls `tbl:SetMetaTable(self)`, `tbl:SetContract(tbl)`, stores `self.Self = tbl`.
21+
22+
### 2. `Self2` (via `@Self2`)
23+
Lighter alternative, just stores a reference. No contract enforcement. Effectively dormant -- no current tests use it. Exists because `Self` was too heavy. Used with `setmetatable2`.
24+
25+
### 3. `potential_self`
26+
Runtime inference -- accumulates a union of all tables passed to `setmetatable()` with a given meta. Only used during deferred/unreachable code analysis (`CrawlFunctionWithoutOrigin`). Set at `globals.nlua:540-541`.
27+
28+
### Priority chain for resolving `self` type in methods
29+
Located in `expressions/function.lua:44-51`:
30+
```
31+
Self > Self2 > contract > potential_self > Union({Any(), val}) fallback
32+
```
33+
34+
### `@` property mechanism
35+
Keys starting with `@` in type annotations dispatch to `Set<Name>`/`Get<Name>` methods on TTable. E.g., `type META.@Self = ...` calls `META:SetSelf(...)`. Handled in `table.lua` lines 794-813 (`Set`), 849-853 (`SetExplicit`), 865-884 (`Get`).
36+
37+
### Literal widening
38+
NattLua numbers/strings have `IsLiteral()` -- a number `0` is literal, `number` is not. To widen, use `val:Widen()` (not `SetLiteral` which doesn't exist). Tables and functions should not be widened.
39+
40+
## What Has Been Accomplished
41+
42+
### Implementation
43+
1. **Added `NewMetaTable` field to TTable** -- `GetSet("NewMetaTable", false)` at line 41 of `table.lua`, initialized in `New()`, copied in `Copy()`
44+
2. **Implemented `SetNewMetaTable()`** -- simple flag setter in `table.lua` (after line 87)
45+
3. **Modified `setmetatable()` analyzer function** -- in `globals.nlua` lines 505+, added a `NewMetaTable` branch that builds an inferred Self from the table's fields (widened to base types) without contract enforcement
46+
47+
### Tests (20 passing)
48+
All in `test/tests/nattlua/analyzer/new_metatable.lua`:
49+
1. Basic field inference
50+
2. Multiple fields
51+
3. Mutation via methods
52+
4. `__add` metamethod
53+
5. `__call` constructor
54+
6. Constructor function pattern
55+
7. Method calls
56+
8. Method returning self for chaining
57+
9. Multiple instances
58+
10. Basic inheritance with `setmetatable(Player, {__index = Entity})`
59+
11. `__tostring` metamethod
60+
12. `__len` metamethod
61+
13. `__concat` metamethod
62+
14. `__eq` metamethod
63+
15. `__newindex` metamethod
64+
16. Empty table with methods only
65+
17. Multiple `setmetatable()` calls merge fields
66+
18. `@Self` takes precedence (backward compatibility)
67+
19. Three-level inheritance (Base -> Mid -> Leaf)
68+
20. Method override in child class
69+
70+
### Full test suite
71+
All 1410 tests pass across 101 files, no regressions.
72+
73+
## Key Challenge: Methods Defined Before `setmetatable()`
74+
75+
Methods are typically defined BEFORE `setmetatable()` is called, so the inferred Self isn't populated yet when method bodies are first encountered. The solution: don't set `Self` at `@NewMetaTable` declaration time. Instead, build `Self` when `setmetatable()` is first called. Methods defined before that use the deferred analysis path (via `potential_self`/`Union({Any(), val})`), then when called after `setmetatable()`, the actual arguments provide the correct types.
76+
77+
## Auto-Detection: Attempted and Reverted
78+
79+
### What was tried
80+
Adding auto-detection in `NewIndexOperator` (`newindex.lua`): when `key == "__index"` and `val == obj` (self-referential assignment `META.__index = META`), automatically set `obj.NewMetaTable = true`.
81+
82+
### Why it failed
83+
The `NewMetaTable` system and the `potential_self` system conflict:
84+
85+
1. `NewMetaTable` sets `meta.Self` at `setmetatable()` time, which blocks the `potential_self` accumulation path (line 540 in `globals.nlua`)
86+
2. Methods defined after `Self` is set skip the deferred analysis widening (`Union({Any(), obj})`) because `newindex.lua:149` checks `not arg.Self`
87+
3. Existing tests that use `analyzer:AnalyzeUnreachableCode()` depend on literal-precision results from `potential_self` unions (e.g., expecting `2 | 3` from two constructors with `foo = 1` and `foo = 2`)
88+
89+
### Specific regression
90+
Test in `metatable.lua:423-448`: Two constructors create tables with `foo = 1` and `foo = 2`. Via `potential_self`, the deferred analysis sees `self.foo` as `1 | 2`, computing `self.foo + 1` as `2 | 3`. With `NewMetaTable`, the first `setmetatable()` widens `foo` to `number`, so the result becomes `number` instead of the expected `2 | 3`.
91+
92+
A secondary regression in `function.lua:551-572`: A test using both `META.__index = META` and `type META.@Self = {foo = number}` broke because auto-detection set `NewMetaTable` before `@Self` was set. Fixed with `self.NewMetaTable = false` in `SetSelf()`, but this was also reverted.
93+
94+
### Possible future approaches
95+
To make auto-detection work:
96+
- **(a) Make additive**: When auto-detected, still populate `potential_self` alongside the NewMetaTable Self, so deferred analysis continues to work
97+
- **(b) Guard detection**: Only auto-detect when `@Self` isn't set AND no deferred functions are queued
98+
- **(c) Change deferred analysis**: Make it interact properly with `Self` set by `NewMetaTable`
99+
100+
## Relevant Files
101+
102+
### Modified files
103+
- **`nattlua/types/table.lua`** -- Added `NewMetaTable` field (line 41), initialization in `New()`, copy in `Copy()`, `SetNewMetaTable()` method (after line 87)
104+
- **`nattlua/definitions/lua/globals.nlua`** -- Modified `setmetatable()` analyzer function (lines 505+) to handle `NewMetaTable` branch
105+
106+
### Created files
107+
- **`test/tests/nattlua/analyzer/new_metatable.lua`** -- 20 test cases
108+
- **`examples/new_metatable_type_system.lua`** -- Complex example simulating NattLua's type system with the class pattern, including BaseTable (type-level inheritance) with a GMod-like IEntity/IPlayer/INPC/IWeapon hierarchy demonstration (7 BaseTable tests: lookup, isolation, override, chain, subset, copy, describe integration)
109+
110+
### Key files (not modified, for reference)
111+
- **`nattlua/analyzer/expressions/function.lua`** -- Where `self` type is resolved for method definitions (lines 23-56, priority chain at 44-51)
112+
- **`nattlua/analyzer/operators/newindex.lua`** -- Where method functions assigned to tables get self-widened and deferred (lines 112-164). Auto-detection code would go here.
113+
- **`nattlua/analyzer/base/base_analyzer.lua`** -- `add_potential_self()` and `CrawlFunctionWithoutOrigin()` (lines 100-184)
114+
- **`nattlua/analyzer/operators/function_call_body.lua`** -- `potential_self` skip logic (line 334)
115+
- **`test/tests/nattlua/analyzer/metatable.lua`** -- Existing metatable tests (reference for patterns)
116+
- **`nattlua/other/class.lua`** -- Class library using `@Self`
117+
- **`nattlua/definitions/utility.nlua`** -- `copy()` clears `potential_self`
118+
119+
## BaseTable -- Class-Level Inheritance
120+
121+
### What it is
122+
`BaseTable` is a field on TTable (`table.lua:37`) that represents **type-level superclass inheritance**. It's the mechanism for expressing "this type inherits methods from that type" -- specifically for type-declared class hierarchies.
123+
124+
### How it differs from MetaTable and NewMetaTable
125+
126+
| Property | `MetaTable` | `BaseTable` | `NewMetaTable` |
127+
|---|---|---|---|
128+
| **What it models** | Lua's runtime `setmetatable()` | Type-level class hierarchy (supertype) | Inferred instance shape |
129+
| **How it's set** | `setmetatable(tbl, meta)` at runtime | `type X.@BaseTable = Y` annotation only | `type META.@NewMetaTable = true` annotation |
130+
| **Used for lookup** | `__index` metamethod chain | Direct fallback in `HasKey`/`FindKeyValWide` | Builds `Self` at `setmetatable()` time |
131+
| **Scope** | Exists on instances | Exists on type definitions (META tables) | Controls `setmetatable()` behavior |
132+
133+
### How it's used (3 read sites in table.lua)
134+
1. **`HasKey` (lines 722-724)** -- If key not found in self, checks `BaseTable:HasKey(key)`. This makes inherited methods discoverable.
135+
2. **`FindKeyValWide` (lines 787-793)** -- If key-val not found in self, falls through to `BaseTable:FindKeyValWide(key)`. This is how inherited methods are actually retrieved.
136+
3. **`IsSubsetOf` (lines 468-474)** -- During subset checking, if `b.BaseTable == a`, allows inherited keys to satisfy the check. Handles the circular self-reference case.
137+
138+
### The lookup chain for `ply:IsVisible(ply)` with BaseTable
139+
1. `ply` has metatable `IPlayer`
140+
2. `IPlayer.__index` is `IPlayer`
141+
3. `IPlayer:HasKey("IsVisible")` -- not in IPlayer.Data
142+
4. `IPlayer.BaseTable` is `IEntity`, so `IEntity:HasKey("IsVisible")` returns true
143+
5. `FindKeyValWide` retrieves the function type from `IEntity`
144+
145+
### Example from metatable.lua (lines 710-742)
146+
```lua
147+
local type IPlayer = {}
148+
local type IEntity = {}
149+
150+
type IEntity.@Name = "IEntity"
151+
type IEntity.@MetaTable = IEntity
152+
type IEntity.__index = IEntity
153+
type IEntity.IsVisible = function=(IEntity, target: IEntity)>(boolean)
154+
type IEntity.@Contract = IEntity
155+
156+
type IPlayer.@Name = "IPlayer"
157+
type IPlayer.@MetaTable = IPlayer
158+
type IPlayer.__index = IPlayer
159+
type IPlayer.@BaseTable = IEntity -- <-- inheritance link
160+
type IPlayer.GetName = function=(IPlayer)>(string)
161+
type IPlayer.@Contract = IPlayer
162+
```
163+
164+
### Real-world usage
165+
- **GMod** (`glua_base.nlua`): IWeapon, IVehicle, IPlayer, INPC all extend IEntity
166+
- **LOVE2D** (`love_api.nlua`): ~60 usages modeling the hierarchy (Canvas->Texture->Drawable->Object, etc.)
167+
- **Auto-generated** (`build_api.nlua:216`): `@BaseTable` generated from `supertypes` field
168+
169+
### Relationship to NewMetaTable
170+
`BaseTable` and `NewMetaTable` are **orthogonal**:
171+
- `NewMetaTable` = how instances get their type (inferred Self vs explicit `@Self`)
172+
- `BaseTable` = where to find inherited methods (class hierarchy)
173+
174+
A type could have both. In the inheritance test (test 10, 19), `setmetatable(Child, {__index = Parent})` achieves a similar result at runtime -- but `@BaseTable` achieves it at the type level without needing runtime `setmetatable`.
175+
176+
### Key question for NewMetaTable
177+
When `NewMetaTable` is used with inheritance, should it automatically set `BaseTable`? Currently test 10/19 use `setmetatable(Player, {__index = Entity})` which works because the runtime `__index` chain resolves methods. But `BaseTable` could provide the same thing at the type level. The question is whether the `NewMetaTable` system should infer `BaseTable` from `setmetatable(Child, {__index = Parent})` calls.
178+
179+
## Next Steps
180+
181+
- [ ] Auto-detect `META.__index = META` without breaking `potential_self` (see approaches above)
182+
- [ ] Consider whether NewMetaTable should auto-set BaseTable from `setmetatable(Child, {__index = Parent})`
183+
- [ ] Test `rawget`/`rawset` interactions
184+
- [ ] Test more complex inheritance (diamond inheritance, mixins)
185+
- [ ] Eventually deprecate `Self2`, `setmetatable2`, and `potential_self`

test/tests/nattlua/analyzer/complex/self/base_parser.nlua

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/tests/nattlua/analyzer/complex/self/base_type.nlua

Lines changed: 0 additions & 8 deletions
This file was deleted.

test/tests/nattlua/analyzer/complex/self/function.nlua

Lines changed: 0 additions & 4 deletions
This file was deleted.

test/tests/nattlua/analyzer/complex/self/json.nlua

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/tests/nattlua/analyzer/complex/self/lexer.nlua

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)