|
| 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` |
0 commit comments