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.
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/PKG-INFO +1 -1
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/analyzer.py +305 -2
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript.py +459 -4
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/pyproject.toml +1 -1
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/repl.py +1 -1
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/README.md +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/ast_nodes.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/compiler.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/environment.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/errors.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_fmt.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/inscript_test.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/interpreter.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/lexer.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/parser.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/pygame_backend.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/setup.cfg +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/setup.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_extended.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_game.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/stdlib_values.py +0 -0
- {inscript_lang-1.8.3 → inscript_lang-1.9.2}/vm.py +0 -0
|
@@ -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
|
-
|
|
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.
|
|
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="
|
|
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
|
|
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,
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "1.
|
|
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.
|
|
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
|
|
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
|