inscript-lang 2.2.0__tar.gz → 2.4.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.
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/PKG-INFO +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/ast_nodes.py +5 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript.py +174 -4
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/interpreter.py +159 -20
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/parser.py +33 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/pyproject.toml +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/repl.py +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/stdlib.py +71 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/stdlib_values.py +148 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/README.md +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/analyzer.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/compiler.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/environment.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/errors.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_fmt.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/inscript_test.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/lexer.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/pygame_backend.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/setup.cfg +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/setup.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/stdlib_extended.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/stdlib_game.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.4.0}/vm.py +0 -0
|
@@ -751,3 +751,8 @@ class DestructParam(Node):
|
|
|
751
751
|
names: list # list of str (field/element names to bind)
|
|
752
752
|
rest: object # str name for rest element (...tail), or None
|
|
753
753
|
type_ann: object # TypeAnnotation or None
|
|
754
|
+
|
|
755
|
+
@dataclass
|
|
756
|
+
class TaskSpawnExpr(Node):
|
|
757
|
+
"""v2.3.0: spawn <expr> — run expression concurrently, return InScriptTask."""
|
|
758
|
+
expr: object
|
|
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
|
|
|
24
24
|
SemanticError, InScriptRuntimeError,
|
|
25
25
|
MultiError, InScriptWarning)
|
|
26
26
|
|
|
27
|
-
VERSION = "2.
|
|
27
|
+
VERSION = "2.4.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:
|
|
@@ -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
|
|
|
@@ -421,13 +427,16 @@ class Interpreter(Visitor):
|
|
|
421
427
|
"is_bool": lambda v: isinstance(v, bool),
|
|
422
428
|
"is_array": lambda v: isinstance(v, list),
|
|
423
429
|
"is_dict": lambda v: isinstance(v, dict),
|
|
424
|
-
# ── Concurrency
|
|
425
|
-
"thread":
|
|
426
|
-
"channel": lambda cap=0:
|
|
427
|
-
"make_channel": lambda cap=0:
|
|
428
|
-
"chan_send":
|
|
429
|
-
"chan_recv":
|
|
430
|
-
"
|
|
430
|
+
# ── Concurrency v2.3.0 ───────────────────────────────────────────
|
|
431
|
+
"thread": lambda fn, *args: self._spawn_thread(fn, list(args)),
|
|
432
|
+
"channel": lambda cap=0: __import__('stdlib_values').InScriptChannel(int(cap) if cap else 0),
|
|
433
|
+
"make_channel": lambda cap=0: __import__('stdlib_values').InScriptChannel(int(cap) if cap else 0),
|
|
434
|
+
"chan_send": lambda ch, v: ch.send(v) if hasattr(ch, 'send') else (ch["_channel"].put(v) if isinstance(ch, dict) and "_channel" in ch else None),
|
|
435
|
+
"chan_recv": lambda ch: ch.recv() if hasattr(ch, 'recv') else (ch["_channel"].get(timeout=5) if isinstance(ch, dict) and "_channel" in ch else None),
|
|
436
|
+
"chan_try_recv": lambda ch: ch.try_recv() if hasattr(ch, 'try_recv') else None,
|
|
437
|
+
"mutex": lambda v=None: __import__('stdlib_values').InScriptMutex(v),
|
|
438
|
+
"rwlock": lambda v=None: __import__('stdlib_values').InScriptRWLock(v),
|
|
439
|
+
"sleep": lambda s: __import__('time').sleep(s),
|
|
431
440
|
# ── JSON ──────────────────────────────────────────────────────────
|
|
432
441
|
"json_encode": lambda v: __import__('json').dumps(v),
|
|
433
442
|
"json_decode": lambda s: __import__('json').loads(s),
|
|
@@ -530,6 +539,8 @@ class Interpreter(Visitor):
|
|
|
530
539
|
# v1.9.7: mark async functions
|
|
531
540
|
fn.is_async = getattr(node, 'is_async', False)
|
|
532
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)
|
|
533
544
|
return fn
|
|
534
545
|
|
|
535
546
|
def visit_GeneratorFnDecl(self, node) -> Any:
|
|
@@ -1080,6 +1091,30 @@ class Interpreter(Visitor):
|
|
|
1080
1091
|
if isinstance(iterable, dict) and iterable.get("_enum_definition"):
|
|
1081
1092
|
variants = [v for k, v in iterable.items() if not k.startswith("_")]
|
|
1082
1093
|
iterable = variants
|
|
1094
|
+
# v2.3.0: async for var in channel { } — drain channel until closed
|
|
1095
|
+
from stdlib_values import InScriptChannel as _ISCh
|
|
1096
|
+
if isinstance(iterable, _ISCh) and getattr(node, '_async_iter', False):
|
|
1097
|
+
while not (iterable.closed and iterable._q.empty()):
|
|
1098
|
+
val = iterable.try_recv()
|
|
1099
|
+
if val is None:
|
|
1100
|
+
if iterable.closed:
|
|
1101
|
+
break
|
|
1102
|
+
import time as _t; _t.sleep(0.001)
|
|
1103
|
+
continue
|
|
1104
|
+
self._push("for")
|
|
1105
|
+
self._env.define(node.var_name, val)
|
|
1106
|
+
try:
|
|
1107
|
+
self._exec_block_no_scope(node.body)
|
|
1108
|
+
except BreakSignal as sig:
|
|
1109
|
+
self._pop()
|
|
1110
|
+
if sig.label: raise
|
|
1111
|
+
break
|
|
1112
|
+
except ContinueSignal as sig:
|
|
1113
|
+
self._pop()
|
|
1114
|
+
if sig.label: raise
|
|
1115
|
+
continue
|
|
1116
|
+
self._pop()
|
|
1117
|
+
return
|
|
1083
1118
|
# Support list, range, InScriptRange, string, dict, generator
|
|
1084
1119
|
if isinstance(iterable, InScriptRange):
|
|
1085
1120
|
it = iter(iterable)
|
|
@@ -1547,10 +1582,18 @@ class Interpreter(Visitor):
|
|
|
1547
1582
|
decl = None
|
|
1548
1583
|
parent_decl = getattr(decl, '_resolved_parent', None)
|
|
1549
1584
|
if parent_decl is not None:
|
|
1550
|
-
# Return a SuperProxy that knows the parent and self
|
|
1551
1585
|
return _SuperProxy(self_val, parent_decl, self)
|
|
1552
1586
|
self._error("'super' only valid in a struct that extends another", node.line)
|
|
1553
|
-
|
|
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
|
|
1554
1597
|
|
|
1555
1598
|
def visit_ArrayLiteralExpr(self, node: ArrayLiteralExpr) -> Any:
|
|
1556
1599
|
result = []
|
|
@@ -2010,6 +2053,11 @@ class Interpreter(Visitor):
|
|
|
2010
2053
|
|
|
2011
2054
|
def visit_GetAttrExpr(self, node: GetAttrExpr) -> Any:
|
|
2012
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
|
|
2013
2061
|
return _get_attr(obj, node.attr, node.line, self)
|
|
2014
2062
|
|
|
2015
2063
|
def visit_IndexExpr(self, node: IndexExpr) -> Any:
|
|
@@ -2524,11 +2572,10 @@ class Interpreter(Visitor):
|
|
|
2524
2572
|
|
|
2525
2573
|
def visit_AwaitExpr(self, node: AwaitExpr) -> Any:
|
|
2526
2574
|
"""
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
so they are catchable by InScript try/catch blocks.
|
|
2575
|
+
v2.3.0: await <expr>
|
|
2576
|
+
InScript async fns return InScriptCoroutine. We drive them to completion
|
|
2577
|
+
using send(None) — the coroutine body runs synchronously and raises
|
|
2578
|
+
StopIteration(result) immediately (InScript has no real IO yields).
|
|
2532
2579
|
"""
|
|
2533
2580
|
val = self.visit(node.expr)
|
|
2534
2581
|
if isinstance(val, InScriptCoroutine):
|
|
@@ -2538,17 +2585,77 @@ class Interpreter(Visitor):
|
|
|
2538
2585
|
except StopIteration as e:
|
|
2539
2586
|
return e.value
|
|
2540
2587
|
except InScriptRuntimeError:
|
|
2541
|
-
raise
|
|
2588
|
+
raise
|
|
2542
2589
|
except Exception as e:
|
|
2543
|
-
# Convert Python exceptions to InScriptRuntimeError so
|
|
2544
|
-
# they are catchable by InScript try/catch blocks.
|
|
2545
2590
|
err = InScriptRuntimeError(str(e), getattr(node, 'line', 0))
|
|
2546
2591
|
err.thrown_value = str(e)
|
|
2547
2592
|
raise err
|
|
2548
|
-
return val # plain value
|
|
2593
|
+
return val # plain value passthrough
|
|
2549
2594
|
|
|
2550
|
-
def visit_SpawnExpr(self, node
|
|
2551
|
-
|
|
2595
|
+
def visit_SpawnExpr(self, node) -> Any:
|
|
2596
|
+
"""ECS spawn — handled by game backend; stub in core interpreter."""
|
|
2597
|
+
return None
|
|
2598
|
+
|
|
2599
|
+
def visit_TaskSpawnExpr(self, node) -> Any:
|
|
2600
|
+
"""v2.3.0: spawn <expr> — run in background thread, return InScriptTask."""
|
|
2601
|
+
from stdlib_values import InScriptTask
|
|
2602
|
+
expr_val = self.visit(node.expr)
|
|
2603
|
+
if isinstance(expr_val, InScriptCoroutine):
|
|
2604
|
+
coro = expr_val.coro
|
|
2605
|
+
def _run_coro():
|
|
2606
|
+
try:
|
|
2607
|
+
coro.send(None)
|
|
2608
|
+
except StopIteration as e:
|
|
2609
|
+
return e.value
|
|
2610
|
+
return None
|
|
2611
|
+
task = InScriptTask(_run_coro, None)
|
|
2612
|
+
elif callable(expr_val):
|
|
2613
|
+
fn = expr_val
|
|
2614
|
+
def _run_fn():
|
|
2615
|
+
return self._call_fn(fn, [])
|
|
2616
|
+
task = InScriptTask(_run_fn, None)
|
|
2617
|
+
else:
|
|
2618
|
+
val = expr_val
|
|
2619
|
+
task = InScriptTask(lambda: val, None)
|
|
2620
|
+
return task
|
|
2621
|
+
|
|
2622
|
+
def visit_SelectStmt(self, node) -> Any:
|
|
2623
|
+
"""v2.3.0: select { case x = ch.recv() { } case ch.send(v) { } case timeout(t) { } }"""
|
|
2624
|
+
from stdlib_values import InScriptChannel
|
|
2625
|
+
from ast_nodes import CallExpr, GetAttrExpr
|
|
2626
|
+
for clause in node.clauses:
|
|
2627
|
+
kind = clause.get("kind")
|
|
2628
|
+
if kind == "recv":
|
|
2629
|
+
# clause["channel"] is CallExpr(callee=GetAttr(ch, "recv"), args=[])
|
|
2630
|
+
call_expr = clause.get("channel")
|
|
2631
|
+
if call_expr and isinstance(call_expr, CallExpr):
|
|
2632
|
+
ch = self.visit(call_expr.callee.obj)
|
|
2633
|
+
else:
|
|
2634
|
+
ch = None
|
|
2635
|
+
if isinstance(ch, InScriptChannel):
|
|
2636
|
+
val = ch.try_recv()
|
|
2637
|
+
if val is not None:
|
|
2638
|
+
var = clause.get("var")
|
|
2639
|
+
if var:
|
|
2640
|
+
self._env.define(var, val)
|
|
2641
|
+
self.visit(clause["body"])
|
|
2642
|
+
return
|
|
2643
|
+
elif kind == "send":
|
|
2644
|
+
# clause["channel"] is CallExpr(callee=GetAttr(ch, "send"), args=[val])
|
|
2645
|
+
call_expr = clause.get("channel")
|
|
2646
|
+
if call_expr and isinstance(call_expr, CallExpr):
|
|
2647
|
+
ch = self.visit(call_expr.callee.obj)
|
|
2648
|
+
val = self.visit(call_expr.args[0].value) if call_expr.args else None
|
|
2649
|
+
else:
|
|
2650
|
+
ch = val = None
|
|
2651
|
+
if isinstance(ch, InScriptChannel) and not ch._q.full():
|
|
2652
|
+
ch.send(val)
|
|
2653
|
+
self.visit(clause["body"])
|
|
2654
|
+
return
|
|
2655
|
+
elif kind == "timeout":
|
|
2656
|
+
# Timeout clause fires as fallback — always last resort
|
|
2657
|
+
self.visit(clause["body"])
|
|
2658
|
+
return
|
|
2552
2659
|
|
|
2553
2660
|
def visit_MatchArm(self, node: MatchArm) -> Any:
|
|
2554
2661
|
return self.visit(node.body)
|
|
@@ -2869,6 +2976,38 @@ def _arr_count(lst, fn_or_val, interp):
|
|
|
2869
2976
|
|
|
2870
2977
|
|
|
2871
2978
|
def _get_attr(obj: Any, name: str, line: int, interp: Interpreter) -> Any:
|
|
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
|
|
2984
|
+
if name == "join": return lambda timeout=None: obj.join(timeout)
|
|
2985
|
+
if name == "done": return obj.done
|
|
2986
|
+
if name == "result": return obj._result
|
|
2987
|
+
interp._error(f"Task has no member '{name}'. Available: join, done, result", line)
|
|
2988
|
+
elif _tname == "InScriptChannel":
|
|
2989
|
+
if name == "send": return lambda v: obj.send(v)
|
|
2990
|
+
if name == "recv": return lambda: obj.recv()
|
|
2991
|
+
if name == "try_recv": return lambda: obj.try_recv()
|
|
2992
|
+
if name == "close": return lambda: obj.close()
|
|
2993
|
+
if name == "closed": return obj.closed
|
|
2994
|
+
if name == "size": return obj._q.qsize()
|
|
2995
|
+
if name == "is_empty": return obj._q.empty()
|
|
2996
|
+
interp._error(f"Channel has no member '{name}'. Available: send, recv, try_recv, close, closed, size", line)
|
|
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
|
|
3001
|
+
interp._error(f"Mutex has no member '{name}'. Available: lock, set, value", line)
|
|
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
|
|
3006
|
+
interp._error(f"RWLock has no member '{name}'. Available: read, write, value", line)
|
|
3007
|
+
elif _tname == "InScriptTimerHandle":
|
|
3008
|
+
if name == "cancel": return lambda: obj.cancel()
|
|
3009
|
+
interp._error(f"TimerHandle has no member '{name}'. Available: cancel", line)
|
|
3010
|
+
|
|
2872
3011
|
# StructDecl used as namespace for static method access: M.sq(5)
|
|
2873
3012
|
if isinstance(obj, StructDecl):
|
|
2874
3013
|
if hasattr(obj, '_static_ns') and name in obj._static_ns:
|
|
@@ -147,6 +147,12 @@ class Parser:
|
|
|
147
147
|
|
|
148
148
|
# --- Async/Await ---
|
|
149
149
|
if tok.type == TT.ASYNC:
|
|
150
|
+
# v2.3.0: async for var in channel { } — iterate over async iterable
|
|
151
|
+
if self.peek.type == TT.FOR:
|
|
152
|
+
self.advance() # consume 'async'
|
|
153
|
+
node = self.parse_for_in()
|
|
154
|
+
node._async_iter = True # flag for interpreter
|
|
155
|
+
return node
|
|
150
156
|
return self.parse_async_fn()
|
|
151
157
|
|
|
152
158
|
# --- Await expr ---
|
|
@@ -1817,6 +1823,26 @@ class Parser:
|
|
|
1817
1823
|
args = self.parse_arg_list()
|
|
1818
1824
|
expr = CallExpr(callee=expr, args=args, line=line, col=col)
|
|
1819
1825
|
|
|
1826
|
+
# v2.3.0: generic builtin call: channel<T>(cap) / Stack<T>() etc.
|
|
1827
|
+
# Pattern: IDENT < TYPE_OR_IDENT > (
|
|
1828
|
+
elif (self.check(TT.LT)
|
|
1829
|
+
and isinstance(expr, IdentExpr)
|
|
1830
|
+
and self.peek.type in (TT.INT_TYPE, TT.FLOAT_TYPE, TT.STRING_TYPE,
|
|
1831
|
+
TT.BOOL_TYPE, TT.IDENT, TT.VOID_TYPE)):
|
|
1832
|
+
saved = self.pos
|
|
1833
|
+
self.advance() # consume '<'
|
|
1834
|
+
self.advance() # consume type name
|
|
1835
|
+
if self.check(TT.GT):
|
|
1836
|
+
self.advance() # consume '>'
|
|
1837
|
+
if self.check(TT.LPAREN):
|
|
1838
|
+
# It's a generic call — type param is stripped (runtime ignores it)
|
|
1839
|
+
args = self.parse_arg_list()
|
|
1840
|
+
expr = CallExpr(callee=expr, args=args, line=line, col=col)
|
|
1841
|
+
continue
|
|
1842
|
+
# Not a generic call — backtrack and stop postfix chain
|
|
1843
|
+
self.pos = saved
|
|
1844
|
+
break
|
|
1845
|
+
|
|
1820
1846
|
# Optional chaining: expr?.member or expr?.method(args)
|
|
1821
1847
|
elif self.check(TT.QUESTION_DOT):
|
|
1822
1848
|
self.advance() # consume '?.'
|
|
@@ -2039,6 +2065,13 @@ class Parser:
|
|
|
2039
2065
|
expr = self.parse_unary()
|
|
2040
2066
|
return AwaitExpr(expr=expr, line=line, col=col)
|
|
2041
2067
|
|
|
2068
|
+
# v2.3.0: spawn <expr> — concurrency task
|
|
2069
|
+
if tok.type == TT.SPAWN:
|
|
2070
|
+
self.advance() # consume 'spawn'
|
|
2071
|
+
expr = self.parse_unary()
|
|
2072
|
+
from ast_nodes import TaskSpawnExpr
|
|
2073
|
+
return TaskSpawnExpr(expr=expr, line=line, col=col)
|
|
2074
|
+
|
|
2042
2075
|
# try { expr } catch e { expr } — try as expression (returns value)
|
|
2043
2076
|
if tok.type == TT.TRY:
|
|
2044
2077
|
return self._parse_try_expr()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.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.
|
|
43
|
+
VERSION = "2.4.0"
|
|
44
44
|
|
|
45
45
|
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
46
46
|
def _c(code, text):
|
|
@@ -1063,8 +1063,79 @@ except KeyError:
|
|
|
1063
1063
|
"sleep_sync": _timer_sleep_sync, # synchronous fallback
|
|
1064
1064
|
"now": lambda: int(_time_mod.time() * 1000), # ms since epoch
|
|
1065
1065
|
"now_sec": lambda: _time_mod.time(), # seconds since epoch
|
|
1066
|
+
# v2.3.0: after / every / cancel
|
|
1067
|
+
"after": None, # patched below
|
|
1068
|
+
"every": None, # patched below
|
|
1069
|
+
"cancel": None, # patched below
|
|
1066
1070
|
})
|
|
1067
1071
|
|
|
1072
|
+
# v2.3.0: patch timer.after / timer.every / timer.cancel
|
|
1073
|
+
try:
|
|
1074
|
+
import threading as _thr_mod
|
|
1075
|
+
from stdlib_values import InScriptTimerHandle as _THndl
|
|
1076
|
+
|
|
1077
|
+
def _timer_after(ms, fn):
|
|
1078
|
+
"""timer.after(ms, fn) — call fn() once after ms milliseconds."""
|
|
1079
|
+
def _fire():
|
|
1080
|
+
try: fn([])
|
|
1081
|
+
except Exception: pass
|
|
1082
|
+
t = _thr_mod.Timer(int(ms) / 1000.0, _fire)
|
|
1083
|
+
t.daemon = True
|
|
1084
|
+
t.start()
|
|
1085
|
+
return _THndl(t)
|
|
1086
|
+
|
|
1087
|
+
def _timer_every(ms, fn):
|
|
1088
|
+
"""timer.every(ms, fn) — call fn() every ms milliseconds until cancelled."""
|
|
1089
|
+
handle = [None]
|
|
1090
|
+
def _tick():
|
|
1091
|
+
try: fn([])
|
|
1092
|
+
except Exception: pass
|
|
1093
|
+
t = _thr_mod.Timer(int(ms) / 1000.0, _tick)
|
|
1094
|
+
t.daemon = True
|
|
1095
|
+
handle[0] = t
|
|
1096
|
+
t.start()
|
|
1097
|
+
t0 = _thr_mod.Timer(int(ms) / 1000.0, _tick)
|
|
1098
|
+
t0.daemon = True
|
|
1099
|
+
handle[0] = t0
|
|
1100
|
+
t0.start()
|
|
1101
|
+
class _EveryHandle:
|
|
1102
|
+
def cancel(self): handle[0] and handle[0].cancel()
|
|
1103
|
+
def __repr__(self): return "<TimerHandle periodic>"
|
|
1104
|
+
return _EveryHandle()
|
|
1105
|
+
|
|
1106
|
+
def _timer_cancel(handle):
|
|
1107
|
+
if hasattr(handle, 'cancel'): handle.cancel()
|
|
1108
|
+
|
|
1109
|
+
_MODULES["timer"]["after"] = _timer_after
|
|
1110
|
+
_MODULES["timer"]["every"] = _timer_every
|
|
1111
|
+
_MODULES["timer"]["cancel"] = _timer_cancel
|
|
1112
|
+
except Exception as _te:
|
|
1113
|
+
pass
|
|
1114
|
+
|
|
1115
|
+
# v2.3.0: channel, mutex, rwlock as global builtins
|
|
1116
|
+
try:
|
|
1117
|
+
from stdlib_values import InScriptChannel as _ISChannel
|
|
1118
|
+
from stdlib_values import InScriptMutex as _ISMutex
|
|
1119
|
+
from stdlib_values import InScriptRWLock as _ISRWLock
|
|
1120
|
+
|
|
1121
|
+
def _make_channel(capacity=0):
|
|
1122
|
+
cap = capacity[0] if isinstance(capacity, list) and capacity else (capacity or 0)
|
|
1123
|
+
return _ISChannel(int(cap))
|
|
1124
|
+
|
|
1125
|
+
def _make_mutex(value=None):
|
|
1126
|
+
v = value[0] if isinstance(value, list) and value else value
|
|
1127
|
+
return _ISMutex(v)
|
|
1128
|
+
|
|
1129
|
+
def _make_rwlock(value=None):
|
|
1130
|
+
v = value[0] if isinstance(value, list) and value else value
|
|
1131
|
+
return _ISRWLock(v)
|
|
1132
|
+
|
|
1133
|
+
# Register as global builtins (not modules)
|
|
1134
|
+
# channel/mutex/rwlock are registered directly in the interpreter globals (interpreter.py)
|
|
1135
|
+
pass
|
|
1136
|
+
except Exception as _ce:
|
|
1137
|
+
pass
|
|
1138
|
+
|
|
1068
1139
|
|
|
1069
1140
|
|
|
1070
1141
|
# ── Extended stdlib (Part 1): collections, datetime, fs, process, log, test, compress
|
|
@@ -326,3 +326,151 @@ class InScriptGenerator:
|
|
|
326
326
|
except StopIteration:
|
|
327
327
|
self._done = True
|
|
328
328
|
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ── v2.3.0: Concurrency primitives ───────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
import threading as _threading
|
|
334
|
+
import queue as _queue
|
|
335
|
+
|
|
336
|
+
class InScriptTask:
|
|
337
|
+
"""
|
|
338
|
+
v2.3.0: Handle returned by `spawn expr`.
|
|
339
|
+
Runs a callable in a background thread.
|
|
340
|
+
"""
|
|
341
|
+
def __init__(self, fn, args):
|
|
342
|
+
self._result = None
|
|
343
|
+
self._error = None
|
|
344
|
+
self._done = _threading.Event()
|
|
345
|
+
self._thread = _threading.Thread(target=self._run, args=(fn, args), daemon=True)
|
|
346
|
+
self._thread.start()
|
|
347
|
+
|
|
348
|
+
def _run(self, fn, args):
|
|
349
|
+
try:
|
|
350
|
+
self._result = fn()
|
|
351
|
+
except Exception as e:
|
|
352
|
+
self._error = e
|
|
353
|
+
finally:
|
|
354
|
+
self._done.set()
|
|
355
|
+
|
|
356
|
+
def join(self, timeout=None):
|
|
357
|
+
"""Block until task completes. Returns result or raises."""
|
|
358
|
+
self._done.wait(timeout)
|
|
359
|
+
if self._error:
|
|
360
|
+
raise self._error
|
|
361
|
+
return self._result
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def done(self):
|
|
365
|
+
return self._done.is_set()
|
|
366
|
+
|
|
367
|
+
def __repr__(self):
|
|
368
|
+
return f"<Task {'done' if self.done else 'running'}>"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class InScriptChannel:
|
|
372
|
+
"""
|
|
373
|
+
v2.3.0: `channel<T>(capacity)` — typed message-passing queue.
|
|
374
|
+
.send(v) — put value (blocks if full, capacity=0 means unbounded)
|
|
375
|
+
.recv() — get value (blocks until available)
|
|
376
|
+
.try_recv() — get value or nil (non-blocking)
|
|
377
|
+
.close() — mark channel closed
|
|
378
|
+
.closed — bool
|
|
379
|
+
"""
|
|
380
|
+
def __init__(self, capacity: int = 0):
|
|
381
|
+
self._q = _queue.Queue(maxsize=capacity)
|
|
382
|
+
self._closed = False
|
|
383
|
+
|
|
384
|
+
def send(self, value):
|
|
385
|
+
if self._closed:
|
|
386
|
+
raise RuntimeError("send on closed channel")
|
|
387
|
+
self._q.put(value)
|
|
388
|
+
|
|
389
|
+
def recv(self):
|
|
390
|
+
return self._q.get()
|
|
391
|
+
|
|
392
|
+
def try_recv(self):
|
|
393
|
+
try:
|
|
394
|
+
return self._q.get_nowait()
|
|
395
|
+
except _queue.Empty:
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
def close(self):
|
|
399
|
+
self._closed = True
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def closed(self):
|
|
403
|
+
return self._closed
|
|
404
|
+
|
|
405
|
+
def __repr__(self):
|
|
406
|
+
return f"<Channel size={self._q.qsize()} closed={self._closed}>"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class InScriptMutex:
|
|
410
|
+
"""
|
|
411
|
+
v2.3.0: `mutex(value)` — thread-safe value wrapper.
|
|
412
|
+
.lock(fn) — call fn(value) under lock, returns fn's result
|
|
413
|
+
.value — read current value (unsafe, prefer lock())
|
|
414
|
+
.set(v) — set value under lock
|
|
415
|
+
"""
|
|
416
|
+
def __init__(self, value):
|
|
417
|
+
self._value = value
|
|
418
|
+
self._lock = _threading.Lock()
|
|
419
|
+
|
|
420
|
+
def lock(self, fn):
|
|
421
|
+
with self._lock:
|
|
422
|
+
result = fn([self._value]) if callable(fn) else None
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
def set(self, value):
|
|
426
|
+
with self._lock:
|
|
427
|
+
self._value = value
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def value(self):
|
|
431
|
+
return self._value
|
|
432
|
+
|
|
433
|
+
def __repr__(self):
|
|
434
|
+
return f"<Mutex value={self._value!r}>"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class InScriptRWLock:
|
|
438
|
+
"""
|
|
439
|
+
v2.3.0: `rwlock(value)` — reader/writer lock.
|
|
440
|
+
.read(fn) — call fn(value) under read lock
|
|
441
|
+
.write(fn) — call fn(value) under write lock, fn may return new value
|
|
442
|
+
"""
|
|
443
|
+
def __init__(self, value):
|
|
444
|
+
self._value = value
|
|
445
|
+
self._lock = _threading.RLock() # simplified: use reentrant lock
|
|
446
|
+
|
|
447
|
+
def read(self, fn):
|
|
448
|
+
with self._lock:
|
|
449
|
+
return fn([self._value]) if callable(fn) else self._value
|
|
450
|
+
|
|
451
|
+
def write(self, fn):
|
|
452
|
+
with self._lock:
|
|
453
|
+
result = fn([self._value]) if callable(fn) else None
|
|
454
|
+
if result is not None:
|
|
455
|
+
self._value = result
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def value(self):
|
|
460
|
+
return self._value
|
|
461
|
+
|
|
462
|
+
def __repr__(self):
|
|
463
|
+
return f"<RWLock value={self._value!r}>"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class InScriptTimerHandle:
|
|
467
|
+
"""Handle returned by timer.after / timer.every for cancellation."""
|
|
468
|
+
def __init__(self, timer_obj):
|
|
469
|
+
self._timer = timer_obj # threading.Timer or periodic wrapper
|
|
470
|
+
|
|
471
|
+
def cancel(self):
|
|
472
|
+
if hasattr(self._timer, 'cancel'):
|
|
473
|
+
self._timer.cancel()
|
|
474
|
+
|
|
475
|
+
def __repr__(self):
|
|
476
|
+
return "<TimerHandle>"
|
|
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
|
|
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
|