inscript-lang 1.8.3__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.3 → inscript_lang-1.8.4}/PKG-INFO +1 -1
  2. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/analyzer.py +291 -0
  3. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript.py +1 -1
  4. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/pyproject.toml +1 -1
  6. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/repl.py +1 -1
  7. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/README.md +0 -0
  8. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/ast_nodes.py +0 -0
  9. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/compiler.py +0 -0
  10. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/environment.py +0 -0
  11. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/errors.py +0 -0
  12. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_fmt.py +0 -0
  13. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_lang.egg-info/SOURCES.txt +0 -0
  14. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_lang.egg-info/dependency_links.txt +0 -0
  15. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_lang.egg-info/entry_points.txt +0 -0
  16. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_lang.egg-info/requires.txt +0 -0
  17. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_lang.egg-info/top_level.txt +0 -0
  18. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/inscript_test.py +0 -0
  19. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/interpreter.py +0 -0
  20. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/lexer.py +0 -0
  21. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/parser.py +0 -0
  22. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/pygame_backend.py +0 -0
  23. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/setup.cfg +0 -0
  24. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/setup.py +0 -0
  25. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/stdlib.py +0 -0
  26. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/stdlib_extended.py +0 -0
  27. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/stdlib_game.py +0 -0
  29. {inscript_lang-1.8.3 → inscript_lang-1.8.4}/stdlib_values.py +0 -0
  30. {inscript_lang-1.8.3 → 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.3
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
@@ -635,6 +635,17 @@ class Analyzer(Visitor):
635
635
  def visit_FunctionDecl(self, node: FunctionDecl) -> InScriptType:
636
636
  ret_type = self._resolve_type_ann(node.return_type)
637
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
+
638
649
  # Register in current scope (if not already hoisted)
639
650
  if not self._scope.lookup_local(node.name):
640
651
  self._define(Symbol(
@@ -1290,8 +1301,288 @@ class Analyzer(Visitor):
1290
1301
  return sym.type_
1291
1302
 
1292
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
1500
+ return T_ANY
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:
1293
1527
  return T_ANY
1294
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
1295
1586
  return T_ANY
1296
1587
 
1297
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.3"
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.3
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.3"
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.3"
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