lt-script 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -147
- package/dist/compiler/codegen/LuaEmitter.js +1 -0
- package/dist/compiler/parser/AST.d.ts +6 -0
- package/dist/compiler/parser/AST.js +1 -0
- package/dist/compiler/parser/Parser.d.ts +2 -0
- package/dist/compiler/parser/Parser.js +77 -7
- package/dist/compiler/semantics/SemanticAnalyzer.d.ts +5 -0
- package/dist/compiler/semantics/SemanticAnalyzer.js +287 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,269 +6,561 @@
|
|
|
6
6
|
<img src="https://img.shields.io/badge/FiveM-Ready-green" alt="FiveM Ready">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
-
**LT** is a modern,
|
|
9
|
+
**LT** is a modern, statically-typed superset of Lua designed specifically for **FiveM development**. It combines the elegance of TypeScript with the performance of Lua, adding FiveM-specific syntactic sugar to make your scripts robust, readable, and faster to write.
|
|
10
|
+
|
|
11
|
+
> **Goodbye boilerplate, hello productivity.**
|
|
12
|
+
> Write clean, type-safe code that compiles to optimized 100% vanilla Lua.
|
|
13
|
+
|
|
14
|
+
---
|
|
10
15
|
|
|
11
16
|
## ✨ Why LT?
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
* **🛡️ Type Safety**: Catch type mismatches and `nil` errors *before* you run the game.
|
|
19
|
+
* **⚡ Modern Syntax**: Use Arrow Functions, Destructuring, Spread syntax, and more.
|
|
20
|
+
* **🎮 FiveM-First**: Built-in keywords like `netevent`, `addcmd`, `thread`, and `wait`.
|
|
21
|
+
* **🔌 Zero Overhead**: Compiles to pure, readable Lua with no runtime dependencies.
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
- **FiveM Sugar**: Built-in `thread`, `wait`, `netevent`, `addcmd`, `export` keywords
|
|
17
|
-
- **Type Safety**: Static type checking catches errors at compile time
|
|
18
|
-
- **Clean Code**: No more boilerplate — just write what you mean
|
|
23
|
+
---
|
|
19
24
|
|
|
20
25
|
## 📦 Installation
|
|
21
26
|
|
|
27
|
+
Prerequisites: [Node.js](https://nodejs.org/) (Version 16+)
|
|
28
|
+
|
|
22
29
|
```bash
|
|
30
|
+
# Global installation (Recommended)
|
|
23
31
|
npm install -g lt-script
|
|
32
|
+
|
|
33
|
+
# OR run directly via npx
|
|
34
|
+
npx lt-script build .
|
|
24
35
|
```
|
|
25
36
|
|
|
37
|
+
---
|
|
38
|
+
|
|
26
39
|
## 🚀 Quick Start
|
|
27
40
|
|
|
28
|
-
Create a
|
|
41
|
+
Create a `client.lt` file:
|
|
29
42
|
|
|
30
43
|
```typescript
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
// Define a type for player data
|
|
45
|
+
interface PlayerData {
|
|
46
|
+
id: number,
|
|
47
|
+
name: string,
|
|
48
|
+
health: number
|
|
49
|
+
}
|
|
34
50
|
|
|
35
|
-
//
|
|
36
|
-
let
|
|
51
|
+
// Variables with types
|
|
52
|
+
let active: boolean = false
|
|
53
|
+
const MAX_HEALTH = 100
|
|
37
54
|
|
|
38
|
-
//
|
|
39
|
-
|
|
55
|
+
// FiveM Command
|
|
56
|
+
addcmd "heal" (source, args)
|
|
57
|
+
let amount = tonumber(args[1]) ?? MAX_HEALTH
|
|
58
|
+
SetEntityHealth(PlayerPedId(), amount)
|
|
59
|
+
print("Healed for ${amount}")
|
|
60
|
+
end
|
|
40
61
|
|
|
41
|
-
//
|
|
62
|
+
// Thread with loop
|
|
42
63
|
thread
|
|
64
|
+
print("System active")
|
|
43
65
|
loop (true)
|
|
44
66
|
wait 1000
|
|
45
|
-
|
|
67
|
+
if LocalPlayer.state?.isDead then
|
|
68
|
+
print("Player is dead!")
|
|
69
|
+
end
|
|
46
70
|
end
|
|
47
71
|
end
|
|
48
|
-
|
|
49
|
-
// Register a command easily
|
|
50
|
-
addcmd "heal" (source, args)
|
|
51
|
-
health = MAX_HEALTH
|
|
52
|
-
print("Player healed!")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
// Network events without boilerplate
|
|
56
|
-
netevent "player:spawn" (data)
|
|
57
|
-
print("Player spawned: $data")
|
|
58
|
-
end
|
|
59
72
|
```
|
|
60
73
|
|
|
61
|
-
Compile it
|
|
62
|
-
|
|
74
|
+
**Compile it:**
|
|
63
75
|
```bash
|
|
64
76
|
ltc build .
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
|
|
68
|
-
```lua
|
|
69
|
-
local health = 100
|
|
70
|
-
local MAX_HEALTH <const> = 200
|
|
79
|
+
---
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
## 📘 Language Reference
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
### 1. Variables & Types
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
while true do
|
|
78
|
-
Wait(1000)
|
|
79
|
-
print("Tick...")
|
|
80
|
-
end
|
|
81
|
-
end)
|
|
85
|
+
LT supports explicit typing with compile-time validation.
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
```typescript
|
|
88
|
+
// Mutable variables
|
|
89
|
+
let score: number = 0
|
|
90
|
+
var playerName: string = "John" // Type inferred if omitted
|
|
91
|
+
|
|
92
|
+
// Constants (Compiles to Lua 5.4 <const>)
|
|
93
|
+
const MAX_PLAYERS = 32
|
|
94
|
+
|
|
95
|
+
// Available primitive types
|
|
96
|
+
let str: string = "hello"
|
|
97
|
+
let num: number = 42
|
|
98
|
+
let flag: boolean = true
|
|
99
|
+
let data: table = { key: "value" }
|
|
100
|
+
let pos: vector3 = <100.0, 200.0, 30.0>
|
|
101
|
+
let callback: function = () => print("hi")
|
|
102
|
+
|
|
103
|
+
// Arrays
|
|
104
|
+
let inventory: string[] = ["Apple", "Water"]
|
|
105
|
+
let numbers: number[] = [1, 2, 3]
|
|
92
106
|
```
|
|
93
107
|
|
|
94
|
-
|
|
108
|
+
### 2. Interfaces & Type Aliases
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|---------|-------------|
|
|
98
|
-
| `ltc build <dir>` | Compile all `.lt` files in directory |
|
|
99
|
-
| `ltc watch <dir>` | Watch mode — auto-compile on save |
|
|
110
|
+
Define complex data structures for compile-time validation.
|
|
100
111
|
|
|
101
|
-
## 🎯 Feature Highlights
|
|
102
|
-
|
|
103
|
-
### Variables & Constants
|
|
104
112
|
```typescript
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
// Interface - Object shape definition
|
|
114
|
+
interface VehicleConfig {
|
|
115
|
+
model: string,
|
|
116
|
+
price: number,
|
|
117
|
+
color: string
|
|
118
|
+
}
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Explicit types
|
|
113
|
-
let name: string = "John"
|
|
114
|
-
let age: number = 25
|
|
115
|
-
let active: boolean = true
|
|
120
|
+
// Type Alias - Alternative syntax
|
|
121
|
+
type PlayerPos = { x: number, y: number, z: number }
|
|
116
122
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
// Usage with validation
|
|
124
|
+
const myCar: VehicleConfig = {
|
|
125
|
+
model: "sultan",
|
|
126
|
+
price: 50000,
|
|
127
|
+
color: "red"
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
// Arrays of interfaces
|
|
131
|
+
interface StoreConfig {
|
|
132
|
+
id: number,
|
|
133
|
+
position: vector3,
|
|
134
|
+
isOpen: boolean
|
|
135
|
+
}
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
const Stores: StoreConfig[] = [
|
|
138
|
+
{ id: 1, position: <100.0, 200.0, 30.0>, isOpen: true },
|
|
139
|
+
{ id: 2, position: <150.0, 250.0, 35.0>, isOpen: false }
|
|
140
|
+
]
|
|
133
141
|
```
|
|
134
142
|
|
|
135
|
-
|
|
143
|
+
> **Note:** Type validation ensures field types match. Assigning `number` to a `vector3` field will cause a compile error.
|
|
144
|
+
|
|
145
|
+
### 3. Functions
|
|
146
|
+
|
|
136
147
|
```typescript
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
// Standard function with typed parameters
|
|
149
|
+
func Add(a: number, b: number)
|
|
150
|
+
return a + b
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
// Default parameters
|
|
154
|
+
func Greet(name: string, prefix = "Hello")
|
|
155
|
+
print("${prefix}, ${name}!")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
// Local function (Lua local function)
|
|
159
|
+
local function PrivateHelper()
|
|
160
|
+
return "internal"
|
|
161
|
+
end
|
|
139
162
|
|
|
140
|
-
|
|
141
|
-
let
|
|
163
|
+
// Arrow functions
|
|
164
|
+
let double = (x: number) => x * 2
|
|
165
|
+
let greet = (name) => print("Hello ${name}")
|
|
166
|
+
|
|
167
|
+
// Multi-line arrow
|
|
168
|
+
let process = (data) => {
|
|
169
|
+
let result = data * 2
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
142
172
|
```
|
|
143
173
|
|
|
144
|
-
###
|
|
174
|
+
### 4. Control Flow
|
|
175
|
+
|
|
176
|
+
#### If / Else
|
|
145
177
|
```typescript
|
|
146
|
-
|
|
178
|
+
// Note: 'then' keyword is required after condition
|
|
179
|
+
if health < 20 then
|
|
180
|
+
print("Low health!")
|
|
181
|
+
elseif health < 50 then
|
|
182
|
+
print("Medium health")
|
|
183
|
+
else
|
|
184
|
+
print("Full health")
|
|
185
|
+
end
|
|
147
186
|
```
|
|
148
187
|
|
|
149
|
-
|
|
188
|
+
#### Switch / Case
|
|
150
189
|
```typescript
|
|
151
|
-
let
|
|
152
|
-
|
|
190
|
+
let job = "police"
|
|
191
|
+
|
|
192
|
+
switch job
|
|
193
|
+
case "police", "ambulance"
|
|
194
|
+
print("Government worker")
|
|
195
|
+
case "mechanic"
|
|
196
|
+
print("Service worker")
|
|
197
|
+
default
|
|
198
|
+
print("Civilian")
|
|
199
|
+
end
|
|
153
200
|
```
|
|
154
201
|
|
|
155
|
-
|
|
202
|
+
#### Guard Clauses
|
|
203
|
+
Clean up nested if-statements with early returns.
|
|
204
|
+
|
|
156
205
|
```typescript
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
print("
|
|
206
|
+
func RevivePlayer(player)
|
|
207
|
+
// Returns immediately if player is nil
|
|
208
|
+
guard player return
|
|
209
|
+
|
|
210
|
+
// Returns with custom logic if condition fails
|
|
211
|
+
guard player.isDead else
|
|
212
|
+
print("Player is already alive")
|
|
213
|
+
return
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
// Main logic
|
|
217
|
+
player.revive()
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
// Guard with return value
|
|
221
|
+
func GetPlayer(id)
|
|
222
|
+
let player = GetPlayerById(id)
|
|
223
|
+
guard player return nil
|
|
224
|
+
return player
|
|
164
225
|
end
|
|
165
226
|
```
|
|
166
227
|
|
|
167
|
-
|
|
228
|
+
#### For Loops
|
|
168
229
|
```typescript
|
|
169
|
-
//
|
|
170
|
-
for i
|
|
230
|
+
// Numeric for (start, end)
|
|
231
|
+
for i = 1, 10 do
|
|
171
232
|
print(i)
|
|
172
233
|
end
|
|
173
234
|
|
|
174
|
-
//
|
|
175
|
-
for i
|
|
235
|
+
// Numeric for with step (start, end, step)
|
|
236
|
+
for i = 0, 100, 5 do
|
|
176
237
|
print(i)
|
|
177
238
|
end
|
|
178
239
|
|
|
179
|
-
//
|
|
180
|
-
for i
|
|
240
|
+
// Range for with step
|
|
241
|
+
for i in 1..10 by 2 do
|
|
181
242
|
print(i)
|
|
182
243
|
end
|
|
244
|
+
|
|
245
|
+
// For-in (pairs)
|
|
246
|
+
for key, value in pairs(myTable) do
|
|
247
|
+
print("${key}: ${value}")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
// For-in (ipairs)
|
|
251
|
+
for index, item in ipairs(myArray) do
|
|
252
|
+
print("${index}: ${item}")
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### While Loop
|
|
257
|
+
```typescript
|
|
258
|
+
let count = 0
|
|
259
|
+
while count < 10 do
|
|
260
|
+
print(count)
|
|
261
|
+
count += 1
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 5. Modern Operators
|
|
266
|
+
|
|
267
|
+
#### Compound Assignment
|
|
268
|
+
```typescript
|
|
269
|
+
let x = 10
|
|
270
|
+
x += 5
|
|
271
|
+
x -= 3
|
|
272
|
+
x *= 2
|
|
273
|
+
x /= 4
|
|
274
|
+
x %= 3
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Null Coalescing (`??`)
|
|
278
|
+
```typescript
|
|
279
|
+
let name = playerName ?? "Unknown"
|
|
280
|
+
let health = GetHealth() ?? 100
|
|
183
281
|
```
|
|
184
282
|
|
|
185
|
-
|
|
283
|
+
#### Optional Chaining (`?.`)
|
|
284
|
+
```typescript
|
|
285
|
+
let state = LocalPlayer.state?.isDead
|
|
286
|
+
let nested = obj?.deep?.value
|
|
287
|
+
let arrItem = arr?[0]
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Ternary Operator
|
|
291
|
+
```typescript
|
|
292
|
+
let status = isDead ? "Dead" : "Alive"
|
|
293
|
+
let label = count > 0 ? "${count} items" : "Empty"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 6. Destructuring & Spread
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Object Destructuring
|
|
300
|
+
let { x, y, z } = GetEntityCoords(ped)
|
|
301
|
+
let { name, health } = playerData
|
|
302
|
+
|
|
303
|
+
// Array Destructuring
|
|
304
|
+
let [first, second] = GetValues()
|
|
305
|
+
|
|
306
|
+
// Object Spread (tables only)
|
|
307
|
+
let base = { a: 1, b: 2 }
|
|
308
|
+
let extended = { ...base, c: 3 }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 7. String Interpolation
|
|
312
|
+
|
|
313
|
+
Use `${expression}` inside strings to embed values:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
let name = "John"
|
|
317
|
+
let age = 25
|
|
318
|
+
|
|
319
|
+
print("Hello, ${name}!")
|
|
320
|
+
print("You are ${age} years old")
|
|
321
|
+
print("Next year you'll be ${age + 1}")
|
|
322
|
+
|
|
323
|
+
// Complex expressions
|
|
324
|
+
let player = { health: 100 }
|
|
325
|
+
print("Health: ${player.health}")
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 8. Vector Literals
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// Vector3 (FiveM native)
|
|
332
|
+
let pos: vector3 = <100.0, 200.0, 30.0>
|
|
333
|
+
|
|
334
|
+
// Vector2
|
|
335
|
+
let screenPos: vector2 = <0.5, 0.5>
|
|
336
|
+
|
|
337
|
+
// Vector4 (includes heading)
|
|
338
|
+
let spawn: vector4 = <100.0, 200.0, 30.0, 90.0>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 🎮 FiveM Features
|
|
344
|
+
|
|
345
|
+
### Thread & Loop
|
|
186
346
|
|
|
187
|
-
#### Threads & Wait
|
|
188
347
|
```typescript
|
|
348
|
+
// Create a thread (Citizen.CreateThread)
|
|
189
349
|
thread
|
|
190
|
-
|
|
191
|
-
|
|
350
|
+
print("Thread started")
|
|
351
|
+
|
|
352
|
+
loop (true)
|
|
353
|
+
wait 0 // Run every frame
|
|
354
|
+
|
|
355
|
+
// Game logic here
|
|
356
|
+
let ped = PlayerPedId()
|
|
357
|
+
let health = GetEntityHealth(ped)
|
|
358
|
+
|
|
359
|
+
if health < 50 then
|
|
360
|
+
print("Low health warning!")
|
|
361
|
+
end
|
|
362
|
+
end
|
|
192
363
|
end
|
|
193
364
|
```
|
|
194
365
|
|
|
195
|
-
|
|
366
|
+
### Wait
|
|
367
|
+
|
|
196
368
|
```typescript
|
|
197
|
-
|
|
198
|
-
|
|
369
|
+
// Wait in milliseconds
|
|
370
|
+
wait 1000 // Wait 1 second
|
|
371
|
+
wait 0 // Yield one frame
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Network Events
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// Register + Handle network event (Client <-> Server)
|
|
378
|
+
netevent "bank:deposit" (amount)
|
|
379
|
+
print("Depositing ${amount}")
|
|
380
|
+
UpdateBankBalance(amount)
|
|
199
381
|
end
|
|
200
382
|
|
|
201
|
-
event
|
|
202
|
-
|
|
383
|
+
// Local event handler
|
|
384
|
+
event "onClientResourceStart" (resName)
|
|
385
|
+
guard resName == GetCurrentResourceName() return
|
|
386
|
+
print("Resource started!")
|
|
203
387
|
end
|
|
204
388
|
```
|
|
205
389
|
|
|
206
|
-
|
|
390
|
+
### Emit Events
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Emit to server
|
|
394
|
+
emit "server:event:name" (arg1, arg2)
|
|
395
|
+
|
|
396
|
+
// Explicit emit variants
|
|
397
|
+
emitServer "myevent" (data)
|
|
398
|
+
emitClient "myevent" (targetPlayer, data)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Commands
|
|
402
|
+
|
|
207
403
|
```typescript
|
|
404
|
+
// Register a command (RegisterCommand wrapper)
|
|
208
405
|
addcmd "teleport" (source, args)
|
|
209
|
-
|
|
406
|
+
let x = tonumber(args[1]) ?? 0
|
|
407
|
+
let y = tonumber(args[2]) ?? 0
|
|
408
|
+
let z = tonumber(args[3]) ?? 0
|
|
409
|
+
|
|
410
|
+
SetEntityCoords(PlayerPedId(), x, y, z)
|
|
411
|
+
print("Teleported to ${x}, ${y}, ${z}")
|
|
210
412
|
end
|
|
211
413
|
```
|
|
212
414
|
|
|
213
|
-
|
|
415
|
+
### Exports
|
|
416
|
+
|
|
214
417
|
```typescript
|
|
215
|
-
|
|
216
|
-
|
|
418
|
+
// Export a function
|
|
419
|
+
export "GetPlayerMoney" ()
|
|
420
|
+
return playerMoney
|
|
217
421
|
end
|
|
218
422
|
```
|
|
219
423
|
|
|
220
|
-
|
|
424
|
+
### Timeout & Interval
|
|
425
|
+
|
|
221
426
|
```typescript
|
|
222
|
-
|
|
223
|
-
|
|
427
|
+
// Run once after delay
|
|
428
|
+
timeout 5000
|
|
429
|
+
print("5 seconds passed!")
|
|
430
|
+
end
|
|
224
431
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
print("
|
|
228
|
-
return
|
|
432
|
+
// Run repeatedly
|
|
433
|
+
interval 1000
|
|
434
|
+
print("Every second")
|
|
229
435
|
end
|
|
230
436
|
```
|
|
231
437
|
|
|
232
|
-
|
|
438
|
+
### Try / Catch
|
|
439
|
+
|
|
233
440
|
```typescript
|
|
234
441
|
try
|
|
235
|
-
|
|
442
|
+
RiskyOperation()
|
|
236
443
|
catch err
|
|
237
|
-
print("Error: $err")
|
|
444
|
+
print("Error: ${err}")
|
|
445
|
+
end
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## ⚡ Complete Example: Store Robbery System
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// 1. Type Definition
|
|
454
|
+
interface StoreConfig {
|
|
455
|
+
id: number,
|
|
456
|
+
position: vector3,
|
|
457
|
+
reward: number,
|
|
458
|
+
isOpen: boolean
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 2. Configuration
|
|
462
|
+
const Stores: StoreConfig[] = [
|
|
463
|
+
{ id: 1, position: <120.5, -500.2, 30.0>, reward: 5000, isOpen: true },
|
|
464
|
+
{ id: 2, position: <250.0, 300.0, 25.0>, reward: 10000, isOpen: true }
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
let isRobbing = false
|
|
468
|
+
|
|
469
|
+
// 3. Main Game Loop
|
|
470
|
+
thread
|
|
471
|
+
loop (true)
|
|
472
|
+
wait 0
|
|
473
|
+
|
|
474
|
+
if isRobbing then
|
|
475
|
+
break
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
let ped = PlayerPedId()
|
|
479
|
+
let pos = GetEntityCoords(ped)
|
|
480
|
+
|
|
481
|
+
for _, store in ipairs(Stores) do
|
|
482
|
+
if not store.isOpen then
|
|
483
|
+
break
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
let distance = #(pos - store.position)
|
|
487
|
+
if distance < 2.0 then
|
|
488
|
+
ShowHelpText("Press ~INPUT_CONTEXT~ to rob")
|
|
489
|
+
|
|
490
|
+
if IsControlJustPressed(0, 38) then
|
|
491
|
+
StartRobbery(store)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
// 4. Robbery Function
|
|
499
|
+
func StartRobbery(store: StoreConfig)
|
|
500
|
+
isRobbing = true
|
|
501
|
+
|
|
502
|
+
// Notify server
|
|
503
|
+
emit "robbery:start" (store.id)
|
|
504
|
+
|
|
505
|
+
// Play animation
|
|
506
|
+
TaskPlayAnim(PlayerPedId(), "anim_dict", "anim_name", 8.0, -8.0, -1, 0, 0, false, false, false)
|
|
507
|
+
|
|
508
|
+
wait 5000
|
|
509
|
+
|
|
510
|
+
ClearPedTasks(PlayerPedId())
|
|
511
|
+
isRobbing = false
|
|
512
|
+
|
|
513
|
+
print("Robbery complete! Reward: ${store.reward}")
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
// 5. Server Sync Handler
|
|
517
|
+
netevent "robbery:sync" (storeId, newState)
|
|
518
|
+
let store = Stores.find(s => s.id == storeId)
|
|
519
|
+
guard store return
|
|
520
|
+
|
|
521
|
+
store.isOpen = newState
|
|
522
|
+
let stateStr = newState ? "open" : "closed"
|
|
523
|
+
print("Store ${storeId} is now ${stateStr}")
|
|
238
524
|
end
|
|
239
525
|
```
|
|
240
526
|
|
|
241
|
-
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## 🛠️ CLI Reference
|
|
530
|
+
|
|
531
|
+
| Command | Usage | Description |
|
|
532
|
+
|:--------|:------|:------------|
|
|
533
|
+
| **Build** | `ltc build ./` | Compiles all `.lt` files in the folder to `.lua` |
|
|
534
|
+
| **Watch** | `ltc watch ./` | Watches for file changes and recompiles instantly |
|
|
535
|
+
| **Version** | `ltc -v` | Displays the current compiler version |
|
|
536
|
+
|
|
537
|
+
### Project Structure
|
|
538
|
+
|
|
539
|
+
LT automatically detects `src/` folders:
|
|
242
540
|
|
|
243
541
|
```
|
|
244
542
|
my-resource/
|
|
245
543
|
├── src/
|
|
246
544
|
│ ├── client.lt
|
|
247
545
|
│ └── server.lt
|
|
248
|
-
├── client.lua
|
|
249
|
-
|
|
250
|
-
└── fxmanifest.lua
|
|
546
|
+
├── client.lua ← Generated
|
|
547
|
+
└── server.lua ← Generated
|
|
251
548
|
```
|
|
252
549
|
|
|
253
|
-
|
|
550
|
+
---
|
|
254
551
|
|
|
255
552
|
## 🔧 VS Code Extension
|
|
256
553
|
|
|
257
|
-
Get
|
|
258
|
-
|
|
259
|
-
**[LT Language Extension](https://marketplace.visualstudio.com/items?itemName=laot.lt-language)**
|
|
260
|
-
|
|
261
|
-
- Syntax Highlighting
|
|
262
|
-
- IntelliSense for Variables & Functions
|
|
263
|
-
- **Native FiveM Autocompletion** (GetHashKey, CreateVehicle, etc.)
|
|
264
|
-
- Snippets for common patterns
|
|
265
|
-
|
|
266
|
-
## 📜 License
|
|
554
|
+
Get the official **[LT Language Extension](https://marketplace.visualstudio.com/items?itemName=laot.lt-language)** for the best experience.
|
|
267
555
|
|
|
268
|
-
|
|
556
|
+
**Features:**
|
|
557
|
+
* 🎨 Full Syntax Highlighting
|
|
558
|
+
* 🔍 Intelligent Autocompletion
|
|
559
|
+
* 📦 **FiveM Native Intellisense**
|
|
560
|
+
* ✨ Real-time Error Checking
|
|
269
561
|
|
|
270
562
|
---
|
|
271
563
|
|
|
272
564
|
<p align="center">
|
|
273
|
-
|
|
565
|
+
Made with ❤️ for the FiveM Community by <b>LaotScripts</b>
|
|
274
566
|
</p>
|
|
@@ -29,6 +29,7 @@ export declare enum NodeType {
|
|
|
29
29
|
EventHandler = "EventHandler",
|
|
30
30
|
ExportDecl = "ExportDecl",
|
|
31
31
|
TypeDecl = "TypeDecl",
|
|
32
|
+
TypeAliasDecl = "TypeAliasDecl",
|
|
32
33
|
BinaryExpr = "BinaryExpr",
|
|
33
34
|
UnaryExpr = "UnaryExpr",
|
|
34
35
|
UpdateExpr = "UpdateExpr",// ++, --
|
|
@@ -93,6 +94,11 @@ export interface TypeDecl extends Statement {
|
|
|
93
94
|
name: Identifier;
|
|
94
95
|
fields: TypeField[];
|
|
95
96
|
}
|
|
97
|
+
export interface TypeAliasDecl extends Statement {
|
|
98
|
+
kind: NodeType.TypeAliasDecl;
|
|
99
|
+
name: Identifier;
|
|
100
|
+
type: string;
|
|
101
|
+
}
|
|
96
102
|
export interface AssignmentStmt extends Statement {
|
|
97
103
|
kind: NodeType.AssignmentStmt;
|
|
98
104
|
targets: Expression[];
|
|
@@ -35,6 +35,7 @@ export var NodeType;
|
|
|
35
35
|
NodeType["ExportDecl"] = "ExportDecl";
|
|
36
36
|
// Type System
|
|
37
37
|
NodeType["TypeDecl"] = "TypeDecl";
|
|
38
|
+
NodeType["TypeAliasDecl"] = "TypeAliasDecl";
|
|
38
39
|
// Expressions
|
|
39
40
|
NodeType["BinaryExpr"] = "BinaryExpr";
|
|
40
41
|
NodeType["UnaryExpr"] = "UnaryExpr";
|
|
@@ -19,6 +19,18 @@ export class Parser {
|
|
|
19
19
|
}
|
|
20
20
|
// ============ Statement Parsing ============
|
|
21
21
|
parseStatement() {
|
|
22
|
+
// Ambiguity Check: 'type' Identifier = ... VS type(x)
|
|
23
|
+
if (this.at().type === TokenType.IDENTIFIER && this.at().value === 'type') {
|
|
24
|
+
// Lookahead to see if it's a declaration
|
|
25
|
+
const next = this.tokens[this.pos + 1];
|
|
26
|
+
if (next && next.type === TokenType.IDENTIFIER) {
|
|
27
|
+
const nextNext = this.tokens[this.pos + 2];
|
|
28
|
+
if (nextNext && nextNext.type === TokenType.EQUALS) {
|
|
29
|
+
this.eat(); // consume 'type' pseudo-keyword
|
|
30
|
+
return this.parseTypeAlias();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
switch (this.at().type) {
|
|
23
35
|
case TokenType.VAR:
|
|
24
36
|
case TokenType.LET:
|
|
@@ -77,6 +89,65 @@ export class Parser {
|
|
|
77
89
|
return this.parseExpressionStatement();
|
|
78
90
|
}
|
|
79
91
|
}
|
|
92
|
+
// ============ Type Parsing ============
|
|
93
|
+
parseType() {
|
|
94
|
+
// If we're parsing a type and hit a '{', it's an object type literal: value: { a: number }
|
|
95
|
+
if (this.at().type === TokenType.LBRACE) {
|
|
96
|
+
this.eat(); // {
|
|
97
|
+
let typeBody = "{ ";
|
|
98
|
+
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
99
|
+
const key = this.parseIdentifier().name;
|
|
100
|
+
let val = "any";
|
|
101
|
+
// Check for optional property
|
|
102
|
+
if (this.match(TokenType.QUESTION)) {
|
|
103
|
+
key + "?";
|
|
104
|
+
}
|
|
105
|
+
if (this.match(TokenType.COLON)) {
|
|
106
|
+
val = this.parseType();
|
|
107
|
+
}
|
|
108
|
+
typeBody += `${key}: ${val}`;
|
|
109
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
110
|
+
this.match(TokenType.COMMA) || this.match(TokenType.SEMICOLON);
|
|
111
|
+
typeBody += ", ";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.expect(TokenType.RBRACE);
|
|
115
|
+
typeBody += " }";
|
|
116
|
+
// Handle array of objects: { ... }[]
|
|
117
|
+
while (this.match(TokenType.LBRACKET)) {
|
|
118
|
+
this.expect(TokenType.RBRACKET);
|
|
119
|
+
typeBody += "[]";
|
|
120
|
+
}
|
|
121
|
+
return typeBody;
|
|
122
|
+
}
|
|
123
|
+
const typeToken = this.eat();
|
|
124
|
+
let typeName = typeToken.value;
|
|
125
|
+
// Handle generic types: List<string> (Basic support)
|
|
126
|
+
if (this.match(TokenType.LT_ANGLE)) { // <
|
|
127
|
+
typeName += "<";
|
|
128
|
+
typeName += this.parseType();
|
|
129
|
+
while (this.match(TokenType.COMMA)) {
|
|
130
|
+
typeName += ", ";
|
|
131
|
+
typeName += this.parseType();
|
|
132
|
+
}
|
|
133
|
+
this.expect(TokenType.GT_ANGLE); // >
|
|
134
|
+
typeName += ">";
|
|
135
|
+
}
|
|
136
|
+
// Handle array types: string[] or string[][]
|
|
137
|
+
while (this.match(TokenType.LBRACKET)) {
|
|
138
|
+
this.expect(TokenType.RBRACKET);
|
|
139
|
+
typeName += "[]";
|
|
140
|
+
}
|
|
141
|
+
return typeName;
|
|
142
|
+
}
|
|
143
|
+
parseTypeAlias() {
|
|
144
|
+
// 'type' is already consumed or handled by caller
|
|
145
|
+
const name = this.parseIdentifier();
|
|
146
|
+
this.expect(TokenType.EQUALS);
|
|
147
|
+
const typeStr = this.parseType();
|
|
148
|
+
return { kind: AST.NodeType.TypeAliasDecl, name, type: typeStr };
|
|
149
|
+
}
|
|
150
|
+
// ============ Statement Parsing ============
|
|
80
151
|
parseVariableDecl() {
|
|
81
152
|
const scopeToken = this.eat();
|
|
82
153
|
const scope = scopeToken.value;
|
|
@@ -105,12 +176,12 @@ export class Parser {
|
|
|
105
176
|
};
|
|
106
177
|
names.push(parseName());
|
|
107
178
|
if (this.match(TokenType.COLON)) {
|
|
108
|
-
typeAnnotations[0] = this.
|
|
179
|
+
typeAnnotations[0] = this.parseType();
|
|
109
180
|
}
|
|
110
181
|
while (this.match(TokenType.COMMA)) {
|
|
111
182
|
names.push(parseName());
|
|
112
183
|
if (this.match(TokenType.COLON)) {
|
|
113
|
-
typeAnnotations[names.length - 1] = this.
|
|
184
|
+
typeAnnotations[names.length - 1] = this.parseType();
|
|
114
185
|
}
|
|
115
186
|
}
|
|
116
187
|
let values;
|
|
@@ -376,7 +447,7 @@ export class Parser {
|
|
|
376
447
|
this.expect(TokenType.RPAREN);
|
|
377
448
|
let returnType;
|
|
378
449
|
if (this.match(TokenType.COLON)) {
|
|
379
|
-
returnType = this.
|
|
450
|
+
returnType = this.parseType();
|
|
380
451
|
}
|
|
381
452
|
const body = this.parseBlockUntil(TokenType.END);
|
|
382
453
|
this.expect(TokenType.END);
|
|
@@ -399,7 +470,7 @@ export class Parser {
|
|
|
399
470
|
this.expect(TokenType.RPAREN);
|
|
400
471
|
let returnType;
|
|
401
472
|
if (this.match(TokenType.COLON)) {
|
|
402
|
-
returnType = this.
|
|
473
|
+
returnType = this.parseType();
|
|
403
474
|
}
|
|
404
475
|
const body = this.parseBlockUntil(TokenType.END);
|
|
405
476
|
this.expect(TokenType.END);
|
|
@@ -460,13 +531,12 @@ export class Parser {
|
|
|
460
531
|
parseTypeDecl() {
|
|
461
532
|
this.eat(); // interface
|
|
462
533
|
const name = this.parseIdentifier();
|
|
463
|
-
this.expect(TokenType.EQUALS);
|
|
464
534
|
this.expect(TokenType.LBRACE);
|
|
465
535
|
const fields = [];
|
|
466
536
|
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
467
537
|
const fieldName = this.parseIdentifier().name;
|
|
468
538
|
this.expect(TokenType.COLON);
|
|
469
|
-
const fieldType = this.
|
|
539
|
+
const fieldType = this.parseType(); // Get the type as string
|
|
470
540
|
fields.push({ name: fieldName, type: fieldType });
|
|
471
541
|
// Allow trailing comma
|
|
472
542
|
if (!this.check(TokenType.RBRACE)) {
|
|
@@ -913,7 +983,7 @@ export class Parser {
|
|
|
913
983
|
const name = this.parseIdentifierName();
|
|
914
984
|
let typeAnnotation;
|
|
915
985
|
if (this.match(TokenType.COLON)) {
|
|
916
|
-
typeAnnotation = this.
|
|
986
|
+
typeAnnotation = this.parseType();
|
|
917
987
|
}
|
|
918
988
|
let defaultValue;
|
|
919
989
|
if (this.match(TokenType.EQUALS)) {
|
|
@@ -3,7 +3,9 @@ export declare class SemanticAnalyzer {
|
|
|
3
3
|
private globalScope;
|
|
4
4
|
private currentScope;
|
|
5
5
|
private typeRegistry;
|
|
6
|
+
private typeAliasRegistry;
|
|
6
7
|
private memberTypes;
|
|
8
|
+
private functionRegistry;
|
|
7
9
|
analyze(program: AST.Program): void;
|
|
8
10
|
private enterScope;
|
|
9
11
|
private exitScope;
|
|
@@ -18,6 +20,9 @@ export declare class SemanticAnalyzer {
|
|
|
18
20
|
private visitExpression;
|
|
19
21
|
private inferType;
|
|
20
22
|
private stringToType;
|
|
23
|
+
private visitTypeAliasDecl;
|
|
21
24
|
private visitTypeDecl;
|
|
25
|
+
private validateTypeExists;
|
|
22
26
|
private validateTableAgainstType;
|
|
27
|
+
private parseObjectLiteralType;
|
|
23
28
|
}
|
|
@@ -23,14 +23,31 @@ class Scope {
|
|
|
23
23
|
export class SemanticAnalyzer {
|
|
24
24
|
globalScope = new Scope();
|
|
25
25
|
currentScope = this.globalScope;
|
|
26
|
-
typeRegistry = new Map();
|
|
27
|
-
|
|
26
|
+
typeRegistry = new Map(); // Stores interface fields
|
|
27
|
+
typeAliasRegistry = new Map(); // Stores type alias strings
|
|
28
|
+
memberTypes = new Map(); // Tracks Table.Field -> type mappings
|
|
29
|
+
functionRegistry = new Map(); // Stores function signatures
|
|
28
30
|
analyze(program) {
|
|
29
31
|
// Reset scope for fresh analysis (though typically instance is fresh)
|
|
30
32
|
this.currentScope = this.globalScope;
|
|
31
|
-
|
|
33
|
+
this.typeRegistry.clear();
|
|
34
|
+
this.typeAliasRegistry.clear();
|
|
35
|
+
this.memberTypes.clear();
|
|
36
|
+
this.functionRegistry.clear();
|
|
37
|
+
// First pass: Register types (to allow forward references)
|
|
32
38
|
for (const stmt of program.body) {
|
|
33
|
-
|
|
39
|
+
if (stmt.kind === AST.NodeType.TypeDecl) {
|
|
40
|
+
this.visitTypeDecl(stmt);
|
|
41
|
+
}
|
|
42
|
+
else if (stmt.kind === AST.NodeType.TypeAliasDecl) {
|
|
43
|
+
this.visitTypeAliasDecl(stmt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Second pass: Analyze code
|
|
47
|
+
for (const stmt of program.body) {
|
|
48
|
+
if (stmt.kind !== AST.NodeType.TypeDecl && stmt.kind !== AST.NodeType.TypeAliasDecl) {
|
|
49
|
+
this.visitStatement(stmt);
|
|
50
|
+
}
|
|
34
51
|
}
|
|
35
52
|
}
|
|
36
53
|
enterScope() {
|
|
@@ -117,6 +134,29 @@ export class SemanticAnalyzer {
|
|
|
117
134
|
this.exitScope();
|
|
118
135
|
}
|
|
119
136
|
break;
|
|
137
|
+
case AST.NodeType.EventHandler:
|
|
138
|
+
{
|
|
139
|
+
const s = stmt;
|
|
140
|
+
this.enterScope();
|
|
141
|
+
s.params.forEach(p => this.currentScope.define(p.name.name, 'param', 'any', s.line || 0));
|
|
142
|
+
this.visitBlock(s.body);
|
|
143
|
+
this.exitScope();
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case AST.NodeType.TryCatchStmt:
|
|
147
|
+
{
|
|
148
|
+
const s = stmt;
|
|
149
|
+
this.visitBlock(s.tryBody);
|
|
150
|
+
this.enterScope();
|
|
151
|
+
this.currentScope.define(s.catchParam.name, 'let', 'any', s.line || 0);
|
|
152
|
+
this.visitBlock(s.catchBody);
|
|
153
|
+
this.exitScope();
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
// Handle expression statements (standalone function calls like ProcessNumber("hello"))
|
|
157
|
+
case AST.NodeType.CallExpr:
|
|
158
|
+
this.visitExpression(stmt);
|
|
159
|
+
break;
|
|
120
160
|
// Basic fallback recursive visiting
|
|
121
161
|
default:
|
|
122
162
|
// Identify other nodes that contain blocks/expressions to visit
|
|
@@ -171,8 +211,30 @@ export class SemanticAnalyzer {
|
|
|
171
211
|
if (decl.values && decl.values[index]) {
|
|
172
212
|
const value = decl.values[index];
|
|
173
213
|
const inferred = this.inferType(value);
|
|
174
|
-
|
|
175
|
-
|
|
214
|
+
const annotation = decl.typeAnnotations && decl.typeAnnotations[index];
|
|
215
|
+
// If it's an array type (e.g., RobberyConfig[] or number[]) and value is a table, validate each element
|
|
216
|
+
if (annotation && annotation.endsWith('[]') && value.kind === AST.NodeType.TableLiteral) {
|
|
217
|
+
const elementTypeName = annotation.slice(0, -2); // Remove []
|
|
218
|
+
const expectedElementType = this.stringToType(elementTypeName);
|
|
219
|
+
const tableValue = value;
|
|
220
|
+
// Iterate through each element and validate
|
|
221
|
+
for (let i = 0; i < tableValue.fields.length; i++) {
|
|
222
|
+
const field = tableValue.fields[i];
|
|
223
|
+
// If element is a TableLiteral and element type is an interface, validate against interface
|
|
224
|
+
if (field.value.kind === AST.NodeType.TableLiteral && this.typeRegistry.has(elementTypeName)) {
|
|
225
|
+
this.validateTableAgainstType(field.value, elementTypeName, decl.line || 0);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// For primitive types (number[], string[], etc.), check each element type
|
|
229
|
+
const actualType = this.inferType(field.value);
|
|
230
|
+
if (expectedElementType !== 'any' && actualType !== 'any' && expectedElementType !== actualType) {
|
|
231
|
+
throw new Error(`Type mismatch in array at line ${decl.line}: Element ${i + 1} expected '${elementTypeName}', got '${actualType}'`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// If we have a custom type annotation and the value is a table (not array), validate it
|
|
237
|
+
else if (customTypeName && value.kind === AST.NodeType.TableLiteral) {
|
|
176
238
|
this.validateTableAgainstType(value, customTypeName, decl.line || 0);
|
|
177
239
|
}
|
|
178
240
|
// If we have an annotation, check compatibility
|
|
@@ -221,8 +283,36 @@ export class SemanticAnalyzer {
|
|
|
221
283
|
}
|
|
222
284
|
}
|
|
223
285
|
}
|
|
224
|
-
// Handle member expression type checking
|
|
286
|
+
// Handle member expression type checking
|
|
225
287
|
if (t.kind === AST.NodeType.MemberExpr) {
|
|
288
|
+
const member = t;
|
|
289
|
+
// 1. Strict Property Existence Check
|
|
290
|
+
if (member.object.kind === AST.NodeType.Identifier && !member.computed) {
|
|
291
|
+
const objName = member.object.name;
|
|
292
|
+
const sym = this.currentScope.lookup(objName);
|
|
293
|
+
const propName = member.property.name;
|
|
294
|
+
if (sym && sym.type !== 'any' && sym.type !== 'table') {
|
|
295
|
+
// Check if it's an interface or alias
|
|
296
|
+
let fields;
|
|
297
|
+
// Remove array suffix for type lookup if needed (though variable shouldn't be array if accessing prop directly)
|
|
298
|
+
const baseType = sym.type.replace('[]', '');
|
|
299
|
+
if (this.typeRegistry.has(baseType)) {
|
|
300
|
+
fields = this.typeRegistry.get(baseType);
|
|
301
|
+
}
|
|
302
|
+
else if (this.typeAliasRegistry.has(baseType)) {
|
|
303
|
+
const aliasStr = this.typeAliasRegistry.get(baseType);
|
|
304
|
+
if (aliasStr.startsWith('{')) {
|
|
305
|
+
fields = this.parseObjectLiteralType(aliasStr);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (fields) {
|
|
309
|
+
const fieldExists = fields.some(f => f.name === propName);
|
|
310
|
+
if (!fieldExists) {
|
|
311
|
+
throw new Error(`Property '${propName}' does not exist on type '${sym.type}' at line ${stmt.line}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
226
316
|
const memberKey = this.getMemberExprKey(t);
|
|
227
317
|
// If this assignment has a type annotation, register it
|
|
228
318
|
if (stmt.typeAnnotation && index === 0) {
|
|
@@ -279,8 +369,22 @@ export class SemanticAnalyzer {
|
|
|
279
369
|
}
|
|
280
370
|
visitFunctionDecl(decl) {
|
|
281
371
|
// Name is defined in CURRENT scope
|
|
372
|
+
let funcName;
|
|
282
373
|
if (decl.name && decl.name.kind === AST.NodeType.Identifier) {
|
|
283
|
-
|
|
374
|
+
funcName = decl.name.name;
|
|
375
|
+
this.currentScope.define(funcName, 'const', 'function', decl.line || 0);
|
|
376
|
+
}
|
|
377
|
+
// Register function signature for call validation
|
|
378
|
+
const paramTypes = [];
|
|
379
|
+
decl.params.forEach(p => {
|
|
380
|
+
let type = 'any';
|
|
381
|
+
if (p.typeAnnotation) {
|
|
382
|
+
type = this.stringToType(p.typeAnnotation);
|
|
383
|
+
}
|
|
384
|
+
paramTypes.push({ name: p.name.name, type });
|
|
385
|
+
});
|
|
386
|
+
if (funcName) {
|
|
387
|
+
this.functionRegistry.set(funcName, { params: paramTypes });
|
|
284
388
|
}
|
|
285
389
|
this.enterScope();
|
|
286
390
|
decl.params.forEach(p => {
|
|
@@ -310,6 +414,41 @@ export class SemanticAnalyzer {
|
|
|
310
414
|
// Mostly just traversing to find nested scopes in functions/arrows
|
|
311
415
|
if (!expr)
|
|
312
416
|
return;
|
|
417
|
+
// Check for undefined variable usage
|
|
418
|
+
if (expr.kind === AST.NodeType.Identifier) {
|
|
419
|
+
const id = expr;
|
|
420
|
+
const sym = this.currentScope.lookup(id.name);
|
|
421
|
+
// Skip globals/builtins - only check if it looks like a user variable
|
|
422
|
+
// FiveM/Lua has many globals, so we only warn for simple lowercase identifiers
|
|
423
|
+
// that are not common Lua globals
|
|
424
|
+
const luaGlobals = ['print', 'type', 'tostring', 'tonumber', 'pairs', 'ipairs',
|
|
425
|
+
'next', 'error', 'assert', 'pcall', 'xpcall', 'require',
|
|
426
|
+
'table', 'string', 'math', 'os', 'io', 'coroutine', 'debug',
|
|
427
|
+
'setmetatable', 'getmetatable', 'rawget', 'rawset', 'rawequal',
|
|
428
|
+
'collectgarbage', 'dofile', 'load', 'loadfile', 'select',
|
|
429
|
+
'unpack', '_G', '_VERSION', 'true', 'false', 'nil',
|
|
430
|
+
// FiveM common globals
|
|
431
|
+
'Citizen', 'CreateThread', 'Wait', 'GetGameTimer', 'vector3',
|
|
432
|
+
'vector2', 'vector4', 'GetCurrentResourceName', 'exports',
|
|
433
|
+
'PlayerPedId', 'GetPlayerPed', 'GetEntityCoords', 'TriggerEvent',
|
|
434
|
+
'TriggerServerEvent', 'TriggerClientEvent', 'RegisterNetEvent',
|
|
435
|
+
'AddEventHandler', 'RegisterCommand', 'IsControlJustPressed',
|
|
436
|
+
'AddEventHandler', 'RegisterCommand', 'LocalPlayer', 'PlayerId',
|
|
437
|
+
'GetPlayerServerId', 'source', 'SetEntityHealth', 'ESX', 'QBCore',
|
|
438
|
+
// Additional FiveM/Lua globals
|
|
439
|
+
'Config', 'Framework', 'MySQL', 'lib', 'cache', 'json',
|
|
440
|
+
'NetworkGetNetworkIdFromEntity', 'NetworkGetEntityFromNetworkId',
|
|
441
|
+
'GetPlayerPed', 'GetVehiclePedIsIn', 'IsPedInAnyVehicle',
|
|
442
|
+
'SetPedCanRagdoll', 'SetTimeout', 'SetInterval', 'ClearTimeout',
|
|
443
|
+
'GetHashKey', 'IsModelValid', 'RequestModel', 'HasModelLoaded'];
|
|
444
|
+
if (!sym && !luaGlobals.includes(id.name)) {
|
|
445
|
+
// Check if it's a type name (not a variable)
|
|
446
|
+
if (!this.typeRegistry.has(id.name) && !this.typeAliasRegistry.has(id.name)) {
|
|
447
|
+
throw new Error(`Undefined variable '${id.name}' at line ${id.line ?? 0}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
313
452
|
if (expr.kind === AST.NodeType.ArrowFunc) {
|
|
314
453
|
const arrow = expr;
|
|
315
454
|
this.enterScope();
|
|
@@ -335,8 +474,57 @@ export class SemanticAnalyzer {
|
|
|
335
474
|
this.visitFunctionDecl(expr);
|
|
336
475
|
return;
|
|
337
476
|
}
|
|
338
|
-
//
|
|
339
|
-
|
|
477
|
+
// Handle TableLiteral - recurse into field values only
|
|
478
|
+
if (expr.kind === AST.NodeType.TableLiteral) {
|
|
479
|
+
const table = expr;
|
|
480
|
+
for (const field of table.fields) {
|
|
481
|
+
// Don't check keys - they are field names, not variable references
|
|
482
|
+
// Only check computed keys like [expression] or string keys
|
|
483
|
+
if (field.key && field.key.kind !== AST.NodeType.Identifier) {
|
|
484
|
+
this.visitExpression(field.key);
|
|
485
|
+
}
|
|
486
|
+
this.visitExpression(field.value);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Handle BinaryExpr
|
|
491
|
+
if (expr.kind === AST.NodeType.BinaryExpr) {
|
|
492
|
+
const bin = expr;
|
|
493
|
+
this.visitExpression(bin.left);
|
|
494
|
+
this.visitExpression(bin.right);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Handle CallExpr - recurse into callee and args, validate argument types
|
|
498
|
+
if (expr.kind === AST.NodeType.CallExpr) {
|
|
499
|
+
const call = expr;
|
|
500
|
+
this.visitExpression(call.callee);
|
|
501
|
+
// Validate argument types if callee is a known function
|
|
502
|
+
if (call.callee.kind === AST.NodeType.Identifier) {
|
|
503
|
+
const funcName = call.callee.name;
|
|
504
|
+
const funcInfo = this.functionRegistry.get(funcName);
|
|
505
|
+
if (funcInfo) {
|
|
506
|
+
// Check each argument against expected param type
|
|
507
|
+
for (let i = 0; i < call.args.length && i < funcInfo.params.length; i++) {
|
|
508
|
+
const expectedType = funcInfo.params[i].type;
|
|
509
|
+
const argType = this.inferType(call.args[i]);
|
|
510
|
+
if (expectedType !== 'any' && argType !== 'any' && expectedType !== argType) {
|
|
511
|
+
throw new Error(`Type mismatch in function call '${funcName}' at line ${call.line || 0}: Argument ${i + 1} expects '${expectedType}', got '${argType}'`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const arg of call.args) {
|
|
517
|
+
this.visitExpression(arg);
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Handle MemberExpr - only check the object, not property (obj.property)
|
|
522
|
+
if (expr.kind === AST.NodeType.MemberExpr) {
|
|
523
|
+
const member = expr;
|
|
524
|
+
this.visitExpression(member.object);
|
|
525
|
+
// Don't check member.property - it's a field access, not a variable
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
340
528
|
}
|
|
341
529
|
inferType(expr) {
|
|
342
530
|
if (!expr)
|
|
@@ -361,29 +549,96 @@ export class SemanticAnalyzer {
|
|
|
361
549
|
}
|
|
362
550
|
}
|
|
363
551
|
stringToType(str) {
|
|
364
|
-
|
|
552
|
+
// Array types are tables
|
|
553
|
+
if (str.endsWith('[]'))
|
|
554
|
+
return 'table';
|
|
555
|
+
// Remove array notation for basic type check: string[] -> string
|
|
556
|
+
const baseStr = str.replace(/\[\]/g, '');
|
|
557
|
+
switch (baseStr) {
|
|
365
558
|
case 'string': return 'string';
|
|
366
559
|
case 'number': return 'number';
|
|
367
560
|
case 'boolean': return 'boolean';
|
|
368
|
-
case 'vector':
|
|
561
|
+
case 'vector':
|
|
562
|
+
case 'vector2':
|
|
563
|
+
case 'vector3':
|
|
564
|
+
case 'vector4': return 'vector';
|
|
369
565
|
case 'table': return 'table';
|
|
370
566
|
case 'function': return 'function';
|
|
371
567
|
case 'any': return 'any';
|
|
372
568
|
default:
|
|
373
|
-
// Check if it's a
|
|
374
|
-
if (this.typeRegistry.has(
|
|
375
|
-
return 'table';
|
|
569
|
+
// Check if it's a registered interface
|
|
570
|
+
if (this.typeRegistry.has(baseStr)) {
|
|
571
|
+
return 'table';
|
|
572
|
+
}
|
|
573
|
+
// Check if it's a type alias
|
|
574
|
+
if (this.typeAliasRegistry.has(baseStr)) {
|
|
575
|
+
// If alias resolves to a primitive, return that. If object literal, return 'table'
|
|
576
|
+
const aliased = this.typeAliasRegistry.get(baseStr);
|
|
577
|
+
if (aliased.startsWith('{'))
|
|
578
|
+
return 'table';
|
|
579
|
+
return this.stringToType(aliased);
|
|
376
580
|
}
|
|
377
581
|
return 'any';
|
|
378
582
|
}
|
|
379
583
|
}
|
|
584
|
+
visitTypeAliasDecl(decl) {
|
|
585
|
+
this.typeAliasRegistry.set(decl.name.name, decl.type);
|
|
586
|
+
// If it's an object literal type, validate field types
|
|
587
|
+
if (decl.type.startsWith('{')) {
|
|
588
|
+
const fields = this.parseObjectLiteralType(decl.type);
|
|
589
|
+
for (const field of fields) {
|
|
590
|
+
this.validateTypeExists(field.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
// It's a simple alias, validate the aliased type exists
|
|
595
|
+
this.validateTypeExists(decl.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
380
598
|
visitTypeDecl(decl) {
|
|
381
599
|
const typeName = decl.name.name;
|
|
382
600
|
// Register the type
|
|
383
601
|
this.typeRegistry.set(typeName, decl.fields);
|
|
602
|
+
// Validate that all field types exist
|
|
603
|
+
for (const field of decl.fields) {
|
|
604
|
+
this.validateTypeExists(field.type, decl.name.line ?? 0, `interface '${typeName}'`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
validateTypeExists(typeStr, line, context) {
|
|
608
|
+
// Remove array brackets for base type check
|
|
609
|
+
const baseType = typeStr.replace(/\[\]/g, '').trim();
|
|
610
|
+
// Skip object literal types (they are inline definitions)
|
|
611
|
+
if (baseType.startsWith('{'))
|
|
612
|
+
return;
|
|
613
|
+
// Check primitive types (including FiveM vector types)
|
|
614
|
+
const primitives = ['string', 'number', 'boolean', 'nil', 'any', 'table', 'function', 'vector', 'void', 'vector2', 'vector3', 'vector4'];
|
|
615
|
+
if (primitives.includes(baseType))
|
|
616
|
+
return;
|
|
617
|
+
// Check type registry (interfaces)
|
|
618
|
+
if (this.typeRegistry.has(baseType))
|
|
619
|
+
return;
|
|
620
|
+
// Check type alias registry
|
|
621
|
+
if (this.typeAliasRegistry.has(baseType))
|
|
622
|
+
return;
|
|
623
|
+
// Type not found!
|
|
624
|
+
throw new Error(`Unknown type '${baseType}' in ${context} at line ${line}`);
|
|
384
625
|
}
|
|
385
626
|
validateTableAgainstType(table, typeName, line) {
|
|
386
|
-
|
|
627
|
+
let fields;
|
|
628
|
+
// Remove array brackets if present (e.g. validating a single object against MyType[])
|
|
629
|
+
// This shouldn't happen often in this function call context but good for safety
|
|
630
|
+
typeName = typeName.replace(/\[\]/g, '');
|
|
631
|
+
// 1. Check Interface Registry
|
|
632
|
+
if (this.typeRegistry.has(typeName)) {
|
|
633
|
+
fields = this.typeRegistry.get(typeName);
|
|
634
|
+
}
|
|
635
|
+
// 2. Check Type Aliases (specifically object literal aliases)
|
|
636
|
+
else if (this.typeAliasRegistry.has(typeName)) {
|
|
637
|
+
const aliasStr = this.typeAliasRegistry.get(typeName);
|
|
638
|
+
if (aliasStr.startsWith('{')) {
|
|
639
|
+
fields = this.parseObjectLiteralType(aliasStr);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
387
642
|
if (!fields)
|
|
388
643
|
return; // Unknown type, skip validation
|
|
389
644
|
const providedFields = new Map();
|
|
@@ -408,4 +663,20 @@ export class SemanticAnalyzer {
|
|
|
408
663
|
}
|
|
409
664
|
}
|
|
410
665
|
}
|
|
666
|
+
parseObjectLiteralType(typeStr) {
|
|
667
|
+
const content = typeStr.trim().slice(1, -1);
|
|
668
|
+
if (!content)
|
|
669
|
+
return [];
|
|
670
|
+
const fields = [];
|
|
671
|
+
const parts = content.split(/,|;/);
|
|
672
|
+
for (const part of parts) {
|
|
673
|
+
if (!part.trim())
|
|
674
|
+
continue;
|
|
675
|
+
const [key, val] = part.split(':').map(s => s.trim());
|
|
676
|
+
if (key && val) {
|
|
677
|
+
fields.push({ name: key.replace('?', ''), type: val });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return fields;
|
|
681
|
+
}
|
|
411
682
|
}
|