inscript-lang 1.8.4__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.4 → inscript_lang-1.9.2}/PKG-INFO +1 -1
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/analyzer.py +14 -2
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript.py +459 -4
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/pyproject.toml +1 -1
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/repl.py +1 -1
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/README.md +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/ast_nodes.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/compiler.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/environment.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/errors.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_fmt.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/inscript_test.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/interpreter.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/lexer.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/parser.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/pygame_backend.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/setup.cfg +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/setup.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/stdlib.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/stdlib_extended.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/stdlib_game.py +0 -0
- {inscript_lang-1.8.4 → inscript_lang-1.9.2}/stdlib_values.py +0 -0
- {inscript_lang-1.8.4 → 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:
|
|
@@ -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
|