inscript-lang 2.3.0__tar.gz → 2.5.0__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-2.3.0 → inscript_lang-2.5.0}/PKG-INFO +1 -1
  2. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript.py +174 -4
  3. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  4. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/interpreter.py +38 -20
  5. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/pyproject.toml +1 -1
  6. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/repl.py +1 -1
  7. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/README.md +0 -0
  8. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/analyzer.py +0 -0
  9. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/ast_nodes.py +0 -0
  10. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/compiler.py +0 -0
  11. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/environment.py +0 -0
  12. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/errors.py +0 -0
  13. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_fmt.py +0 -0
  14. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
  15. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  16. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  17. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_lang.egg-info/requires.txt +0 -0
  18. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_lang.egg-info/top_level.txt +0 -0
  19. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/inscript_test.py +0 -0
  20. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/lexer.py +0 -0
  21. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/parser.py +0 -0
  22. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/pygame_backend.py +0 -0
  23. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/setup.cfg +0 -0
  24. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/setup.py +0 -0
  25. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/stdlib.py +0 -0
  26. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/stdlib_extended.py +0 -0
  27. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/stdlib_game.py +0 -0
  29. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/stdlib_values.py +0 -0
  30. {inscript_lang-2.3.0 → inscript_lang-2.5.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.3.0
3
+ Version: 2.5.0
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
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.3.0"
27
+ VERSION = "2.5.0"
28
28
 
29
29
  MANIFEST_FILENAME = "inscript.toml"
30
30
  LOCK_FILENAME = "inscript.lock"
@@ -389,19 +389,166 @@ def info_package(pkg_name: str) -> int:
389
389
 
390
390
 
391
391
  def run_file(path: str, type_check: bool = True) -> int:
392
- """Load and execute an InScript source file. Returns exit code."""
392
+ """Load and execute an InScript source file (or .ibc bytecode). Returns exit code."""
393
393
  if not os.path.exists(path):
394
394
  print(f"[InScript] Error: file not found: '{path}'", file=sys.stderr)
395
395
  return 1
396
+ # v2.4.0: transparent .ibc execution via bytecode VM
397
+ if path.endswith(".ibc"):
398
+ return _run_ibc(path)
396
399
  if not path.endswith(".ins"):
397
400
  print(f"[InScript] Warning: expected .ins extension, got '{path}'", file=sys.stderr)
398
-
399
401
  with open(path, "r", encoding="utf-8") as f:
400
402
  source = f.read()
401
-
402
403
  return run_source(source, filename=path, type_check=type_check)
403
404
 
404
405
 
406
+ # ── v2.4.0: Real Bytecode Pipeline ───────────────────────────────────────────
407
+
408
+ def _ibc_path_for(src: str, out: str | None = None) -> str:
409
+ if out:
410
+ return out
411
+ base = src[:-4] if src.endswith(".ins") else src
412
+ return base + ".ibc"
413
+
414
+
415
+ def _compile_cmd(src_path: str, out_path, target: str,
416
+ dce: bool, incremental: bool) -> int:
417
+ """v2.4.0: AOT-compile .ins → .ibc using the real register-based VM compiler."""
418
+ if not os.path.exists(src_path):
419
+ print(f"[InScript] compile: file not found: '{src_path}'", file=sys.stderr)
420
+ return 1
421
+
422
+ # WASM / native — honest stubs with roadmap info
423
+ if target in ("wasm", "native"):
424
+ print(f"[InScript] --target {target} is planned for v2.5.x.")
425
+ print(f" WASM output: requires LLVM/Emscripten toolchain")
426
+ print(f" Native output: requires transpile-to-C pipeline")
427
+ print(f" Today's target: inscript --compile {src_path} --target ibc")
428
+ return 0
429
+
430
+ dest = _ibc_path_for(src_path, out_path)
431
+
432
+ # Incremental: skip if .ibc is up to date
433
+ if incremental and os.path.exists(dest):
434
+ if os.path.getmtime(dest) >= os.path.getmtime(src_path):
435
+ print(f"[InScript] compile: {dest} is up to date (--incremental)")
436
+ return 0
437
+
438
+ with open(src_path, "r", encoding="utf-8") as f:
439
+ source = f.read()
440
+
441
+ # Parse + compile to FnProto bytecode
442
+ try:
443
+ from compiler import compile_source
444
+ from vm_code import write_ibc
445
+ proto = compile_source(source, filename=src_path)
446
+ except Exception as e:
447
+ print(f"[InScript] compile error: {e}", file=sys.stderr)
448
+ return 1
449
+
450
+ # Dead-code elimination at the proto level
451
+ if dce:
452
+ n_removed = _dce_proto(proto)
453
+ if n_removed:
454
+ print(f"[InScript] DCE: pruned {n_removed} unreachable nested proto(s)")
455
+
456
+ # Write .ibc
457
+ try:
458
+ write_ibc(proto, dest)
459
+ except Exception as e:
460
+ print(f"[InScript] serialize error: {e}", file=sys.stderr)
461
+ return 1
462
+
463
+ src_size = os.path.getsize(src_path)
464
+ ibc_size = os.path.getsize(dest)
465
+ dce_tag = " +DCE" if dce else ""
466
+ print(f"[InScript] compiled{dce_tag}: {src_path} → {dest}")
467
+ print(f" source {src_size:,}B → bytecode {ibc_size:,}B "
468
+ f"({ibc_size/src_size*100:.0f}% of source)")
469
+ return 0
470
+
471
+
472
+ def _run_ibc(path: str) -> int:
473
+ """v2.4.0: Load and execute a pre-compiled .ibc bytecode file."""
474
+ try:
475
+ from vm_code import read_ibc
476
+ from vm import VM
477
+ proto = read_ibc(path)
478
+ except ValueError as e:
479
+ print(f"[InScript] run: {e}", file=sys.stderr)
480
+ return 1
481
+ except Exception as e:
482
+ print(f"[InScript] run: failed to load '{path}': {e}", file=sys.stderr)
483
+ return 1
484
+ try:
485
+ vm = VM(filename=path)
486
+ vm.run(proto)
487
+ return 0
488
+ except Exception as e:
489
+ print(f"[InScript] runtime error: {e}", file=sys.stderr)
490
+ return 1
491
+
492
+
493
+ def _dce_proto(proto) -> int:
494
+ """
495
+ v2.4.0: Dead-code elimination at the FnProto level.
496
+ Removes nested protos whose names are never referenced in the parent's
497
+ name table. Returns the number of protos removed.
498
+ """
499
+ if not proto.protos:
500
+ return 0
501
+ referenced = set(proto.names)
502
+ before = len(proto.protos)
503
+ # Recurse into survivors first
504
+ for sub in proto.protos:
505
+ _dce_proto(sub)
506
+ # Remove unreferenced nested protos
507
+ proto.protos = [p for p in proto.protos if p.name in referenced or p.name == "<main>"]
508
+ removed = before - len(proto.protos)
509
+ return removed
510
+
511
+
512
+ def _dce_pass(program):
513
+ """AST-level DCE (used by test_v240 for unit-testing DCE logic)."""
514
+ from ast_nodes import FunctionDecl, IdentExpr, Program
515
+
516
+ referenced: set = set()
517
+
518
+ def _walk(node):
519
+ if node is None: return
520
+ if isinstance(node, IdentExpr):
521
+ referenced.add(node.name)
522
+ return
523
+ for attr in (vars(node).values() if hasattr(node, '__dict__') else []):
524
+ if hasattr(attr, '__dict__'):
525
+ _walk(attr)
526
+ elif isinstance(attr, list):
527
+ for item in attr:
528
+ if hasattr(item, '__dict__'): _walk(item)
529
+ elif isinstance(attr, tuple):
530
+ for item in attr:
531
+ if hasattr(item, '__dict__'): _walk(item)
532
+
533
+ for stmt in program.body:
534
+ _walk(stmt)
535
+
536
+ kept = []
537
+ removed = 0
538
+ for stmt in program.body:
539
+ if isinstance(stmt, FunctionDecl) and stmt.name not in referenced:
540
+ if not getattr(stmt, 'exported', False):
541
+ removed += 1
542
+ continue
543
+ kept.append(stmt)
544
+
545
+ if removed:
546
+ print(f"[InScript] DCE: eliminated {removed} unreferenced function(s)")
547
+ return Program(body=kept)
548
+
549
+
550
+
551
+
405
552
  # ─── v1.6.0 helpers ──────────────────────────────────────────────────────────
406
553
 
407
554
  def _find_ins_files(directory: str):
@@ -2108,12 +2255,35 @@ Examples:
2108
2255
  help="Window height for --game mode (default: 600)")
2109
2256
  parser.add_argument("--fps", type=int, default=60,
2110
2257
  help="Target FPS for --game mode (default: 60)")
2258
+ # ── v2.4.0: compilation flags ────────────────────────────────────────────
2259
+ parser.add_argument("--compile", metavar="FILE",
2260
+ help="v2.4.0: AOT-compile FILE.ins → FILE.ibc bytecode")
2261
+ parser.add_argument("--output", "-o", metavar="OUT",
2262
+ help="v2.4.0: Output path for --compile (default: same dir as input)")
2263
+ parser.add_argument("--target", metavar="TARGET", default="interpreter",
2264
+ choices=["interpreter", "wasm", "native", "ibc"],
2265
+ help="v2.4.0: Compilation target: interpreter (default), ibc, wasm, native")
2266
+ parser.add_argument("--no-dce", action="store_true",
2267
+ help="v2.4.0: Disable dead-code elimination before compilation")
2268
+ parser.add_argument("--incremental", action="store_true",
2269
+ help="v2.4.0: Skip recompile if .ibc is newer than source")
2111
2270
  args = parser.parse_args()
2112
2271
 
2113
2272
  if args.version:
2114
2273
  print(f"InScript {VERSION}")
2115
2274
  return
2116
2275
 
2276
+ # ── v2.4.0: --compile ────────────────────────────────────────────────────
2277
+ if args.compile:
2278
+ import sys as _sys
2279
+ _sys.exit(_compile_cmd(
2280
+ src_path = args.compile,
2281
+ out_path = args.output,
2282
+ target = args.target,
2283
+ dce = not args.no_dce,
2284
+ incremental= args.incremental,
2285
+ ) or 0)
2286
+
2117
2287
  # ── inscript --fmt / --fmt-check / --fmt-dry-run ─────────────────────────
2118
2288
  if args.fmt or args.fmt_check or args.fmt_dry_run:
2119
2289
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.3.0
3
+ Version: 2.5.0
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
@@ -28,6 +28,9 @@ from errors import (
28
28
  # INTERPRETER
29
29
  # ─────────────────────────────────────────────────────────────────────────────
30
30
 
31
+ # v2.4.0: sentinel for inline cache misses (faster than None since None is valid)
32
+ _CACHE_MISS = object()
33
+
31
34
  class Interpreter(Visitor):
32
35
  """
33
36
  Tree-walking interpreter. Visits each AST node and returns a Python value.
@@ -48,6 +51,9 @@ class Interpreter(Visitor):
48
51
  self._current_fn: object = None
49
52
  # v1.4.0: defer stack — list of exprs to run at end of current function
50
53
  self._deferred: list = []
54
+ # v2.4.0: inline caches — monomorphic site caching for hot paths
55
+ self._ic_struct_fields: dict = {} # struct_name -> {field: default_idx}
56
+ self._ic_fn_lookup: dict = {} # fn_name -> InScriptFunction (global scope)
51
57
 
52
58
  self._register_builtins()
53
59
 
@@ -533,6 +539,8 @@ class Interpreter(Visitor):
533
539
  # v1.9.7: mark async functions
534
540
  fn.is_async = getattr(node, 'is_async', False)
535
541
  self._env.define(node.name, fn)
542
+ # v2.4.0: invalidate inline cache if this function was cached
543
+ self._ic_fn_lookup.pop(node.name, None)
536
544
  return fn
537
545
 
538
546
  def visit_GeneratorFnDecl(self, node) -> Any:
@@ -1574,10 +1582,18 @@ class Interpreter(Visitor):
1574
1582
  decl = None
1575
1583
  parent_decl = getattr(decl, '_resolved_parent', None)
1576
1584
  if parent_decl is not None:
1577
- # Return a SuperProxy that knows the parent and self
1578
1585
  return _SuperProxy(self_val, parent_decl, self)
1579
1586
  self._error("'super' only valid in a struct that extends another", node.line)
1580
- return self._env.get(node.name, node.line)
1587
+ # v2.4.0: inline cache — fast path for global function lookups
1588
+ name = node.name
1589
+ ic = self._ic_fn_lookup.get(name)
1590
+ if ic is not None:
1591
+ return ic
1592
+ val = self._env.get(name, node.line)
1593
+ # Populate cache for top-level InScriptFunctions (stable globals only)
1594
+ if isinstance(val, InScriptFunction) and self._env is self._globals:
1595
+ self._ic_fn_lookup[name] = val
1596
+ return val
1581
1597
 
1582
1598
  def visit_ArrayLiteralExpr(self, node: ArrayLiteralExpr) -> Any:
1583
1599
  result = []
@@ -2037,6 +2053,11 @@ class Interpreter(Visitor):
2037
2053
 
2038
2054
  def visit_GetAttrExpr(self, node: GetAttrExpr) -> Any:
2039
2055
  obj = self.visit(node.obj)
2056
+ # v2.4.0: inline cache for struct field access — skip full _get_attr for hot path
2057
+ if isinstance(obj, InScriptInstance):
2058
+ val = obj.fields.get(node.attr, _CACHE_MISS)
2059
+ if val is not _CACHE_MISS:
2060
+ return val
2040
2061
  return _get_attr(obj, node.attr, node.line, self)
2041
2062
 
2042
2063
  def visit_IndexExpr(self, node: IndexExpr) -> Any:
@@ -2955,16 +2976,16 @@ def _arr_count(lst, fn_or_val, interp):
2955
2976
 
2956
2977
 
2957
2978
  def _get_attr(obj: Any, name: str, line: int, interp: Interpreter) -> Any:
2958
- # v2.3.0: concurrency primitive attribute dispatch
2959
- from stdlib_values import InScriptTask, InScriptChannel, InScriptMutex, InScriptRWLock, InScriptTimerHandle
2960
-
2961
- if isinstance(obj, InScriptTask):
2979
+ # v2.3.0/v2.4.0: concurrency primitive attribute dispatch
2980
+ # Only import when obj is one of the concurrency types (type-name fast check)
2981
+ _tname = type(obj).__name__
2982
+ if _tname == "InScriptTask":
2983
+ import stdlib_values as _sv
2962
2984
  if name == "join": return lambda timeout=None: obj.join(timeout)
2963
2985
  if name == "done": return obj.done
2964
2986
  if name == "result": return obj._result
2965
2987
  interp._error(f"Task has no member '{name}'. Available: join, done, result", line)
2966
-
2967
- if isinstance(obj, InScriptChannel):
2988
+ elif _tname == "InScriptChannel":
2968
2989
  if name == "send": return lambda v: obj.send(v)
2969
2990
  if name == "recv": return lambda: obj.recv()
2970
2991
  if name == "try_recv": return lambda: obj.try_recv()
@@ -2973,20 +2994,17 @@ def _get_attr(obj: Any, name: str, line: int, interp: Interpreter) -> Any:
2973
2994
  if name == "size": return obj._q.qsize()
2974
2995
  if name == "is_empty": return obj._q.empty()
2975
2996
  interp._error(f"Channel has no member '{name}'. Available: send, recv, try_recv, close, closed, size", line)
2976
-
2977
- if isinstance(obj, InScriptMutex):
2978
- if name == "lock": return lambda fn: obj.lock(lambda args: interp._call_fn(fn, args))
2979
- if name == "set": return lambda v: obj.set(v)
2980
- if name == "value": return obj.value
2997
+ elif _tname == "InScriptMutex":
2998
+ if name == "lock": return lambda fn: obj.lock(lambda args: interp._call_fn(fn, args))
2999
+ if name == "set": return lambda v: obj.set(v)
3000
+ if name == "value": return obj.value
2981
3001
  interp._error(f"Mutex has no member '{name}'. Available: lock, set, value", line)
2982
-
2983
- if isinstance(obj, InScriptRWLock):
2984
- if name == "read": return lambda fn: obj.read(lambda args: interp._call_fn(fn, args))
2985
- if name == "write": return lambda fn: obj.write(lambda args: interp._call_fn(fn, args))
2986
- if name == "value": return obj.value
3002
+ elif _tname == "InScriptRWLock":
3003
+ if name == "read": return lambda fn: obj.read(lambda args: interp._call_fn(fn, args))
3004
+ if name == "write": return lambda fn: obj.write(lambda args: interp._call_fn(fn, args))
3005
+ if name == "value": return obj.value
2987
3006
  interp._error(f"RWLock has no member '{name}'. Available: read, write, value", line)
2988
-
2989
- if isinstance(obj, InScriptTimerHandle):
3007
+ elif _tname == "InScriptTimerHandle":
2990
3008
  if name == "cancel": return lambda: obj.cancel()
2991
3009
  interp._error(f"TimerHandle has no member '{name}'. Available: cancel", line)
2992
3010
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript-lang"
7
- version = "2.3.0"
7
+ version = "2.5.0"
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 = "2.3.0"
43
+ VERSION = "2.5.0"
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
File without changes