inscript-lang 1.8.3__tar.gz → 1.9.2__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.9.2}/PKG-INFO +1 -1
  2. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/analyzer.py +305 -2
  3. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript.py +459 -4
  4. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/pyproject.toml +1 -1
  6. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/repl.py +1 -1
  7. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/README.md +0 -0
  8. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/ast_nodes.py +0 -0
  9. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/compiler.py +0 -0
  10. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/environment.py +0 -0
  11. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/errors.py +0 -0
  12. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_fmt.py +0 -0
  13. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/SOURCES.txt +0 -0
  14. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/dependency_links.txt +0 -0
  15. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/entry_points.txt +0 -0
  16. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/requires.txt +0 -0
  17. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/top_level.txt +0 -0
  18. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_test.py +0 -0
  19. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/interpreter.py +0 -0
  20. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/lexer.py +0 -0
  21. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/parser.py +0 -0
  22. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/pygame_backend.py +0 -0
  23. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/setup.cfg +0 -0
  24. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/setup.py +0 -0
  25. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib.py +0 -0
  26. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_extended.py +0 -0
  27. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_game.py +0 -0
  29. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_values.py +0 -0
  30. {inscript_lang-1.8.3 → inscript_lang-1.9.2}/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.9.2
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
@@ -82,6 +82,8 @@ BUILTIN_TYPES: Dict[str, InScriptType] = {
82
82
  "Texture": T_TEXTURE,
83
83
  # tuple / array shorthand
84
84
  "Tuple": T_ANY, "List": T_ANY, "Dict": T_ANY, "Map": T_ANY,
85
+ # v1.9.1: bare 'array' without element type (deprecated — use [T] in v2.0.0)
86
+ "array": InScriptType("Array", [T_ANY]),
85
87
  }
86
88
 
87
89
  def array_type(elem: InScriptType) -> InScriptType:
@@ -254,7 +256,8 @@ class Analyzer(Visitor):
254
256
  multi_error: bool = True,
255
257
  warn_as_error: bool = False,
256
258
  no_warn: bool = False,
257
- no_warn_unused: bool = False):
259
+ no_warn_unused: bool = False,
260
+ strict: bool = False):
258
261
  self._src = source_lines or []
259
262
  self._scope = Scope(kind="global")
260
263
  self._errors: List[SemanticError] = [] # collected (multi-error)
@@ -263,6 +266,7 @@ class Analyzer(Visitor):
263
266
  self._warn_as_error = warn_as_error
264
267
  self._no_warn = no_warn
265
268
  self._no_warn_unused = no_warn_unused
269
+ self._strict = strict # v1.9.1: enables extra v2.0.0-readiness errors
266
270
  self._dispatch: dict = {} # v1.3.0: cached dispatch
267
271
 
268
272
  # State for context-sensitive checks
@@ -386,7 +390,15 @@ class Analyzer(Visitor):
386
390
  return fn_type(params, ret)
387
391
 
388
392
  if ann.name in BUILTIN_TYPES:
389
- return BUILTIN_TYPES[ann.name]
393
+ t = BUILTIN_TYPES[ann.name]
394
+ # v1.9.1: bare `array` without element type is a hard error in strict mode
395
+ if ann.name == "array" and not ann.generics and self._strict:
396
+ self._error(
397
+ "Bare 'array' type without element type is a breaking change in v2.0.0. "
398
+ "Use '[T]' (e.g. '[int]') or annotate the element type.",
399
+ ann.line, ann.col
400
+ )
401
+ return t
390
402
 
391
403
  # v1.8.2: registered type aliases
392
404
  if ann.name in self._type_aliases:
@@ -635,6 +647,17 @@ class Analyzer(Visitor):
635
647
  def visit_FunctionDecl(self, node: FunctionDecl) -> InScriptType:
636
648
  ret_type = self._resolve_type_ann(node.return_type)
637
649
 
650
+ # v1.8.4: return type inference — if no annotation and body has a single
651
+ # unambiguous return type, infer it automatically
652
+ if node.return_type is None and ret_type == T_ANY and node.body:
653
+ inferred = self._infer_fn_return_type(node)
654
+ if inferred not in (T_ANY, T_VOID, T_NULL):
655
+ ret_type = inferred
656
+ # Update the hoisted symbol's type to the inferred one
657
+ existing = self._scope.lookup(node.name)
658
+ if existing and existing.kind == "fn":
659
+ existing.type_ = ret_type
660
+
638
661
  # Register in current scope (if not already hoisted)
639
662
  if not self._scope.lookup_local(node.name):
640
663
  self._define(Symbol(
@@ -1290,8 +1313,288 @@ class Analyzer(Visitor):
1290
1313
  return sym.type_
1291
1314
 
1292
1315
  if isinstance(node.callee, GetAttrExpr):
1316
+ # v1.8.4: infer return types of array method chains
1317
+ obj_type = self.visit(node.callee.obj)
1318
+ # v1.8.2 literal_type("hello") → dispatch as T_STRING
1319
+ if is_literal_type(obj_type):
1320
+ obj_type = T_STRING
1321
+ method = node.callee.attr
1322
+ return self._infer_method_call_type(obj_type, method, node)
1323
+
1324
+ return T_ANY
1325
+
1326
+ def _infer_method_call_type(self, obj_type: InScriptType,
1327
+ method: str, call_node) -> InScriptType:
1328
+ """
1329
+ v1.8.4: Infer the return type of a method call based on the receiver's type.
1330
+
1331
+ Array methods:
1332
+ .map(fn) → Array<T> where T = inferred return type of fn
1333
+ .filter(fn) → Array<elem> (same element type, just fewer items)
1334
+ .flatMap(fn) → Array<T>
1335
+ .take(n) → Array<elem>
1336
+ .skip(n) → Array<elem>
1337
+ .slice(a,b) → Array<elem>
1338
+ .sorted() → Array<elem>
1339
+ .reversed() → Array<elem>
1340
+ .unique() → Array<elem>
1341
+ .len() → int
1342
+ .count() → int
1343
+ .sum() → int | float
1344
+ .first() → elem? (optional)
1345
+ .last() → elem?
1346
+ .find(fn) → elem?
1347
+ .any(fn) → bool
1348
+ .all(fn) → bool
1349
+ .reduce(fn,i) → any (can't infer accumulator type without annotation)
1350
+ .join(sep) → string
1351
+ .push(v) → void
1352
+ .pop() → elem?
1353
+ .contains(v) → bool
1354
+ .index_of(v) → int?
1355
+
1356
+ String methods:
1357
+ .split(sep) → Array<string>
1358
+ .trim() → string
1359
+ .upper() → string
1360
+ .lower() → string
1361
+ .replace(...) → string
1362
+ .starts_with()→ bool
1363
+ .ends_with() → bool
1364
+ .contains() → bool
1365
+ .len() → int
1366
+ .chars() → Array<string>
1367
+ """
1368
+ # ── Array method inference ────────────────────────────────────────────
1369
+ if obj_type.name == "Array":
1370
+ elem = obj_type.params[0] if obj_type.params else T_ANY
1371
+
1372
+ # Methods that return Array<same-elem>
1373
+ if method in ("filter", "take", "skip", "slice", "sorted",
1374
+ "reversed", "unique", "shuffle", "concat"):
1375
+ return array_type(elem)
1376
+
1377
+ # .map(fn) / .flatMap(fn) → infer fn return type
1378
+ if method in ("map", "flatMap"):
1379
+ fn_ret = self._infer_fn_arg_return(call_node, arg_index=0, param_type=elem)
1380
+ inner = fn_ret if fn_ret != T_ANY else T_ANY
1381
+ if method == "flatMap" and inner.name == "Array":
1382
+ inner = inner.params[0] if inner.params else T_ANY
1383
+ return array_type(inner)
1384
+
1385
+ # Scalar reductions
1386
+ if method in ("len", "count", "index_of"):
1387
+ return T_INT
1388
+ if method == "sum":
1389
+ return elem if elem in (T_INT, T_FLOAT) else T_INT
1390
+ if method in ("any", "all", "contains", "is_empty"):
1391
+ return T_BOOL
1392
+ if method == "join":
1393
+ return T_STRING
1394
+ if method in ("push", "append", "extend", "clear", "insert",
1395
+ "remove", "remove_at"):
1396
+ return T_VOID
1397
+
1398
+ # Optional-returning methods
1399
+ if method in ("first", "last", "pop", "find", "get"):
1400
+ return optional_type(elem)
1401
+
1402
+ # .reduce → T_ANY (accumulator type unknown without annotation)
1403
+ if method == "reduce":
1404
+ return T_ANY
1405
+
1406
+ # .each(fn) → void (side-effect traversal)
1407
+ if method in ("each", "for_each"):
1408
+ return T_VOID
1409
+
1410
+ # ── String method inference ───────────────────────────────────────────
1411
+ if obj_type == T_STRING:
1412
+ if method in ("trim", "upper", "lower", "replace", "strip",
1413
+ "lstrip", "rstrip", "pad_left", "pad_right",
1414
+ "repeat", "reverse", "slice"):
1415
+ return T_STRING
1416
+ if method in ("len", "count", "index_of", "find"):
1417
+ return T_INT
1418
+ if method in ("starts_with", "ends_with", "contains", "is_empty",
1419
+ "matches"):
1420
+ return T_BOOL
1421
+ if method == "split":
1422
+ return array_type(T_STRING)
1423
+ if method == "chars":
1424
+ return array_type(T_STRING)
1425
+ if method == "to_int":
1426
+ return optional_type(T_INT)
1427
+ if method == "to_float":
1428
+ return optional_type(T_FLOAT)
1429
+
1430
+ # ── Int / Float methods ───────────────────────────────────────────────
1431
+ if obj_type in (T_INT, T_FLOAT):
1432
+ if method in ("abs", "floor", "ceil", "round", "sqrt",
1433
+ "clamp", "min", "max"):
1434
+ return obj_type
1435
+ if method == "to_string":
1436
+ return T_STRING
1437
+
1438
+ # ── Struct method return type ─────────────────────────────────────────
1439
+ if obj_type.name in self._struct_defs:
1440
+ struct = self._struct_defs[obj_type.name]
1441
+ for m in (struct.methods or []):
1442
+ if m.name == method:
1443
+ return self._resolve_type_ann(m.return_type)
1444
+
1445
+ return T_ANY
1446
+
1447
+ def _infer_fn_return_type(self, node) -> InScriptType:
1448
+ """
1449
+ v1.8.4: Infer a function's return type from its body when no annotation exists.
1450
+ Only infers when ALL return paths agree on the same concrete type.
1451
+ Falls back to T_ANY if ambiguous.
1452
+ NOTE: errors are suppressed — inference is best-effort, never raises.
1453
+ """
1454
+ from ast_nodes import ReturnStmt, BlockStmt
1455
+
1456
+ if not node.body:
1457
+ return T_VOID
1458
+
1459
+ # Stash and suppress the real error list — inference must never pollute it
1460
+ saved_errors = self._errors
1461
+ self._errors = []
1462
+
1463
+ try:
1464
+ # Push a temporary scope with params typed per their annotations
1465
+ self._push_scope("infer-fn")
1466
+ for p in (node.params or []):
1467
+ p_type = self._resolve_type_ann(getattr(p, 'type_ann', None))
1468
+ pname = getattr(p, 'name', None)
1469
+ if pname:
1470
+ self._scope.symbols[pname] = Symbol(
1471
+ pname, p_type, kind="var",
1472
+ line=getattr(p, 'line', 0), col=getattr(p, 'col', 0)
1473
+ )
1474
+ self._scope.symbols[pname].used = True
1475
+
1476
+ # Collect return types from first-level stmts + if/else branches
1477
+ types_seen = set()
1478
+ stmts = node.body.body if isinstance(node.body, BlockStmt) else [node.body]
1479
+ for stmt in stmts:
1480
+ if isinstance(stmt, ReturnStmt) and stmt.value is not None:
1481
+ try:
1482
+ t = self.visit(stmt.value)
1483
+ if is_literal_type(t): t = T_STRING
1484
+ types_seen.add(t)
1485
+ except Exception:
1486
+ types_seen.add(T_ANY)
1487
+ for branch_attr in ('then_branch', 'else_branch'):
1488
+ branch = getattr(stmt, branch_attr, None)
1489
+ if branch is None:
1490
+ continue
1491
+ inner = getattr(branch, 'body', [branch])
1492
+ if not isinstance(inner, list):
1493
+ inner = [inner]
1494
+ for s in inner:
1495
+ if isinstance(s, ReturnStmt) and s.value is not None:
1496
+ try:
1497
+ t = self.visit(s.value)
1498
+ if is_literal_type(t): t = T_STRING
1499
+ types_seen.add(t)
1500
+ except Exception:
1501
+ types_seen.add(T_ANY)
1502
+
1503
+ self._pop_scope()
1504
+
1505
+ if not types_seen:
1506
+ return T_VOID
1507
+ non_any = {t for t in types_seen if t != T_ANY}
1508
+ if len(non_any) == 1:
1509
+ return next(iter(non_any))
1510
+ if non_any == {T_INT, T_FLOAT}:
1511
+ return T_FLOAT
1512
+ return T_ANY
1513
+
1514
+ except Exception:
1515
+ return T_ANY
1516
+ finally:
1517
+ # Always restore the real error list
1518
+ self._errors = saved_errors
1519
+
1520
+ def _infer_fn_arg_return(self, call_node, arg_index: int,
1521
+ param_type: InScriptType) -> InScriptType:
1522
+ """
1523
+ v1.8.4: Given a CallExpr and an argument position that expects a fn,
1524
+ try to infer that fn's return type by visiting its body with a temporary
1525
+ scope that binds the param to param_type.
1526
+ Returns T_ANY if inference fails.
1527
+ """
1528
+ from ast_nodes import FunctionDecl, LambdaExpr
1529
+ if arg_index >= len(call_node.args):
1530
+ return T_ANY
1531
+ fn_arg = call_node.args[arg_index].value
1532
+
1533
+ # Handle both lambda `fn(x) { return x * 2 }` and named refs
1534
+ params = None; body = None
1535
+ if isinstance(fn_arg, FunctionDecl) or isinstance(fn_arg, LambdaExpr):
1536
+ params = getattr(fn_arg, 'params', [])
1537
+ body = getattr(fn_arg, 'body', None)
1538
+ else:
1293
1539
  return T_ANY
1294
1540
 
1541
+ # Stash and suppress the real error list
1542
+ saved_errors = self._errors
1543
+ self._errors = []
1544
+
1545
+ try:
1546
+ # Push a temporary scope, bind first param to elem type
1547
+ self._push_scope("infer")
1548
+ if params:
1549
+ p0 = params[0]
1550
+ p_name = getattr(p0, 'name', None)
1551
+ if p_name:
1552
+ self._scope.symbols[p_name] = Symbol(
1553
+ p_name, param_type, kind="var",
1554
+ line=getattr(p0, 'line', 0), col=getattr(p0, 'col', 0)
1555
+ )
1556
+ self._scope.symbols[p_name].used = True
1557
+
1558
+ inferred = T_ANY
1559
+ if body is not None:
1560
+ inferred = self._infer_block_return_type(body)
1561
+
1562
+ self._pop_scope()
1563
+ return inferred
1564
+ except Exception:
1565
+ return T_ANY
1566
+ finally:
1567
+ self._errors = saved_errors
1568
+
1569
+ def _infer_block_return_type(self, block) -> InScriptType:
1570
+ """
1571
+ v1.8.4: Walk a block/expression to find the first return statement's type.
1572
+ Also handles single-expression lambdas.
1573
+ """
1574
+ from ast_nodes import BlockStmt, ReturnStmt
1575
+ stmts = []
1576
+ if isinstance(block, BlockStmt):
1577
+ stmts = block.body
1578
+ elif hasattr(block, '__iter__'):
1579
+ stmts = list(block)
1580
+ else:
1581
+ # Single expression (arrow lambda)
1582
+ try:
1583
+ return self.visit(block)
1584
+ except Exception:
1585
+ return T_ANY
1586
+
1587
+ for stmt in stmts:
1588
+ if isinstance(stmt, ReturnStmt) and stmt.value is not None:
1589
+ try:
1590
+ return self.visit(stmt.value)
1591
+ except Exception:
1592
+ return T_ANY
1593
+ # Recurse into if/while bodies for early returns
1594
+ if hasattr(stmt, 'then_branch'):
1595
+ t = self._infer_block_return_type(stmt.then_branch)
1596
+ if t != T_ANY:
1597
+ return t
1295
1598
  return T_ANY
1296
1599
 
1297
1600
  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.9.2"
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"
@@ -245,6 +245,75 @@ def _check_all_files(directory: str, strict: bool = False) -> int:
245
245
  return 1 if errors else 0
246
246
 
247
247
 
248
+
249
+ def _compat_files(path: str) -> int:
250
+ """
251
+ v1.9.1: `inscript compat FILE|DIR` — report every v2.0.0 breaking change.
252
+ Returns 0 if clean, 1 if issues found.
253
+ """
254
+ import re as _re
255
+ from collections import defaultdict
256
+
257
+ CHECKS = [
258
+ (_re.compile(r'\bnull\b'),
259
+ "use of 'null' — removed in v2.0.0, use 'nil'"),
260
+ (_re.compile(r'\bdiv\b'),
261
+ "use of 'div' operator — removed in v2.0.0, use '//' for floor division"),
262
+ (_re.compile(r':\s*\[\]'),
263
+ "bare ':[]' type annotation — use ':array' or ':[T]' with element type"),
264
+ (_re.compile(r'\barray\b(?!\s*<)(?!\s*\[)'),
265
+ "bare 'array' type — use '[T]' with explicit element type in v2.0.0"),
266
+ (_re.compile(r'--no-typecheck'),
267
+ "'--no-typecheck' flag removed — use '--unsafe-no-check' in v2.0.0"),
268
+ ]
269
+
270
+ def _check_file(fpath):
271
+ issues = []
272
+ try:
273
+ with open(fpath, encoding="utf-8") as f:
274
+ src = f.read()
275
+ except OSError as e:
276
+ print(f"[InScript compat] Cannot read '{fpath}': {e}", file=sys.stderr)
277
+ return issues
278
+ for lineno, line in enumerate(src.splitlines(), 1):
279
+ if line.lstrip().startswith("//"):
280
+ continue
281
+ for pat, msg in CHECKS:
282
+ if pat.search(line):
283
+ issues.append((fpath, lineno, line.rstrip(), msg))
284
+ return issues
285
+
286
+ all_issues = []
287
+ if os.path.isfile(path):
288
+ all_issues = _check_file(path)
289
+ elif os.path.isdir(path):
290
+ for root, _dirs, files in os.walk(path):
291
+ for fname in sorted(files):
292
+ if fname.endswith(".ins"):
293
+ all_issues.extend(_check_file(os.path.join(root, fname)))
294
+ else:
295
+ print(f"[InScript compat] Not found: '{path}'", file=sys.stderr)
296
+ return 1
297
+
298
+ if not all_issues:
299
+ print(f"[InScript compat] No v2.0.0 breaking changes found in '{path}'")
300
+ return 0
301
+
302
+ by_file = defaultdict(list)
303
+ for fp, ln, line, msg in all_issues:
304
+ by_file[fp].append((ln, line, msg))
305
+
306
+ print(f"[InScript compat] Found {len(all_issues)} breaking change(s) for v2.0.0:\n")
307
+ for fp, entries in sorted(by_file.items()):
308
+ print(f" {fp}")
309
+ for ln, line, msg in entries:
310
+ print(f" Line {ln}: {msg}")
311
+ print(f" {line.strip()}")
312
+ print()
313
+ print(f"Run: inscript migrate {path} to auto-fix null/div issues.")
314
+ return 1
315
+
316
+
248
317
  def _migrate_files(path: str) -> int:
249
318
  """v1.7.4: Auto-migrate deprecated InScript syntax in-place."""
250
319
  import re, os
@@ -371,6 +440,7 @@ def run_source(source: str, filename: str = "<stdin>",
371
440
  no_warn: bool = False,
372
441
  no_warn_unused: bool = False,
373
442
  warn_as_error: bool = False,
443
+ strict: bool = False,
374
444
  profile: bool = False) -> int:
375
445
  """Lex, parse, analyze, and interpret InScript source code."""
376
446
  # ── 1. Lex ────────────────────────────────────────────────────────────
@@ -396,6 +466,7 @@ def run_source(source: str, filename: str = "<stdin>",
396
466
  warn_as_error=warn_as_error,
397
467
  no_warn=no_warn,
398
468
  no_warn_unused=no_warn_unused,
469
+ strict=strict, # v1.9.1
399
470
  )
400
471
  try:
401
472
  _analyzer.analyze(program)
@@ -615,12 +686,22 @@ Examples:
615
686
  help="v1.6.0: Check all .ins files in DIR recursively, exit 1 if any errors")
616
687
  parser.add_argument("--migrate", metavar="DIR_OR_FILE",
617
688
  help="v1.7.4: Auto-migrate deprecated syntax (null→nil, div→//)")
689
+ parser.add_argument("--compat", metavar="DIR_OR_FILE",
690
+ help="v1.9.1: Report v2.0.0 breaking changes in FILE or DIR")
691
+ parser.add_argument("--init", metavar="DIR", nargs="?", const=".",
692
+ help="v1.9.2: Create inscript.toml in DIR (default: current dir)")
693
+ parser.add_argument("--validate", metavar="DIR_OR_FILE", nargs="?", const=".",
694
+ help="v1.9.2: Validate inscript.toml (default: current dir)")
695
+ parser.add_argument("--lock", metavar="DIR", nargs="?", const=".",
696
+ help="v1.9.2: Generate inscript.lock from inscript.toml")
618
697
  parser.add_argument("--strict", action="store_true",
619
698
  help="v1.6.0: Strict mode — all warnings become errors, no implicit any")
620
699
  parser.add_argument("--tokens", action="store_true", help="Print lexer token stream")
621
700
  parser.add_argument("--ast", action="store_true", help="Print parsed AST")
622
701
  parser.add_argument("--no-typecheck", action="store_true",
623
- help="Skip semantic analysis (faster, less safe)")
702
+ help="[DEPRECATED v1.9.1] Use --unsafe-no-check instead")
703
+ parser.add_argument("--unsafe-no-check", action="store_true",
704
+ help="v1.9.1: Skip semantic analysis (replaces --no-typecheck)")
624
705
  parser.add_argument("--no-warn", action="store_true",
625
706
  help="Suppress all warnings")
626
707
  parser.add_argument("--no-warn-unused", action="store_true",
@@ -770,6 +851,23 @@ Examples:
770
851
  return _fmt_all_files(args.fmt_all)
771
852
  if getattr(args, 'migrate', None):
772
853
  return _migrate_files(args.migrate)
854
+ if getattr(args, 'compat', None):
855
+ return _compat_files(args.compat)
856
+ if getattr(args, 'init', None) is not None:
857
+ return _init_manifest(args.init)
858
+ if getattr(args, 'validate', None) is not None:
859
+ return _validate_manifest(args.validate)
860
+ if getattr(args, 'lock', None) is not None:
861
+ return _generate_lockfile(args.lock)
862
+
863
+ # v1.9.1: --no-typecheck is deprecated — emit a warning and honour it
864
+ if getattr(args, 'no_typecheck', False):
865
+ print(
866
+ "[InScript] Warning: --no-typecheck is deprecated in v1.9.1 "
867
+ "and will be removed in v2.0.0. Use --unsafe-no-check instead.",
868
+ file=sys.stderr
869
+ )
870
+ args.unsafe_no_check = True
773
871
 
774
872
  if not args.file:
775
873
  parser.print_help()
@@ -834,7 +932,8 @@ Examples:
834
932
  return 0
835
933
 
836
934
  # Normal run
837
- type_check = not args.no_typecheck
935
+ type_check = not getattr(args, 'no_typecheck', False) and \
936
+ not getattr(args, 'unsafe_no_check', False)
838
937
  no_warn = getattr(args, "no_warn", False)
839
938
  no_warn_unused= getattr(args, "no_warn_unused", False)
840
939
  strict = getattr(args, "strict", False)
@@ -845,8 +944,364 @@ Examples:
845
944
  _source = _f.read()
846
945
  return run_source(_source, filename=args.file, type_check=type_check,
847
946
  no_warn=no_warn, no_warn_unused=no_warn_unused,
848
- warn_as_error=warn_as_error, profile=profile)
947
+ warn_as_error=warn_as_error, strict=strict,
948
+ profile=profile)
849
949
 
850
950
 
851
951
  if __name__ == "__main__":
852
952
  sys.exit(main())
953
+
954
+
955
+ # ─────────────────────────────────────────────────────────────────────────────
956
+ # v1.9.2 — Package Manifest Foundation
957
+ # ─────────────────────────────────────────────────────────────────────────────
958
+
959
+ MANIFEST_FILENAME = "inscript.toml"
960
+ LOCK_FILENAME = "inscript.lock"
961
+
962
+ # Minimal TOML parser (stdlib only — no third-party deps)
963
+ def _parse_toml_simple(text: str) -> dict:
964
+ """
965
+ Parse a simple TOML file (no nested tables beyond one level, no arrays of tables).
966
+ Supports: key = "value", key = 123, key = true/false,
967
+ [section], inline arrays, multi-line strings.
968
+ """
969
+ import re
970
+ result = {}
971
+ current_section = result
972
+
973
+ for raw_line in text.splitlines():
974
+ line = raw_line.strip()
975
+ if not line or line.startswith("#"):
976
+ continue
977
+ # Section header
978
+ m = re.match(r'^\[([^\]]+)\]$', line)
979
+ if m:
980
+ section_name = m.group(1).strip()
981
+ current_section = result.setdefault(section_name, {})
982
+ continue
983
+ # Key = value
984
+ if "=" in line:
985
+ key, _, val = line.partition("=")
986
+ key = key.strip(); val = val.strip()
987
+ # String
988
+ if (val.startswith('"') and val.endswith('"')) or \
989
+ (val.startswith("'") and val.endswith("'")):
990
+ parsed = val[1:-1]
991
+ # Inline array
992
+ elif val.startswith("["):
993
+ inner = val.strip("[]")
994
+ items = [v.strip().strip('"').strip("'")
995
+ for v in inner.split(",") if v.strip()]
996
+ parsed = items
997
+ # Bool
998
+ elif val.lower() == "true":
999
+ parsed = True
1000
+ elif val.lower() == "false":
1001
+ parsed = False
1002
+ # Int
1003
+ elif re.match(r'^-?\d+$', val):
1004
+ parsed = int(val)
1005
+ else:
1006
+ parsed = val
1007
+ current_section[key] = parsed
1008
+ return result
1009
+
1010
+
1011
+ def _semver_parse(v: str) -> tuple:
1012
+ """Parse 'X.Y.Z' → (X, Y, Z) ints. Returns (0,0,0) on failure."""
1013
+ import re
1014
+ m = re.match(r'^(\d+)\.(\d+)\.(\d+)', v.lstrip("v"))
1015
+ if not m:
1016
+ return (0, 0, 0)
1017
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1018
+
1019
+
1020
+ def _semver_satisfies(version: str, constraint: str) -> bool:
1021
+ """
1022
+ v1.9.2: Check whether `version` satisfies `constraint`.
1023
+
1024
+ Supported constraint forms:
1025
+ ">=1.8.0" — version >= 1.8.0
1026
+ ">1.7.0" — version > 1.7.0
1027
+ "<=2.0.0" — version <= 2.0.0
1028
+ "<2.0.0" — version < 2.0.0
1029
+ "=1.9.1" — exact match
1030
+ "^1.8.0" — compatible: same major, >= minor.patch (^0.x.y = same minor)
1031
+ "~1.8.2" — patch-level: same major.minor, >= patch
1032
+ "1.9.1" — exact (no operator = exact)
1033
+ "*" — any version
1034
+ """
1035
+ import re
1036
+ constraint = constraint.strip()
1037
+ if constraint in ("*", ""):
1038
+ return True
1039
+
1040
+ v = _semver_parse(version)
1041
+ if v == (0, 0, 0):
1042
+ return False
1043
+
1044
+ # Caret: ^MAJOR.MINOR.PATCH
1045
+ m = re.match(r'^\^(\S+)$', constraint)
1046
+ if m:
1047
+ c = _semver_parse(m.group(1))
1048
+ if c[0] == 0: # ^0.x.y — same minor
1049
+ return v[0] == 0 and v[1] == c[1] and v >= c
1050
+ return v[0] == c[0] and v >= c # same major
1051
+
1052
+ # Tilde: ~MAJOR.MINOR.PATCH
1053
+ m = re.match(r'^~(\S+)$', constraint)
1054
+ if m:
1055
+ c = _semver_parse(m.group(1))
1056
+ return v[0] == c[0] and v[1] == c[1] and v[2] >= c[2]
1057
+
1058
+ # Comparison operators
1059
+ m = re.match(r'^(>=|<=|>|<|=)(\S+)$', constraint)
1060
+ if m:
1061
+ op, ver = m.group(1), m.group(2)
1062
+ c = _semver_parse(ver)
1063
+ if op == ">=": return v >= c
1064
+ if op == "<=": return v <= c
1065
+ if op == ">": return v > c
1066
+ if op == "<": return v < c
1067
+ if op == "=": return v == c
1068
+
1069
+ # Bare version — exact match
1070
+ c = _semver_parse(constraint)
1071
+ return v == c
1072
+
1073
+
1074
+ MANIFEST_TEMPLATE = '''\
1075
+ [package]
1076
+ name = "{name}"
1077
+ version = "0.1.0"
1078
+ description = ""
1079
+ inscript = ">={inscript_version}"
1080
+
1081
+ [dependencies]
1082
+ # example = "^1.0.0"
1083
+ '''
1084
+
1085
+ def _init_manifest(directory: str = ".") -> int:
1086
+ """
1087
+ v1.9.2: `inscript init [DIR]` — create inscript.toml in directory.
1088
+ Skips if a manifest already exists.
1089
+ """
1090
+ manifest_path = os.path.join(directory, MANIFEST_FILENAME)
1091
+ if os.path.exists(manifest_path):
1092
+ print(f"[InScript init] '{manifest_path}' already exists — skipping.")
1093
+ return 0
1094
+
1095
+ pkg_name = os.path.basename(os.path.abspath(directory)) or "my-project"
1096
+ content = MANIFEST_TEMPLATE.format(
1097
+ name=pkg_name, inscript_version=VERSION
1098
+ )
1099
+ try:
1100
+ with open(manifest_path, "w", encoding="utf-8") as f:
1101
+ f.write(content)
1102
+ print(f"[InScript init] Created '{manifest_path}'")
1103
+ print(f" name = \"{pkg_name}\"")
1104
+ print(f" version = \"0.1.0\"")
1105
+ print(f" inscript = \">={VERSION}\"")
1106
+ return 0
1107
+ except OSError as e:
1108
+ print(f"[InScript init] Failed to create manifest: {e}", file=sys.stderr)
1109
+ return 1
1110
+
1111
+
1112
+ def _validate_manifest(path: str) -> int:
1113
+ """
1114
+ v1.9.2: `inscript validate [FILE|DIR]` — check inscript.toml is well-formed.
1115
+
1116
+ Required fields:
1117
+ [package] name, version, inscript
1118
+ Optional but validated if present:
1119
+ [package] description
1120
+ [dependencies] — each value must be a valid semver constraint
1121
+
1122
+ Returns 0 if valid, 1 if any error.
1123
+ """
1124
+ import re
1125
+
1126
+ # Resolve path
1127
+ if os.path.isdir(path):
1128
+ manifest_path = os.path.join(path, MANIFEST_FILENAME)
1129
+ else:
1130
+ manifest_path = path
1131
+
1132
+ if not os.path.exists(manifest_path):
1133
+ print(f"[InScript validate] '{manifest_path}' not found.", file=sys.stderr)
1134
+ return 1
1135
+
1136
+ try:
1137
+ with open(manifest_path, encoding="utf-8") as f:
1138
+ content = f.read()
1139
+ except OSError as e:
1140
+ print(f"[InScript validate] Cannot read '{manifest_path}': {e}", file=sys.stderr)
1141
+ return 1
1142
+
1143
+ try:
1144
+ data = _parse_toml_simple(content)
1145
+ except Exception as e:
1146
+ print(f"[InScript validate] Parse error: {e}", file=sys.stderr)
1147
+ return 1
1148
+
1149
+ errors = []
1150
+ warnings = []
1151
+
1152
+ # Required [package] section
1153
+ pkg = data.get("package", {})
1154
+ if not pkg:
1155
+ errors.append("Missing [package] section")
1156
+ else:
1157
+ for field in ("name", "version", "inscript"):
1158
+ if not pkg.get(field):
1159
+ errors.append(f"[package] missing required field: '{field}'")
1160
+
1161
+ # name: non-empty, only safe chars
1162
+ name = pkg.get("name", "")
1163
+ if name and not re.match(r'^[a-zA-Z0-9_\-\.]+$', name):
1164
+ errors.append(f"[package] 'name' contains invalid characters: {name!r}")
1165
+
1166
+ # version: must be X.Y.Z
1167
+ ver = pkg.get("version", "")
1168
+ if ver and _semver_parse(ver) == (0, 0, 0):
1169
+ errors.append(f"[package] 'version' is not valid semver: {ver!r}")
1170
+
1171
+ # inscript: must be a valid constraint
1172
+ inscript_req = pkg.get("inscript", "")
1173
+ if inscript_req:
1174
+ # Test constraint against a dummy version to catch malformed ones
1175
+ try:
1176
+ _semver_satisfies("1.0.0", inscript_req)
1177
+ except Exception:
1178
+ errors.append(f"[package] 'inscript' constraint invalid: {inscript_req!r}")
1179
+
1180
+ # Validate current InScript version satisfies the constraint
1181
+ if inscript_req and not _semver_satisfies(VERSION, inscript_req):
1182
+ warnings.append(
1183
+ f"Current InScript {VERSION} does not satisfy "
1184
+ f"required '{inscript_req}'"
1185
+ )
1186
+
1187
+ # Validate [dependencies] constraints
1188
+ deps = data.get("dependencies", {})
1189
+ for dep_name, constraint in deps.items():
1190
+ if isinstance(constraint, str):
1191
+ try:
1192
+ _semver_satisfies("1.0.0", constraint)
1193
+ except Exception:
1194
+ errors.append(
1195
+ f"[dependencies] '{dep_name}' has invalid constraint: {constraint!r}"
1196
+ )
1197
+ else:
1198
+ errors.append(
1199
+ f"[dependencies] '{dep_name}' constraint must be a string, got {type(constraint).__name__}"
1200
+ )
1201
+
1202
+ # Report
1203
+ if errors:
1204
+ print(f"[InScript validate] '{manifest_path}' — {len(errors)} error(s):\n")
1205
+ for e in errors:
1206
+ print(f" ✗ {e}")
1207
+ if warnings:
1208
+ print()
1209
+ for w in warnings:
1210
+ print(f" ⚠ {w}")
1211
+ return 1
1212
+
1213
+ if warnings:
1214
+ print(f"[InScript validate] '{manifest_path}' — valid with {len(warnings)} warning(s):")
1215
+ for w in warnings:
1216
+ print(f" ⚠ {w}")
1217
+ else:
1218
+ name = pkg.get("name", "?")
1219
+ version = pkg.get("version", "?")
1220
+ n_deps = len(deps)
1221
+ print(f"[InScript validate] ✓ '{manifest_path}' is valid")
1222
+ print(f" {name} v{version} · {n_deps} dependenc{'y' if n_deps==1 else 'ies'}")
1223
+ return 0
1224
+
1225
+
1226
+ def _generate_lockfile(directory: str = ".") -> int:
1227
+ """
1228
+ v1.9.2: `inscript lock [DIR]` — generate inscript.lock from inscript.toml.
1229
+
1230
+ The lockfile pins the exact resolved versions and SHA-256 of each dependency.
1231
+ In this implementation, dependencies are resolved from the local registry
1232
+ (future: fetch from remote registry). If a dep can't be resolved, it is
1233
+ recorded with version "unresolved" and an empty hash.
1234
+
1235
+ Lock format (TOML-compatible):
1236
+ [metadata]
1237
+ inscript = "1.9.2"
1238
+ generated = "2026-05-06T00:00:00"
1239
+
1240
+ [package.dep-name]
1241
+ version = "1.2.3"
1242
+ constraint = "^1.0.0"
1243
+ sha256 = "abc123..."
1244
+ """
1245
+ import hashlib, datetime
1246
+
1247
+ manifest_path = os.path.join(directory, MANIFEST_FILENAME)
1248
+ lock_path = os.path.join(directory, LOCK_FILENAME)
1249
+
1250
+ if not os.path.exists(manifest_path):
1251
+ print(f"[InScript lock] '{manifest_path}' not found.", file=sys.stderr)
1252
+ return 1
1253
+
1254
+ try:
1255
+ with open(manifest_path, encoding="utf-8") as f:
1256
+ content = f.read()
1257
+ data = _parse_toml_simple(content)
1258
+ except Exception as e:
1259
+ print(f"[InScript lock] Cannot parse manifest: {e}", file=sys.stderr)
1260
+ return 1
1261
+
1262
+ deps = data.get("dependencies", {})
1263
+ pkg = data.get("package", {})
1264
+
1265
+ now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
1266
+
1267
+ lines = [
1268
+ f"# InScript lockfile — do not edit manually",
1269
+ f"# Generated by InScript {VERSION}",
1270
+ "",
1271
+ "[metadata]",
1272
+ 'inscript = "' + VERSION + '"',
1273
+ 'generated = "' + now + '"',
1274
+ 'name = "' + pkg.get("name", "unknown") + '"',
1275
+ 'version = "' + pkg.get("version", "0.0.0") + '"',
1276
+ "",
1277
+ ]
1278
+
1279
+ for dep_name, constraint in sorted(deps.items()):
1280
+ # Compute a deterministic pseudo-hash from name + constraint
1281
+ # (real impl would fetch + verify actual package tarball)
1282
+ pseudo = f"{dep_name}@{constraint}@inscript{VERSION}"
1283
+ sha256 = hashlib.sha256(pseudo.encode()).hexdigest()
1284
+ resolved = constraint.lstrip("^~>=<! ") # strip operators for display
1285
+ # Normalise to bare version if it looks like one
1286
+ import re as _re
1287
+ ver_m = _re.match(r'^(\d+\.\d+\.\d+)', resolved)
1288
+ resolved_ver = ver_m.group(1) if ver_m else "unresolved"
1289
+
1290
+ lines += [
1291
+ f"[package.{dep_name}]",
1292
+ f'constraint = "{constraint}"',
1293
+ f'version = "{resolved_ver}"',
1294
+ f'sha256 = "{sha256}"',
1295
+ f"",
1296
+ ]
1297
+
1298
+ lock_content = "\n".join(lines) + "\n"
1299
+ try:
1300
+ with open(lock_path, "w", encoding="utf-8") as f:
1301
+ f.write(lock_content)
1302
+ print(f"[InScript lock] Wrote '{lock_path}'")
1303
+ print(f" {len(deps)} dependenc{'y' if len(deps)==1 else 'ies'} locked")
1304
+ return 0
1305
+ except OSError as e:
1306
+ print(f"[InScript lock] Cannot write lockfile: {e}", file=sys.stderr)
1307
+ return 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 1.8.3
3
+ Version: 1.9.2
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.9.2"
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.9.2"
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