inscript-lang 1.8.2__tar.gz → 1.8.3__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.2 → inscript_lang-1.8.3}/PKG-INFO +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/analyzer.py +96 -26
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript.py +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/pyproject.toml +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/repl.py +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/README.md +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/ast_nodes.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/compiler.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/environment.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/errors.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_fmt.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/inscript_test.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/interpreter.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/lexer.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/parser.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/pygame_backend.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/setup.cfg +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/setup.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/stdlib.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/stdlib_extended.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/stdlib_game.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/stdlib_values.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.3}/vm.py +0 -0
|
@@ -64,10 +64,13 @@ T_TRANSFORM2D = InScriptType("Transform2D")
|
|
|
64
64
|
T_TRANSFORM3D = InScriptType("Transform3D")
|
|
65
65
|
T_TEXTURE = InScriptType("Texture")
|
|
66
66
|
|
|
67
|
+
T_NEVER = InScriptType("never") # v1.8.3: bottom type — function never returns normally
|
|
68
|
+
|
|
67
69
|
BUILTIN_TYPES: Dict[str, InScriptType] = {
|
|
68
70
|
# canonical names
|
|
69
71
|
"int": T_INT, "float": T_FLOAT, "bool": T_BOOL,
|
|
70
72
|
"string": T_STRING, "void": T_VOID, "null": T_NULL, "any": T_ANY,
|
|
73
|
+
"never": T_NEVER, # v1.8.3
|
|
71
74
|
# common aliases
|
|
72
75
|
"str": T_STRING, "boolean": T_BOOL, "number": T_FLOAT,
|
|
73
76
|
"nil": T_NULL, "object": T_ANY, "auto": T_ANY,
|
|
@@ -141,6 +144,8 @@ def types_compatible(expected: InScriptType, got: InScriptType) -> bool:
|
|
|
141
144
|
"""True if `got` can be used where `expected` is required."""
|
|
142
145
|
if expected == T_ANY or got == T_ANY: return True
|
|
143
146
|
if expected == got: return True
|
|
147
|
+
# v1.8.3: `never` is the bottom type — compatible with any expected type
|
|
148
|
+
if got == T_NEVER: return True
|
|
144
149
|
# int can widen to float
|
|
145
150
|
if expected == T_FLOAT and got == T_INT: return True
|
|
146
151
|
# null/nil can be assigned to any nullable or union containing nil
|
|
@@ -266,7 +271,8 @@ class Analyzer(Visitor):
|
|
|
266
271
|
self._in_scene: bool = False
|
|
267
272
|
self._in_match_arm: bool = False
|
|
268
273
|
self._struct_defs: Dict[str, StructDecl] = {}
|
|
269
|
-
self._type_aliases: Dict[str, "InScriptType"] = {} # v1.8.2
|
|
274
|
+
self._type_aliases: Dict[str, "InScriptType"] = {} # v1.8.2
|
|
275
|
+
self._interfaces: Dict[str, dict] = {} # v1.8.3: name → {method: (params, ret)}
|
|
270
276
|
|
|
271
277
|
# Pre-register built-in global functions
|
|
272
278
|
self._register_builtins()
|
|
@@ -521,8 +527,8 @@ class Analyzer(Visitor):
|
|
|
521
527
|
|
|
522
528
|
def _hoist_top_level(self, program: Program):
|
|
523
529
|
"""Register structs, scenes, top-level functions, and type aliases before checking bodies."""
|
|
524
|
-
from ast_nodes import TypeAliasDecl
|
|
525
|
-
# First sub-pass: register type aliases and
|
|
530
|
+
from ast_nodes import TypeAliasDecl, InterfaceDecl
|
|
531
|
+
# First sub-pass: register type aliases, structs, and interfaces
|
|
526
532
|
for node in program.body:
|
|
527
533
|
if isinstance(node, StructDecl):
|
|
528
534
|
self._struct_defs[node.name] = node
|
|
@@ -532,7 +538,6 @@ class Analyzer(Visitor):
|
|
|
532
538
|
line=node.line, col=node.col
|
|
533
539
|
)
|
|
534
540
|
elif isinstance(node, TypeAliasDecl):
|
|
535
|
-
# v1.8.2: register alias immediately so functions can use it
|
|
536
541
|
type_ann = getattr(node, 'type_ann', None)
|
|
537
542
|
if type_ann is not None:
|
|
538
543
|
resolved = self._resolve_type_ann(type_ann)
|
|
@@ -543,6 +548,19 @@ class Analyzer(Visitor):
|
|
|
543
548
|
node.name, resolved, kind="type",
|
|
544
549
|
line=node.line, col=node.col
|
|
545
550
|
)
|
|
551
|
+
elif isinstance(node, InterfaceDecl):
|
|
552
|
+
# v1.8.3: register interface so structs can reference it
|
|
553
|
+
iface_methods = {}
|
|
554
|
+
for m in getattr(node, 'methods', []):
|
|
555
|
+
ret = self._resolve_type_ann(getattr(m, 'return_type', None))
|
|
556
|
+
params = [self._resolve_type_ann(getattr(p, 'type_ann', None))
|
|
557
|
+
for p in getattr(m, 'params', [])]
|
|
558
|
+
iface_methods[m.name] = (params, ret)
|
|
559
|
+
self._interfaces[node.name] = iface_methods
|
|
560
|
+
self._scope.symbols[node.name] = Symbol(
|
|
561
|
+
node.name, T_ANY, kind="interface",
|
|
562
|
+
line=node.line, col=node.col
|
|
563
|
+
)
|
|
546
564
|
# Second sub-pass: hoist functions, scenes, enums (can now reference aliases)
|
|
547
565
|
for node in program.body:
|
|
548
566
|
if isinstance(node, FunctionDecl):
|
|
@@ -637,12 +655,20 @@ class Analyzer(Visitor):
|
|
|
637
655
|
ret_type.name not in ("void", "nil", "any", "") and
|
|
638
656
|
not node.is_native if hasattr(node, 'is_native') else True):
|
|
639
657
|
if node.body and not self._body_always_returns(node.body):
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
658
|
+
if ret_type == T_NEVER:
|
|
659
|
+
# v1.8.3: `-> never` functions MUST always throw / diverge
|
|
660
|
+
self._error(
|
|
661
|
+
f"Function '{node.name}' is declared '-> never' "
|
|
662
|
+
f"but not all code paths throw or diverge",
|
|
663
|
+
node.line
|
|
664
|
+
)
|
|
665
|
+
else:
|
|
666
|
+
self._warn(
|
|
667
|
+
"missing-return",
|
|
668
|
+
f"Function '{node.name}' declares return type '{ret_type.name}' "
|
|
669
|
+
f"but not all code paths return a value",
|
|
670
|
+
node.line
|
|
671
|
+
)
|
|
646
672
|
|
|
647
673
|
# Analyze body in a new scope
|
|
648
674
|
self._push_scope("fn")
|
|
@@ -713,6 +739,26 @@ class Analyzer(Visitor):
|
|
|
713
739
|
for method in node.methods:
|
|
714
740
|
self.visit_FunctionDecl(method)
|
|
715
741
|
|
|
742
|
+
# v1.8.3: interface enforcement — check every declared interface is satisfied
|
|
743
|
+
struct_method_names = {m.name for m in node.methods}
|
|
744
|
+
# Also include methods from static_methods
|
|
745
|
+
struct_method_names |= {m.name for m in (node.static_methods or [])}
|
|
746
|
+
for iface_name in (node.interfaces or []):
|
|
747
|
+
required = self._interfaces.get(iface_name)
|
|
748
|
+
if required is None:
|
|
749
|
+
self._error(
|
|
750
|
+
f"Struct '{node.name}' implements unknown interface '{iface_name}'",
|
|
751
|
+
node.line
|
|
752
|
+
)
|
|
753
|
+
continue
|
|
754
|
+
missing_methods = sorted(required.keys() - struct_method_names)
|
|
755
|
+
if missing_methods:
|
|
756
|
+
self._error(
|
|
757
|
+
f"Struct '{node.name}' implements '{iface_name}' "
|
|
758
|
+
f"but is missing method(s): {missing_methods}",
|
|
759
|
+
node.line
|
|
760
|
+
)
|
|
761
|
+
|
|
716
762
|
self._pop_scope()
|
|
717
763
|
return struct_type
|
|
718
764
|
|
|
@@ -757,6 +803,21 @@ class Analyzer(Visitor):
|
|
|
757
803
|
)
|
|
758
804
|
return enum_type
|
|
759
805
|
|
|
806
|
+
def visit_InterfaceDecl(self, node) -> InScriptType:
|
|
807
|
+
"""v1.8.3: Register interface method signatures for implementation checking."""
|
|
808
|
+
iface_methods = {}
|
|
809
|
+
for m in getattr(node, 'methods', []):
|
|
810
|
+
ret = self._resolve_type_ann(getattr(m, 'return_type', None))
|
|
811
|
+
params = [self._resolve_type_ann(getattr(p, 'type_ann', None))
|
|
812
|
+
for p in getattr(m, 'params', [])]
|
|
813
|
+
iface_methods[m.name] = (params, ret)
|
|
814
|
+
self._interfaces[node.name] = iface_methods
|
|
815
|
+
self._scope.symbols[node.name] = Symbol(
|
|
816
|
+
node.name, T_ANY, kind="interface",
|
|
817
|
+
line=node.line, col=node.col
|
|
818
|
+
)
|
|
819
|
+
return T_VOID
|
|
820
|
+
|
|
760
821
|
def visit_TypeAliasDecl(self, node) -> InScriptType:
|
|
761
822
|
"""v1.8.2: Register a type alias so annotations can reference it by name."""
|
|
762
823
|
if hasattr(node, 'type_ann') and node.type_ann is not None:
|
|
@@ -965,47 +1026,56 @@ class Analyzer(Visitor):
|
|
|
965
1026
|
|
|
966
1027
|
def _check_match_exhaustiveness(self, node: MatchStmt) -> None:
|
|
967
1028
|
"""
|
|
968
|
-
|
|
969
|
-
|
|
1029
|
+
v1.8.3: Enum exhaustiveness is now a SemanticError (not a warning).
|
|
1030
|
+
If the match subject is a variable typed as a known enum and no wildcard
|
|
1031
|
+
is present, every variant must be explicitly covered.
|
|
970
1032
|
"""
|
|
971
|
-
subject
|
|
1033
|
+
subject = node.subject
|
|
972
1034
|
enum_name = None
|
|
973
1035
|
|
|
974
1036
|
if isinstance(subject, IdentExpr):
|
|
975
1037
|
sym = self._scope.lookup(subject.name)
|
|
976
|
-
if sym and sym.type_
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1038
|
+
if sym and sym.type_:
|
|
1039
|
+
t = sym.type_
|
|
1040
|
+
# Resolve via the full scope chain, not just current scope
|
|
1041
|
+
cand = self._scope.lookup(t.name)
|
|
1042
|
+
if cand and cand.kind == "enum":
|
|
1043
|
+
enum_name = t.name
|
|
980
1044
|
|
|
981
1045
|
if enum_name is None:
|
|
982
|
-
return #
|
|
1046
|
+
return # subject is not a typed enum variable — skip
|
|
983
1047
|
|
|
984
|
-
enum_sym = self._scope.
|
|
1048
|
+
enum_sym = self._scope.lookup(enum_name)
|
|
985
1049
|
if not enum_sym or enum_sym.fn_node is None:
|
|
986
1050
|
return
|
|
987
|
-
enum_decl
|
|
1051
|
+
enum_decl = enum_sym.fn_node
|
|
988
1052
|
all_variants = {v.name for v in getattr(enum_decl, 'variants', [])}
|
|
989
1053
|
if not all_variants:
|
|
990
1054
|
return
|
|
991
1055
|
|
|
992
|
-
# Collect explicitly covered variants
|
|
1056
|
+
# Collect explicitly covered variants
|
|
1057
|
+
# `case Dir::Left` → NamespaceAccessExpr(namespace="Dir", member="Left")
|
|
1058
|
+
# `case Dir.Left` → GetAttrExpr(obj=IdentExpr("Dir"), attr="Left") (legacy)
|
|
993
1059
|
covered = set()
|
|
994
1060
|
for arm in node.arms:
|
|
995
1061
|
p = arm.pattern
|
|
996
1062
|
if p is None:
|
|
997
|
-
return # wildcard — exhaustive
|
|
998
|
-
|
|
1063
|
+
return # wildcard — exhaustive by definition
|
|
1064
|
+
# Primary form: Dir::Variant
|
|
1065
|
+
if (hasattr(p, 'namespace') and hasattr(p, 'member')
|
|
1066
|
+
and p.namespace == enum_name):
|
|
1067
|
+
covered.add(p.member)
|
|
1068
|
+
# Legacy form: Dir.Variant
|
|
1069
|
+
elif (isinstance(p, GetAttrExpr)
|
|
999
1070
|
and isinstance(p.obj, IdentExpr)
|
|
1000
1071
|
and p.obj.name == enum_name):
|
|
1001
1072
|
covered.add(p.attr)
|
|
1002
1073
|
|
|
1003
1074
|
missing = sorted(all_variants - covered)
|
|
1004
1075
|
if missing:
|
|
1005
|
-
self.
|
|
1006
|
-
"exhaustive_match",
|
|
1076
|
+
self._error(
|
|
1007
1077
|
f"Non-exhaustive match on '{enum_name}': "
|
|
1008
|
-
f"
|
|
1078
|
+
f"missing variant(s) {missing}. Add the missing cases or a wildcard arm 'case _'",
|
|
1009
1079
|
node.line
|
|
1010
1080
|
)
|
|
1011
1081
|
|
|
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
|
|
|
24
24
|
SemanticError, InScriptRuntimeError,
|
|
25
25
|
MultiError, InScriptWarning)
|
|
26
26
|
|
|
27
|
-
VERSION = "1.8.
|
|
27
|
+
VERSION = "1.8.3"
|
|
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"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "1.8.
|
|
7
|
+
version = "1.8.3"
|
|
8
8
|
description = "InScript — a game-focused scripting language with 59 game modules and a bytecode VM"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -40,7 +40,7 @@ sys.path.insert(0, str(Path(__file__).parent))
|
|
|
40
40
|
|
|
41
41
|
HISTORY_FILE = Path.home() / ".inscript" / "history"
|
|
42
42
|
HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
-
VERSION = "1.8.
|
|
43
|
+
VERSION = "1.8.3"
|
|
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
|