inscript-lang 1.8.2__tar.gz → 1.8.4__tar.gz

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.
Files changed (30) hide show
  1. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/PKG-INFO +1 -1
  2. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/analyzer.py +387 -26
  3. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript.py +1 -1
  4. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/pyproject.toml +1 -1
  6. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/repl.py +1 -1
  7. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/README.md +0 -0
  8. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/ast_nodes.py +0 -0
  9. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/compiler.py +0 -0
  10. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/environment.py +0 -0
  11. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/errors.py +0 -0
  12. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_fmt.py +0 -0
  13. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/SOURCES.txt +0 -0
  14. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/dependency_links.txt +0 -0
  15. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/entry_points.txt +0 -0
  16. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/requires.txt +0 -0
  17. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/top_level.txt +0 -0
  18. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_test.py +0 -0
  19. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/interpreter.py +0 -0
  20. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/lexer.py +0 -0
  21. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/parser.py +0 -0
  22. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/pygame_backend.py +0 -0
  23. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/setup.cfg +0 -0
  24. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/setup.py +0 -0
  25. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib.py +0 -0
  26. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_extended.py +0 -0
  27. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_game.py +0 -0
  29. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_values.py +0 -0
  30. {inscript_lang-1.8.2 → inscript_lang-1.8.4}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 1.8.2
3
+ Version: 1.8.4
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -64,10 +64,13 @@ T_TRANSFORM2D = InScriptType("Transform2D")
64
64
  T_TRANSFORM3D = InScriptType("Transform3D")
65
65
  T_TEXTURE = InScriptType("Texture")
66
66
 
67
+ T_NEVER = InScriptType("never") # v1.8.3: bottom type — function never returns normally
68
+
67
69
  BUILTIN_TYPES: Dict[str, InScriptType] = {
68
70
  # canonical names
69
71
  "int": T_INT, "float": T_FLOAT, "bool": T_BOOL,
70
72
  "string": T_STRING, "void": T_VOID, "null": T_NULL, "any": T_ANY,
73
+ "never": T_NEVER, # v1.8.3
71
74
  # common aliases
72
75
  "str": T_STRING, "boolean": T_BOOL, "number": T_FLOAT,
73
76
  "nil": T_NULL, "object": T_ANY, "auto": T_ANY,
@@ -141,6 +144,8 @@ def types_compatible(expected: InScriptType, got: InScriptType) -> bool:
141
144
  """True if `got` can be used where `expected` is required."""
142
145
  if expected == T_ANY or got == T_ANY: return True
143
146
  if expected == got: return True
147
+ # v1.8.3: `never` is the bottom type — compatible with any expected type
148
+ if got == T_NEVER: return True
144
149
  # int can widen to float
145
150
  if expected == T_FLOAT and got == T_INT: return True
146
151
  # null/nil can be assigned to any nullable or union containing nil
@@ -266,7 +271,8 @@ class Analyzer(Visitor):
266
271
  self._in_scene: bool = False
267
272
  self._in_match_arm: bool = False
268
273
  self._struct_defs: Dict[str, StructDecl] = {}
269
- self._type_aliases: Dict[str, "InScriptType"] = {} # v1.8.2: registered aliases
274
+ self._type_aliases: Dict[str, "InScriptType"] = {} # v1.8.2
275
+ self._interfaces: Dict[str, dict] = {} # v1.8.3: name → {method: (params, ret)}
270
276
 
271
277
  # Pre-register built-in global functions
272
278
  self._register_builtins()
@@ -521,8 +527,8 @@ class Analyzer(Visitor):
521
527
 
522
528
  def _hoist_top_level(self, program: Program):
523
529
  """Register structs, scenes, top-level functions, and type aliases before checking bodies."""
524
- from ast_nodes import TypeAliasDecl
525
- # First sub-pass: register type aliases and structs (order-independent)
530
+ from ast_nodes import TypeAliasDecl, InterfaceDecl
531
+ # First sub-pass: register type aliases, structs, and interfaces
526
532
  for node in program.body:
527
533
  if isinstance(node, StructDecl):
528
534
  self._struct_defs[node.name] = node
@@ -532,7 +538,6 @@ class Analyzer(Visitor):
532
538
  line=node.line, col=node.col
533
539
  )
534
540
  elif isinstance(node, TypeAliasDecl):
535
- # v1.8.2: register alias immediately so functions can use it
536
541
  type_ann = getattr(node, 'type_ann', None)
537
542
  if type_ann is not None:
538
543
  resolved = self._resolve_type_ann(type_ann)
@@ -543,6 +548,19 @@ class Analyzer(Visitor):
543
548
  node.name, resolved, kind="type",
544
549
  line=node.line, col=node.col
545
550
  )
551
+ elif isinstance(node, InterfaceDecl):
552
+ # v1.8.3: register interface so structs can reference it
553
+ iface_methods = {}
554
+ for m in getattr(node, 'methods', []):
555
+ ret = self._resolve_type_ann(getattr(m, 'return_type', None))
556
+ params = [self._resolve_type_ann(getattr(p, 'type_ann', None))
557
+ for p in getattr(m, 'params', [])]
558
+ iface_methods[m.name] = (params, ret)
559
+ self._interfaces[node.name] = iface_methods
560
+ self._scope.symbols[node.name] = Symbol(
561
+ node.name, T_ANY, kind="interface",
562
+ line=node.line, col=node.col
563
+ )
546
564
  # Second sub-pass: hoist functions, scenes, enums (can now reference aliases)
547
565
  for node in program.body:
548
566
  if isinstance(node, FunctionDecl):
@@ -617,6 +635,17 @@ class Analyzer(Visitor):
617
635
  def visit_FunctionDecl(self, node: FunctionDecl) -> InScriptType:
618
636
  ret_type = self._resolve_type_ann(node.return_type)
619
637
 
638
+ # v1.8.4: return type inference — if no annotation and body has a single
639
+ # unambiguous return type, infer it automatically
640
+ if node.return_type is None and ret_type == T_ANY and node.body:
641
+ inferred = self._infer_fn_return_type(node)
642
+ if inferred not in (T_ANY, T_VOID, T_NULL):
643
+ ret_type = inferred
644
+ # Update the hoisted symbol's type to the inferred one
645
+ existing = self._scope.lookup(node.name)
646
+ if existing and existing.kind == "fn":
647
+ existing.type_ = ret_type
648
+
620
649
  # Register in current scope (if not already hoisted)
621
650
  if not self._scope.lookup_local(node.name):
622
651
  self._define(Symbol(
@@ -637,12 +666,20 @@ class Analyzer(Visitor):
637
666
  ret_type.name not in ("void", "nil", "any", "") and
638
667
  not node.is_native if hasattr(node, 'is_native') else True):
639
668
  if node.body and not self._body_always_returns(node.body):
640
- self._warn(
641
- "missing-return",
642
- f"Function '{node.name}' declares return type '{ret_type.name}' "
643
- f"but not all code paths return a value",
644
- node.line
645
- )
669
+ if ret_type == T_NEVER:
670
+ # v1.8.3: `-> never` functions MUST always throw / diverge
671
+ self._error(
672
+ f"Function '{node.name}' is declared '-> never' "
673
+ f"but not all code paths throw or diverge",
674
+ node.line
675
+ )
676
+ else:
677
+ self._warn(
678
+ "missing-return",
679
+ f"Function '{node.name}' declares return type '{ret_type.name}' "
680
+ f"but not all code paths return a value",
681
+ node.line
682
+ )
646
683
 
647
684
  # Analyze body in a new scope
648
685
  self._push_scope("fn")
@@ -713,6 +750,26 @@ class Analyzer(Visitor):
713
750
  for method in node.methods:
714
751
  self.visit_FunctionDecl(method)
715
752
 
753
+ # v1.8.3: interface enforcement — check every declared interface is satisfied
754
+ struct_method_names = {m.name for m in node.methods}
755
+ # Also include methods from static_methods
756
+ struct_method_names |= {m.name for m in (node.static_methods or [])}
757
+ for iface_name in (node.interfaces or []):
758
+ required = self._interfaces.get(iface_name)
759
+ if required is None:
760
+ self._error(
761
+ f"Struct '{node.name}' implements unknown interface '{iface_name}'",
762
+ node.line
763
+ )
764
+ continue
765
+ missing_methods = sorted(required.keys() - struct_method_names)
766
+ if missing_methods:
767
+ self._error(
768
+ f"Struct '{node.name}' implements '{iface_name}' "
769
+ f"but is missing method(s): {missing_methods}",
770
+ node.line
771
+ )
772
+
716
773
  self._pop_scope()
717
774
  return struct_type
718
775
 
@@ -757,6 +814,21 @@ class Analyzer(Visitor):
757
814
  )
758
815
  return enum_type
759
816
 
817
+ def visit_InterfaceDecl(self, node) -> InScriptType:
818
+ """v1.8.3: Register interface method signatures for implementation checking."""
819
+ iface_methods = {}
820
+ for m in getattr(node, 'methods', []):
821
+ ret = self._resolve_type_ann(getattr(m, 'return_type', None))
822
+ params = [self._resolve_type_ann(getattr(p, 'type_ann', None))
823
+ for p in getattr(m, 'params', [])]
824
+ iface_methods[m.name] = (params, ret)
825
+ self._interfaces[node.name] = iface_methods
826
+ self._scope.symbols[node.name] = Symbol(
827
+ node.name, T_ANY, kind="interface",
828
+ line=node.line, col=node.col
829
+ )
830
+ return T_VOID
831
+
760
832
  def visit_TypeAliasDecl(self, node) -> InScriptType:
761
833
  """v1.8.2: Register a type alias so annotations can reference it by name."""
762
834
  if hasattr(node, 'type_ann') and node.type_ann is not None:
@@ -965,47 +1037,56 @@ class Analyzer(Visitor):
965
1037
 
966
1038
  def _check_match_exhaustiveness(self, node: MatchStmt) -> None:
967
1039
  """
968
- If the match subject is a typed variable whose type is a known enum,
969
- warn when not all variants are covered and there is no wildcard arm.
1040
+ v1.8.3: Enum exhaustiveness is now a SemanticError (not a warning).
1041
+ If the match subject is a variable typed as a known enum and no wildcard
1042
+ is present, every variant must be explicitly covered.
970
1043
  """
971
- subject = node.subject
1044
+ subject = node.subject
972
1045
  enum_name = None
973
1046
 
974
1047
  if isinstance(subject, IdentExpr):
975
1048
  sym = self._scope.lookup(subject.name)
976
- if sym and sym.type_ and sym.type_.name in self._scope.symbols:
977
- candidate = self._scope.symbols[sym.type_.name]
978
- if candidate.kind == "enum":
979
- enum_name = sym.type_.name
1049
+ if sym and sym.type_:
1050
+ t = sym.type_
1051
+ # Resolve via the full scope chain, not just current scope
1052
+ cand = self._scope.lookup(t.name)
1053
+ if cand and cand.kind == "enum":
1054
+ enum_name = t.name
980
1055
 
981
1056
  if enum_name is None:
982
- return # can't determine enum — skip
1057
+ return # subject is not a typed enum variable — skip
983
1058
 
984
- enum_sym = self._scope.symbols.get(enum_name)
1059
+ enum_sym = self._scope.lookup(enum_name)
985
1060
  if not enum_sym or enum_sym.fn_node is None:
986
1061
  return
987
- enum_decl = enum_sym.fn_node
1062
+ enum_decl = enum_sym.fn_node
988
1063
  all_variants = {v.name for v in getattr(enum_decl, 'variants', [])}
989
1064
  if not all_variants:
990
1065
  return
991
1066
 
992
- # Collect explicitly covered variants: case EnumName.Variant
1067
+ # Collect explicitly covered variants
1068
+ # `case Dir::Left` → NamespaceAccessExpr(namespace="Dir", member="Left")
1069
+ # `case Dir.Left` → GetAttrExpr(obj=IdentExpr("Dir"), attr="Left") (legacy)
993
1070
  covered = set()
994
1071
  for arm in node.arms:
995
1072
  p = arm.pattern
996
1073
  if p is None:
997
- return # wildcard — exhaustive
998
- if (isinstance(p, GetAttrExpr)
1074
+ return # wildcard — exhaustive by definition
1075
+ # Primary form: Dir::Variant
1076
+ if (hasattr(p, 'namespace') and hasattr(p, 'member')
1077
+ and p.namespace == enum_name):
1078
+ covered.add(p.member)
1079
+ # Legacy form: Dir.Variant
1080
+ elif (isinstance(p, GetAttrExpr)
999
1081
  and isinstance(p.obj, IdentExpr)
1000
1082
  and p.obj.name == enum_name):
1001
1083
  covered.add(p.attr)
1002
1084
 
1003
1085
  missing = sorted(all_variants - covered)
1004
1086
  if missing:
1005
- self._warn(
1006
- "exhaustive_match",
1087
+ self._error(
1007
1088
  f"Non-exhaustive match on '{enum_name}': "
1008
- f"variants {missing} not covered. Add missing cases or a wildcard 'case _'",
1089
+ f"missing variant(s) {missing}. Add the missing cases or a wildcard arm 'case _'",
1009
1090
  node.line
1010
1091
  )
1011
1092
 
@@ -1220,8 +1301,288 @@ class Analyzer(Visitor):
1220
1301
  return sym.type_
1221
1302
 
1222
1303
  if isinstance(node.callee, GetAttrExpr):
1304
+ # v1.8.4: infer return types of array method chains
1305
+ obj_type = self.visit(node.callee.obj)
1306
+ # v1.8.2 literal_type("hello") → dispatch as T_STRING
1307
+ if is_literal_type(obj_type):
1308
+ obj_type = T_STRING
1309
+ method = node.callee.attr
1310
+ return self._infer_method_call_type(obj_type, method, node)
1311
+
1312
+ return T_ANY
1313
+
1314
+ def _infer_method_call_type(self, obj_type: InScriptType,
1315
+ method: str, call_node) -> InScriptType:
1316
+ """
1317
+ v1.8.4: Infer the return type of a method call based on the receiver's type.
1318
+
1319
+ Array methods:
1320
+ .map(fn) → Array<T> where T = inferred return type of fn
1321
+ .filter(fn) → Array<elem> (same element type, just fewer items)
1322
+ .flatMap(fn) → Array<T>
1323
+ .take(n) → Array<elem>
1324
+ .skip(n) → Array<elem>
1325
+ .slice(a,b) → Array<elem>
1326
+ .sorted() → Array<elem>
1327
+ .reversed() → Array<elem>
1328
+ .unique() → Array<elem>
1329
+ .len() → int
1330
+ .count() → int
1331
+ .sum() → int | float
1332
+ .first() → elem? (optional)
1333
+ .last() → elem?
1334
+ .find(fn) → elem?
1335
+ .any(fn) → bool
1336
+ .all(fn) → bool
1337
+ .reduce(fn,i) → any (can't infer accumulator type without annotation)
1338
+ .join(sep) → string
1339
+ .push(v) → void
1340
+ .pop() → elem?
1341
+ .contains(v) → bool
1342
+ .index_of(v) → int?
1343
+
1344
+ String methods:
1345
+ .split(sep) → Array<string>
1346
+ .trim() → string
1347
+ .upper() → string
1348
+ .lower() → string
1349
+ .replace(...) → string
1350
+ .starts_with()→ bool
1351
+ .ends_with() → bool
1352
+ .contains() → bool
1353
+ .len() → int
1354
+ .chars() → Array<string>
1355
+ """
1356
+ # ── Array method inference ────────────────────────────────────────────
1357
+ if obj_type.name == "Array":
1358
+ elem = obj_type.params[0] if obj_type.params else T_ANY
1359
+
1360
+ # Methods that return Array<same-elem>
1361
+ if method in ("filter", "take", "skip", "slice", "sorted",
1362
+ "reversed", "unique", "shuffle", "concat"):
1363
+ return array_type(elem)
1364
+
1365
+ # .map(fn) / .flatMap(fn) → infer fn return type
1366
+ if method in ("map", "flatMap"):
1367
+ fn_ret = self._infer_fn_arg_return(call_node, arg_index=0, param_type=elem)
1368
+ inner = fn_ret if fn_ret != T_ANY else T_ANY
1369
+ if method == "flatMap" and inner.name == "Array":
1370
+ inner = inner.params[0] if inner.params else T_ANY
1371
+ return array_type(inner)
1372
+
1373
+ # Scalar reductions
1374
+ if method in ("len", "count", "index_of"):
1375
+ return T_INT
1376
+ if method == "sum":
1377
+ return elem if elem in (T_INT, T_FLOAT) else T_INT
1378
+ if method in ("any", "all", "contains", "is_empty"):
1379
+ return T_BOOL
1380
+ if method == "join":
1381
+ return T_STRING
1382
+ if method in ("push", "append", "extend", "clear", "insert",
1383
+ "remove", "remove_at"):
1384
+ return T_VOID
1385
+
1386
+ # Optional-returning methods
1387
+ if method in ("first", "last", "pop", "find", "get"):
1388
+ return optional_type(elem)
1389
+
1390
+ # .reduce → T_ANY (accumulator type unknown without annotation)
1391
+ if method == "reduce":
1392
+ return T_ANY
1393
+
1394
+ # .each(fn) → void (side-effect traversal)
1395
+ if method in ("each", "for_each"):
1396
+ return T_VOID
1397
+
1398
+ # ── String method inference ───────────────────────────────────────────
1399
+ if obj_type == T_STRING:
1400
+ if method in ("trim", "upper", "lower", "replace", "strip",
1401
+ "lstrip", "rstrip", "pad_left", "pad_right",
1402
+ "repeat", "reverse", "slice"):
1403
+ return T_STRING
1404
+ if method in ("len", "count", "index_of", "find"):
1405
+ return T_INT
1406
+ if method in ("starts_with", "ends_with", "contains", "is_empty",
1407
+ "matches"):
1408
+ return T_BOOL
1409
+ if method == "split":
1410
+ return array_type(T_STRING)
1411
+ if method == "chars":
1412
+ return array_type(T_STRING)
1413
+ if method == "to_int":
1414
+ return optional_type(T_INT)
1415
+ if method == "to_float":
1416
+ return optional_type(T_FLOAT)
1417
+
1418
+ # ── Int / Float methods ───────────────────────────────────────────────
1419
+ if obj_type in (T_INT, T_FLOAT):
1420
+ if method in ("abs", "floor", "ceil", "round", "sqrt",
1421
+ "clamp", "min", "max"):
1422
+ return obj_type
1423
+ if method == "to_string":
1424
+ return T_STRING
1425
+
1426
+ # ── Struct method return type ─────────────────────────────────────────
1427
+ if obj_type.name in self._struct_defs:
1428
+ struct = self._struct_defs[obj_type.name]
1429
+ for m in (struct.methods or []):
1430
+ if m.name == method:
1431
+ return self._resolve_type_ann(m.return_type)
1432
+
1433
+ return T_ANY
1434
+
1435
+ def _infer_fn_return_type(self, node) -> InScriptType:
1436
+ """
1437
+ v1.8.4: Infer a function's return type from its body when no annotation exists.
1438
+ Only infers when ALL return paths agree on the same concrete type.
1439
+ Falls back to T_ANY if ambiguous.
1440
+ NOTE: errors are suppressed — inference is best-effort, never raises.
1441
+ """
1442
+ from ast_nodes import ReturnStmt, BlockStmt
1443
+
1444
+ if not node.body:
1445
+ return T_VOID
1446
+
1447
+ # Stash and suppress the real error list — inference must never pollute it
1448
+ saved_errors = self._errors
1449
+ self._errors = []
1450
+
1451
+ try:
1452
+ # Push a temporary scope with params typed per their annotations
1453
+ self._push_scope("infer-fn")
1454
+ for p in (node.params or []):
1455
+ p_type = self._resolve_type_ann(getattr(p, 'type_ann', None))
1456
+ pname = getattr(p, 'name', None)
1457
+ if pname:
1458
+ self._scope.symbols[pname] = Symbol(
1459
+ pname, p_type, kind="var",
1460
+ line=getattr(p, 'line', 0), col=getattr(p, 'col', 0)
1461
+ )
1462
+ self._scope.symbols[pname].used = True
1463
+
1464
+ # Collect return types from first-level stmts + if/else branches
1465
+ types_seen = set()
1466
+ stmts = node.body.body if isinstance(node.body, BlockStmt) else [node.body]
1467
+ for stmt in stmts:
1468
+ if isinstance(stmt, ReturnStmt) and stmt.value is not None:
1469
+ try:
1470
+ t = self.visit(stmt.value)
1471
+ if is_literal_type(t): t = T_STRING
1472
+ types_seen.add(t)
1473
+ except Exception:
1474
+ types_seen.add(T_ANY)
1475
+ for branch_attr in ('then_branch', 'else_branch'):
1476
+ branch = getattr(stmt, branch_attr, None)
1477
+ if branch is None:
1478
+ continue
1479
+ inner = getattr(branch, 'body', [branch])
1480
+ if not isinstance(inner, list):
1481
+ inner = [inner]
1482
+ for s in inner:
1483
+ if isinstance(s, ReturnStmt) and s.value is not None:
1484
+ try:
1485
+ t = self.visit(s.value)
1486
+ if is_literal_type(t): t = T_STRING
1487
+ types_seen.add(t)
1488
+ except Exception:
1489
+ types_seen.add(T_ANY)
1490
+
1491
+ self._pop_scope()
1492
+
1493
+ if not types_seen:
1494
+ return T_VOID
1495
+ non_any = {t for t in types_seen if t != T_ANY}
1496
+ if len(non_any) == 1:
1497
+ return next(iter(non_any))
1498
+ if non_any == {T_INT, T_FLOAT}:
1499
+ return T_FLOAT
1223
1500
  return T_ANY
1224
1501
 
1502
+ except Exception:
1503
+ return T_ANY
1504
+ finally:
1505
+ # Always restore the real error list
1506
+ self._errors = saved_errors
1507
+
1508
+ def _infer_fn_arg_return(self, call_node, arg_index: int,
1509
+ param_type: InScriptType) -> InScriptType:
1510
+ """
1511
+ v1.8.4: Given a CallExpr and an argument position that expects a fn,
1512
+ try to infer that fn's return type by visiting its body with a temporary
1513
+ scope that binds the param to param_type.
1514
+ Returns T_ANY if inference fails.
1515
+ """
1516
+ from ast_nodes import FunctionDecl, LambdaExpr
1517
+ if arg_index >= len(call_node.args):
1518
+ return T_ANY
1519
+ fn_arg = call_node.args[arg_index].value
1520
+
1521
+ # Handle both lambda `fn(x) { return x * 2 }` and named refs
1522
+ params = None; body = None
1523
+ if isinstance(fn_arg, FunctionDecl) or isinstance(fn_arg, LambdaExpr):
1524
+ params = getattr(fn_arg, 'params', [])
1525
+ body = getattr(fn_arg, 'body', None)
1526
+ else:
1527
+ return T_ANY
1528
+
1529
+ # Stash and suppress the real error list
1530
+ saved_errors = self._errors
1531
+ self._errors = []
1532
+
1533
+ try:
1534
+ # Push a temporary scope, bind first param to elem type
1535
+ self._push_scope("infer")
1536
+ if params:
1537
+ p0 = params[0]
1538
+ p_name = getattr(p0, 'name', None)
1539
+ if p_name:
1540
+ self._scope.symbols[p_name] = Symbol(
1541
+ p_name, param_type, kind="var",
1542
+ line=getattr(p0, 'line', 0), col=getattr(p0, 'col', 0)
1543
+ )
1544
+ self._scope.symbols[p_name].used = True
1545
+
1546
+ inferred = T_ANY
1547
+ if body is not None:
1548
+ inferred = self._infer_block_return_type(body)
1549
+
1550
+ self._pop_scope()
1551
+ return inferred
1552
+ except Exception:
1553
+ return T_ANY
1554
+ finally:
1555
+ self._errors = saved_errors
1556
+
1557
+ def _infer_block_return_type(self, block) -> InScriptType:
1558
+ """
1559
+ v1.8.4: Walk a block/expression to find the first return statement's type.
1560
+ Also handles single-expression lambdas.
1561
+ """
1562
+ from ast_nodes import BlockStmt, ReturnStmt
1563
+ stmts = []
1564
+ if isinstance(block, BlockStmt):
1565
+ stmts = block.body
1566
+ elif hasattr(block, '__iter__'):
1567
+ stmts = list(block)
1568
+ else:
1569
+ # Single expression (arrow lambda)
1570
+ try:
1571
+ return self.visit(block)
1572
+ except Exception:
1573
+ return T_ANY
1574
+
1575
+ for stmt in stmts:
1576
+ if isinstance(stmt, ReturnStmt) and stmt.value is not None:
1577
+ try:
1578
+ return self.visit(stmt.value)
1579
+ except Exception:
1580
+ return T_ANY
1581
+ # Recurse into if/while bodies for early returns
1582
+ if hasattr(stmt, 'then_branch'):
1583
+ t = self._infer_block_return_type(stmt.then_branch)
1584
+ if t != T_ANY:
1585
+ return t
1225
1586
  return T_ANY
1226
1587
 
1227
1588
  def visit_NamespaceAccessExpr(self, node: NamespaceAccessExpr) -> InScriptType:
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "1.8.2"
27
+ VERSION = "1.8.4"
28
28
  LANG = "InScript"
29
29
  PACKAGES_DIR = os.path.join(os.path.expanduser("~"), ".inscript", "packages")
30
30
  REGISTRY_URL = "https://raw.githubusercontent.com/authorss81/inscript-packages/main/registry.json"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 1.8.2
3
+ Version: 1.8.4
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript-lang"
7
- version = "1.8.2"
7
+ version = "1.8.4"
8
8
  description = "InScript — a game-focused scripting language with 59 game modules and a bytecode VM"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -40,7 +40,7 @@ sys.path.insert(0, str(Path(__file__).parent))
40
40
 
41
41
  HISTORY_FILE = Path.home() / ".inscript" / "history"
42
42
  HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
43
- VERSION = "1.8.2"
43
+ VERSION = "1.8.4"
44
44
 
45
45
  # ── ANSI colours ──────────────────────────────────────────────────────────────
46
46
  def _c(code, text):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes