Skip to content

Commit b6703d3

Browse files
committed
Traceback for Undefined Calls.
1 parent b6fe6de commit b6703d3

File tree

2 files changed

+207
-50
lines changed

2 files changed

+207
-50
lines changed

lib/scripting/lua.dart

Lines changed: 154 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:collection';
33
import 'dart:io';
4+
import 'dart:math';
45

56
import '/extensions.dart';
67
import '/reyveld.dart';
@@ -22,6 +23,9 @@ typedef LuaResult = ({
2223

2324
/// The main class for running lua scripts.
2425
class Lua {
26+
static const String _classMetafield = "__classHash";
27+
static const String _objMetafield = "__objHash";
28+
2529
final WebSocketChannel? socket;
2630

2731
final Set<Stopwatch> _stopwatches = {};
@@ -168,7 +172,33 @@ class Lua {
168172

169173
// Add all static exports as global object.
170174
for (final interface_ in interfaces) {
171-
await addGlobal(state, interface_.className, interface_.staticTable);
175+
await addGlobal(state, interface_.className, {
176+
"className": "_Type"
177+
}, metatable: {
178+
_classMetafield: getClassHash(interface_.className),
179+
"__index": (Lua lua, LuaState state) async {
180+
final indexKey = await lua.getFromTop<String>(state);
181+
Reyveld.talker.verbose("Index key: $indexKey");
182+
if (await state.getMetafield(state.getTop(), _classMetafield) ==
183+
LuaType.luaString) {
184+
final hash = await lua.getFromTop<String>(state);
185+
final interface_ = interfaces
186+
.where((i) => getClassHash(i.className) == hash)
187+
.singleOrNull;
188+
if (interface_ == null) {
189+
throw Exception("No interface found.");
190+
}
191+
final result = interface_.staticTable[indexKey];
192+
if (result == null) {
193+
return Undefined("No result found for $indexKey");
194+
} else {
195+
return result;
196+
}
197+
} else {
198+
return Undefined("No index key found.");
199+
}
200+
},
201+
});
172202
}
173203

174204
await addGlobal(state, "is", (Lua lua, LuaState state) async {
@@ -185,21 +215,67 @@ class Lua {
185215
/// [value] is the table to add as the global.
186216
///
187217
/// This will push the table to the stack and then set the global with the given name.
188-
Future<void> addGlobal(LuaState state, String name, dynamic value) async {
189-
// if (table == null) {
190-
// return;
191-
// }
218+
Future<void> addGlobal(LuaState state, String name, dynamic value,
219+
{Map<String, dynamic>? metatable}) async {
192220
await _pushToStack(state, value);
221+
if (metatable != null) {
222+
await _applyMetatableToTop(state, metatable);
223+
}
193224
await state.setGlobal(name);
194225
}
195226

196-
SInterface? _getObject(LuaState state, String hash) => _objects[state]?[hash];
227+
SInterface? getObject(LuaState state, String hash) => _objects[state]?[hash];
197228
void _setObject(LuaState state, String hash, SInterface interface_) =>
198229
_objects[state]![hash] = interface_;
199230

231+
/// Applies a metatable to the top of the stack.
232+
///
233+
/// This will create a new table on top of the previous table, and then add all key-value pairs from [metatable] to the table.
234+
/// Then, it will set the newly created table as the metatable of the top of the stack.
235+
Future<void> _applyMetatableToTop(
236+
LuaState state, Map<String, dynamic> metatable) async {
237+
state.newTable();
238+
for (final key in metatable.keys) {
239+
await _pushToStack(state, key);
240+
await _pushToStack(state, metatable[key]);
241+
await state.setTable(state.getTop() - 2);
242+
}
243+
state.setMetatable(state.getTop() - 1);
244+
}
245+
246+
/// Handles undefined calls.
247+
Future<void> _undefined(Lua lua, LuaState state) async {
248+
while (state.getTop() > 3) {
249+
state.pop(1);
250+
}
251+
252+
final col = await getFromTop<int>(state) ?? 0;
253+
final row = await getFromTop<int>(state) ?? 0;
254+
final undefined = await getFromTop(state);
255+
String message = "Undefined";
256+
257+
if (undefined is Map) {
258+
if (undefined["message"] != null) {
259+
message = undefined["message"];
260+
}
261+
}
262+
263+
throw ReyveldCallException(message,
264+
traceback: Traceback(getLoadedScript(state), row, col));
265+
}
266+
200267
/// Pushes a value to the stack.
201268
Future<void> _pushToStack(LuaState state, dynamic value) async {
202-
if (value is String) {
269+
Reyveld.talker.verbose("Pushing $value to stack.");
270+
if (value is Undefined) {
271+
// Reyveld.talker.verbose("Pushing undefined to stack.");
272+
await _pushToStack(state, {"message": value.message});
273+
await _applyMetatableToTop(state, {
274+
"__call": _undefined,
275+
"__index": _undefined,
276+
"__newindex": _undefined,
277+
});
278+
} else if (value is String) {
203279
state.pushString(value);
204280
} else if (value is int) {
205281
state.pushInteger(value);
@@ -219,11 +295,20 @@ class Lua {
219295
Reyveld.talker.verbose("Wrapping $value in $interface_.");
220296
final hash = generateUUID();
221297
_setObject(state, hash, interface_);
222-
final table = interface_.toLua(hash);
298+
final table = interface_.toLua();
223299
await _pushToStack(state, table);
300+
await _applyMetatableToTop(
301+
state, {_objMetafield: hash, "__index": SInterface.betterIndex});
224302
} else if (value is FutureOr<dynamic> Function(Lua, LuaState)) {
303+
// Reyveld.talker.verbose("Pushing function to stack.");
225304
state.pushDartFunction((state) async {
226-
await _pushToStack(state, await value(this, state));
305+
try {
306+
await _pushToStack(state, await value(this, state));
307+
} catch (e, st) {
308+
state.pushString("${e.toString()}\n$st");
309+
state.error();
310+
return 0;
311+
}
227312
return 1;
228313
});
229314
} else if (value is LEntry) {
@@ -246,6 +331,8 @@ class Lua {
246331
int row = finalArgs.removeAt(0);
247332
int col = finalArgs.removeAt(0);
248333

334+
final trackback = Traceback(getLoadedScript(state), row, col);
335+
249336
Map namedArgs = {};
250337

251338
if (value.hasNamedArgs &&
@@ -260,8 +347,9 @@ class Lua {
260347
final argValue = finalArgs[i];
261348
final argType = value.args.elementAt(i);
262349
if (argType.cast(argValue) == null && argType.required) {
263-
throw Exception(
264-
"Type Mismatch for argument $argType at position $i! Expected ${argType.type} but got ${argValue.runtimeType}");
350+
throw ReyveldCallException(
351+
"Type Mismatch for argument $argType at position $i! Expected ${argType.type} but got ${argValue.runtimeType}",
352+
traceback: trackback);
265353
} else {
266354
finalArgs[i] = argType.cast(argValue);
267355
}
@@ -272,8 +360,9 @@ class Lua {
272360
final argValue = namedArgs[key];
273361
final argType = value.namedArgs.where((e) => e.name == key).single;
274362
if (argType.cast(argValue) == null && argType.required) {
275-
throw Exception(
276-
"Type Mismatch for argument $argType at position $key! Expected ${argType.type} but got ${argValue.runtimeType}");
363+
throw ReyveldCallException(
364+
"Type Mismatch for argument $argType at position $key! Expected ${argType.type} but got ${argValue.runtimeType}",
365+
traceback: trackback);
277366
} else {
278367
namedArgs[key] = argType.cast(argValue);
279368
}
@@ -345,14 +434,9 @@ class Lua {
345434
return 1;
346435
}
347436
} catch (e) {
348-
_pushToStack(
349-
state,
350-
ReyveldCallException(
351-
"Failed to call function '${value.name}' with $finalArgs$namedArgs",
352-
traceback: Traceback(getLoadedScript(state), row, col))
353-
.toString());
354-
state.error();
355-
return 0;
437+
throw ReyveldCallException(
438+
"Failed to call function '${value.name}' with $finalArgs$namedArgs",
439+
traceback: trackback);
356440
}
357441
});
358442
} else if (value is LField) {
@@ -390,20 +474,36 @@ class Lua {
390474
await state.rawGetI(luaRegistryIndex, (result as LuaFuncRef).ref);
391475
} else if (state.isTable(state.getTop())) {
392476
/// If the top of the stack is a table, get the table and check if it has an objHash key.
393-
final table = await _getTableFromState(state);
394-
if (table.containsKey("objHash") &&
395-
_getObject(state, table["objHash"]) != null) {
396-
/// If the table has an objHash key, then it means it is an interface for an object,
397-
/// so get the object and return it.
398-
result = _getObject(state, table["objHash"])!.object;
399-
} else if (table.containsKey("__hash__") &&
400-
interfaces.any((key) => key.classHash == table["__hash__"])) {
401-
result = interfaces
402-
.firstWhere((key) => key.classHash == table["__hash__"]);
403-
} else if (table.keys.every((key) => key is int)) {
404-
result = table.values.toList();
477+
final success = state.getMetatable(state.getTop());
478+
Reyveld.talker.verbose("Success: $success");
479+
if (success) {
480+
final metatable = await getFromTop<Map>(state);
481+
final table = await _getTableFromState(state);
482+
Reyveld.talker.verbose("Table: $table Metatable: $metatable");
483+
if (metatable != null &&
484+
metatable.containsKey(_objMetafield) &&
485+
getObject(state, metatable[_objMetafield]) != null) {
486+
result = getObject(state, metatable[_objMetafield])!.object;
487+
} else if (metatable != null &&
488+
metatable.containsKey(_classMetafield) &&
489+
interfaces
490+
.any((key) => key.classHash == metatable[_classMetafield])) {
491+
result = interfaces
492+
.where((key) => key.classHash == metatable[_classMetafield])
493+
.singleOrNull;
494+
} else if (table.keys.every((key) => key is int)) {
495+
result = table.values.toList();
496+
} else {
497+
result = table;
498+
}
405499
} else {
406-
result = table;
500+
final table = await _getTableFromState(state);
501+
Reyveld.talker.verbose("Table: $table");
502+
if (table.keys.every((key) => key is int)) {
503+
result = table.values.toList();
504+
} else {
505+
result = table;
506+
}
407507
}
408508
} else if (state.isNoneOrNil(state.getTop())) {
409509
result = null;
@@ -429,11 +529,13 @@ class Lua {
429529
while (state.next(state.getTop() - 1)) {
430530
dynamic value = await getFromTop(state);
431531
dynamic key = await getFromTop(state);
432-
if (key is String && resultTable.isEmpty) {
433-
resultTable = <String, dynamic>{};
434-
} else if (key is int && resultTable.isEmpty) {
435-
resultTable = <int, dynamic>{};
436-
}
532+
// if (key is String && resultTable.isEmpty) {
533+
// Reyveld.talker.verbose("Keys are is string.");
534+
// resultTable = <String, dynamic>{};
535+
// } else if (key is int && resultTable.isEmpty) {
536+
// Reyveld.talker.verbose("Keys are is int.");
537+
// resultTable = <int, dynamic>{};
538+
// }
437539
resultTable[key] = value;
438540
await _pushToStack(state, key);
439541
}
@@ -662,7 +764,10 @@ ${enum_.key} = {
662764
return formattedEnums.join("\n\n");
663765
}
664766

665-
static String getLoadedScript(LuaState state) => _loadedScripts[state] ?? "";
767+
static String getLoadedScript(LuaState state) {
768+
Reyveld.talker.verbose("Getting script.");
769+
return _loadedScripts[state] ?? "";
770+
}
666771
}
667772

668773
/// A reference to a lua function.
@@ -730,8 +835,9 @@ class Traceback {
730835
final lines = code.split("\n");
731836
final buffer = StringBuffer();
732837

733-
final start = line - linesShown ~/ 2;
734-
final end = line + linesShown ~/ 2 + (linesShown % 2);
838+
final start = line - max<int>(linesShown ~/ 2, 0);
839+
final end =
840+
min<int>(line + linesShown ~/ 2 + (linesShown % 2), lines.length);
735841
for (int i = start; i < end; i++) {
736842
if (i < 0 || i >= lines.length) continue;
737843
buffer.write(i == line ? "🔴" : " ");
@@ -740,3 +846,9 @@ class Traceback {
740846
return buffer.toString();
741847
}
742848
}
849+
850+
class Undefined {
851+
final String? message;
852+
853+
const Undefined([this.message]);
854+
}

lib/scripting/sinterface.dart

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22
import 'dart:io';
33

44
import 'package:lua_dardo_async/lua.dart';
5+
import 'package:reyveld/reyveld.dart';
56

67
import '/skit/sobject.dart';
78
export 'lua.dart';
@@ -97,6 +98,19 @@ class LEntry extends LExport {
9798
int get hashCode => name.hashCode;
9899
}
99100

101+
class LVar<T> extends LExport {
102+
final bool nullable;
103+
final T Function() getter;
104+
final void Function(T)? setter;
105+
106+
LVar(
107+
{required super.name,
108+
super.descr = "",
109+
this.nullable = false,
110+
required this.getter,
111+
this.setter});
112+
}
113+
100114
/// The kind of the arg.
101115
/// - requiredPositional
102116
/// Lua: `Example(x)`
@@ -200,13 +214,45 @@ abstract class SInterface<T> {
200214
String get classHash => Lua.getClassHash(className);
201215

202216
/// This converts the interface into a Lua table.
203-
Map<String, dynamic> toLua(String luaHash) {
204-
Map<String, LExport> exportTable = {};
205-
for (final export in allExports) {
206-
export.interface_ = this;
207-
exportTable[export.name] = export;
217+
Map<String, dynamic> toLua() {
218+
// Map<String, LExport> exportTable = {};
219+
// for (final export in allExports) {
220+
// export.interface_ = this;
221+
// exportTable[export.name] = export;
222+
// }
223+
return {"className": className};
224+
}
225+
226+
Object? index(String key) =>
227+
allExports.where((e) => e.name == key).singleOrNull;
228+
229+
static Future<dynamic> betterIndex(Lua lua, LuaState state) async {
230+
final indexKey = await lua.getFromTop<String>(state);
231+
Reyveld.talker.verbose("Index key: $indexKey");
232+
if (indexKey == null) {
233+
throw Exception("No index key found.");
234+
} else if (await state.getMetafield(state.getTop(), "__objHash") ==
235+
LuaType.luaString) {
236+
final hash = await lua.getFromTop<String>(state);
237+
if (hash == null) {
238+
throw ReyveldCallException("No hash found.");
239+
} else {
240+
final interface_ = lua.getObject(state, hash);
241+
if (interface_ == null) {
242+
throw Exception("No interface found.");
243+
} else {
244+
final result = interface_.index(indexKey);
245+
if (result == null) {
246+
Reyveld.talker.verbose("No result found for $indexKey");
247+
return Undefined();
248+
} else {
249+
return result;
250+
}
251+
}
252+
}
253+
} else {
254+
return Undefined();
208255
}
209-
return {"class": classHash, "objHash": luaHash, ...exportTable};
210256
}
211257

212258
/// This is the object that this interface wraps around.
@@ -245,9 +291,8 @@ abstract class SInterface<T> {
245291

246292
/// Used to push statics as a global table.
247293
Map<String, dynamic> get staticTable {
248-
final map = <String, dynamic>{"__hash__": classHash};
294+
final map = <String, dynamic>{};
249295
for (final entry in statics) {
250-
if (entry.name == "__hash__") continue;
251296
map[entry.name] = entry;
252297
}
253298
return map;

0 commit comments

Comments
 (0)