inscript-lang 1.8.2__tar.gz → 1.8.4__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.4}/PKG-INFO +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/analyzer.py +387 -26
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript.py +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/pyproject.toml +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/repl.py +1 -1
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/README.md +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/ast_nodes.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/compiler.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/environment.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/errors.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_fmt.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/inscript_test.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/interpreter.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/lexer.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/parser.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/pygame_backend.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/setup.cfg +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/setup.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_extended.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_game.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/stdlib_values.py +0 -0
- {inscript_lang-1.8.2 → inscript_lang-1.8.4}/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):
|
|
@@ -617,6 +635,17 @@ class Analyzer(Visitor):
|
|
|
617
635
|
def visit_FunctionDecl(self, node: FunctionDecl) -> InScriptType:
|
|
618
636
|
ret_type = self._resolve_type_ann(node.return_type)
|
|
619
637
|
|
|
638
|
+
# v1.8.4: return type inference — if no annotation and body has a single
|
|
639
|
+
# unambiguous return type, infer it automatically
|
|
640
|
+
if node.return_type is None and ret_type == T_ANY and node.body:
|
|
641
|
+
inferred = self._infer_fn_return_type(node)
|
|
642
|
+
if inferred not in (T_ANY, T_VOID, T_NULL):
|
|
643
|
+
ret_type = inferred
|
|
644
|
+
# Update the hoisted symbol's type to the inferred one
|
|
645
|
+
existing = self._scope.lookup(node.name)
|
|
646
|
+
if existing and existing.kind == "fn":
|
|
647
|
+
existing.type_ = ret_type
|
|
648
|
+
|
|
620
649
|
# Register in current scope (if not already hoisted)
|
|
621
650
|
if not self._scope.lookup_local(node.name):
|
|
622
651
|
self._define(Symbol(
|
|
@@ -637,12 +666,20 @@ class Analyzer(Visitor):
|
|
|
637
666
|
ret_type.name not in ("void", "nil", "any", "") and
|
|
638
667
|
not node.is_native if hasattr(node, 'is_native') else True):
|
|
639
668
|
if node.body and not self._body_always_returns(node.body):
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
669
|
+
if ret_type == T_NEVER:
|
|
670
|
+
# v1.8.3: `-> never` functions MUST always throw / diverge
|
|
671
|
+
self._error(
|
|
672
|
+
f"Function '{node.name}' is declared '-> never' "
|
|
673
|
+
f"but not all code paths throw or diverge",
|
|
674
|
+
node.line
|
|
675
|
+
)
|
|
676
|
+
else:
|
|
677
|
+
self._warn(
|
|
678
|
+
"missing-return",
|
|
679
|
+
f"Function '{node.name}' declares return type '{ret_type.name}' "
|
|
680
|
+
f"but not all code paths return a value",
|
|
681
|
+
node.line
|
|
682
|
+
)
|
|
646
683
|
|
|
647
684
|
# Analyze body in a new scope
|
|
648
685
|
self._push_scope("fn")
|
|
@@ -713,6 +750,26 @@ class Analyzer(Visitor):
|
|
|
713
750
|
for method in node.methods:
|
|
714
751
|
self.visit_FunctionDecl(method)
|
|
715
752
|
|
|
753
|
+
# v1.8.3: interface enforcement — check every declared interface is satisfied
|
|
754
|
+
struct_method_names = {m.name for m in node.methods}
|
|
755
|
+
# Also include methods from static_methods
|
|
756
|
+
struct_method_names |= {m.name for m in (node.static_methods or [])}
|
|
757
|
+
for iface_name in (node.interfaces or []):
|
|
758
|
+
required = self._interfaces.get(iface_name)
|
|
759
|
+
if required is None:
|
|
760
|
+
self._error(
|
|
761
|
+
f"Struct '{node.name}' implements unknown interface '{iface_name}'",
|
|
762
|
+
node.line
|
|
763
|
+
)
|
|
764
|
+
continue
|
|
765
|
+
missing_methods = sorted(required.keys() - struct_method_names)
|
|
766
|
+
if missing_methods:
|
|
767
|
+
self._error(
|
|
768
|
+
f"Struct '{node.name}' implements '{iface_name}' "
|
|
769
|
+
f"but is missing method(s): {missing_methods}",
|
|
770
|
+
node.line
|
|
771
|
+
)
|
|
772
|
+
|
|
716
773
|
self._pop_scope()
|
|
717
774
|
return struct_type
|
|
718
775
|
|
|
@@ -757,6 +814,21 @@ class Analyzer(Visitor):
|
|
|
757
814
|
)
|
|
758
815
|
return enum_type
|
|
759
816
|
|
|
817
|
+
def visit_InterfaceDecl(self, node) -> InScriptType:
|
|
818
|
+
"""v1.8.3: Register interface method signatures for implementation checking."""
|
|
819
|
+
iface_methods = {}
|
|
820
|
+
for m in getattr(node, 'methods', []):
|
|
821
|
+
ret = self._resolve_type_ann(getattr(m, 'return_type', None))
|
|
822
|
+
params = [self._resolve_type_ann(getattr(p, 'type_ann', None))
|
|
823
|
+
for p in getattr(m, 'params', [])]
|
|
824
|
+
iface_methods[m.name] = (params, ret)
|
|
825
|
+
self._interfaces[node.name] = iface_methods
|
|
826
|
+
self._scope.symbols[node.name] = Symbol(
|
|
827
|
+
node.name, T_ANY, kind="interface",
|
|
828
|
+
line=node.line, col=node.col
|
|
829
|
+
)
|
|
830
|
+
return T_VOID
|
|
831
|
+
|
|
760
832
|
def visit_TypeAliasDecl(self, node) -> InScriptType:
|
|
761
833
|
"""v1.8.2: Register a type alias so annotations can reference it by name."""
|
|
762
834
|
if hasattr(node, 'type_ann') and node.type_ann is not None:
|
|
@@ -965,47 +1037,56 @@ class Analyzer(Visitor):
|
|
|
965
1037
|
|
|
966
1038
|
def _check_match_exhaustiveness(self, node: MatchStmt) -> None:
|
|
967
1039
|
"""
|
|
968
|
-
|
|
969
|
-
|
|
1040
|
+
v1.8.3: Enum exhaustiveness is now a SemanticError (not a warning).
|
|
1041
|
+
If the match subject is a variable typed as a known enum and no wildcard
|
|
1042
|
+
is present, every variant must be explicitly covered.
|
|
970
1043
|
"""
|
|
971
|
-
subject
|
|
1044
|
+
subject = node.subject
|
|
972
1045
|
enum_name = None
|
|
973
1046
|
|
|
974
1047
|
if isinstance(subject, IdentExpr):
|
|
975
1048
|
sym = self._scope.lookup(subject.name)
|
|
976
|
-
if sym and sym.type_
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1049
|
+
if sym and sym.type_:
|
|
1050
|
+
t = sym.type_
|
|
1051
|
+
# Resolve via the full scope chain, not just current scope
|
|
1052
|
+
cand = self._scope.lookup(t.name)
|
|
1053
|
+
if cand and cand.kind == "enum":
|
|
1054
|
+
enum_name = t.name
|
|
980
1055
|
|
|
981
1056
|
if enum_name is None:
|
|
982
|
-
return #
|
|
1057
|
+
return # subject is not a typed enum variable — skip
|
|
983
1058
|
|
|
984
|
-
enum_sym = self._scope.
|
|
1059
|
+
enum_sym = self._scope.lookup(enum_name)
|
|
985
1060
|
if not enum_sym or enum_sym.fn_node is None:
|
|
986
1061
|
return
|
|
987
|
-
enum_decl
|
|
1062
|
+
enum_decl = enum_sym.fn_node
|
|
988
1063
|
all_variants = {v.name for v in getattr(enum_decl, 'variants', [])}
|
|
989
1064
|
if not all_variants:
|
|
990
1065
|
return
|
|
991
1066
|
|
|
992
|
-
# Collect explicitly covered variants
|
|
1067
|
+
# Collect explicitly covered variants
|
|
1068
|
+
# `case Dir::Left` → NamespaceAccessExpr(namespace="Dir", member="Left")
|
|
1069
|
+
# `case Dir.Left` → GetAttrExpr(obj=IdentExpr("Dir"), attr="Left") (legacy)
|
|
993
1070
|
covered = set()
|
|
994
1071
|
for arm in node.arms:
|
|
995
1072
|
p = arm.pattern
|
|
996
1073
|
if p is None:
|
|
997
|
-
return # wildcard — exhaustive
|
|
998
|
-
|
|
1074
|
+
return # wildcard — exhaustive by definition
|
|
1075
|
+
# Primary form: Dir::Variant
|
|
1076
|
+
if (hasattr(p, 'namespace') and hasattr(p, 'member')
|
|
1077
|
+
and p.namespace == enum_name):
|
|
1078
|
+
covered.add(p.member)
|
|
1079
|
+
# Legacy form: Dir.Variant
|
|
1080
|
+
elif (isinstance(p, GetAttrExpr)
|
|
999
1081
|
and isinstance(p.obj, IdentExpr)
|
|
1000
1082
|
and p.obj.name == enum_name):
|
|
1001
1083
|
covered.add(p.attr)
|
|
1002
1084
|
|
|
1003
1085
|
missing = sorted(all_variants - covered)
|
|
1004
1086
|
if missing:
|
|
1005
|
-
self.
|
|
1006
|
-
"exhaustive_match",
|
|
1087
|
+
self._error(
|
|
1007
1088
|
f"Non-exhaustive match on '{enum_name}': "
|
|
1008
|
-
f"
|
|
1089
|
+
f"missing variant(s) {missing}. Add the missing cases or a wildcard arm 'case _'",
|
|
1009
1090
|
node.line
|
|
1010
1091
|
)
|
|
1011
1092
|
|
|
@@ -1220,8 +1301,288 @@ class Analyzer(Visitor):
|
|
|
1220
1301
|
return sym.type_
|
|
1221
1302
|
|
|
1222
1303
|
if isinstance(node.callee, GetAttrExpr):
|
|
1304
|
+
# v1.8.4: infer return types of array method chains
|
|
1305
|
+
obj_type = self.visit(node.callee.obj)
|
|
1306
|
+
# v1.8.2 literal_type("hello") → dispatch as T_STRING
|
|
1307
|
+
if is_literal_type(obj_type):
|
|
1308
|
+
obj_type = T_STRING
|
|
1309
|
+
method = node.callee.attr
|
|
1310
|
+
return self._infer_method_call_type(obj_type, method, node)
|
|
1311
|
+
|
|
1312
|
+
return T_ANY
|
|
1313
|
+
|
|
1314
|
+
def _infer_method_call_type(self, obj_type: InScriptType,
|
|
1315
|
+
method: str, call_node) -> InScriptType:
|
|
1316
|
+
"""
|
|
1317
|
+
v1.8.4: Infer the return type of a method call based on the receiver's type.
|
|
1318
|
+
|
|
1319
|
+
Array methods:
|
|
1320
|
+
.map(fn) → Array<T> where T = inferred return type of fn
|
|
1321
|
+
.filter(fn) → Array<elem> (same element type, just fewer items)
|
|
1322
|
+
.flatMap(fn) → Array<T>
|
|
1323
|
+
.take(n) → Array<elem>
|
|
1324
|
+
.skip(n) → Array<elem>
|
|
1325
|
+
.slice(a,b) → Array<elem>
|
|
1326
|
+
.sorted() → Array<elem>
|
|
1327
|
+
.reversed() → Array<elem>
|
|
1328
|
+
.unique() → Array<elem>
|
|
1329
|
+
.len() → int
|
|
1330
|
+
.count() → int
|
|
1331
|
+
.sum() → int | float
|
|
1332
|
+
.first() → elem? (optional)
|
|
1333
|
+
.last() → elem?
|
|
1334
|
+
.find(fn) → elem?
|
|
1335
|
+
.any(fn) → bool
|
|
1336
|
+
.all(fn) → bool
|
|
1337
|
+
.reduce(fn,i) → any (can't infer accumulator type without annotation)
|
|
1338
|
+
.join(sep) → string
|
|
1339
|
+
.push(v) → void
|
|
1340
|
+
.pop() → elem?
|
|
1341
|
+
.contains(v) → bool
|
|
1342
|
+
.index_of(v) → int?
|
|
1343
|
+
|
|
1344
|
+
String methods:
|
|
1345
|
+
.split(sep) → Array<string>
|
|
1346
|
+
.trim() → string
|
|
1347
|
+
.upper() → string
|
|
1348
|
+
.lower() → string
|
|
1349
|
+
.replace(...) → string
|
|
1350
|
+
.starts_with()→ bool
|
|
1351
|
+
.ends_with() → bool
|
|
1352
|
+
.contains() → bool
|
|
1353
|
+
.len() → int
|
|
1354
|
+
.chars() → Array<string>
|
|
1355
|
+
"""
|
|
1356
|
+
# ── Array method inference ────────────────────────────────────────────
|
|
1357
|
+
if obj_type.name == "Array":
|
|
1358
|
+
elem = obj_type.params[0] if obj_type.params else T_ANY
|
|
1359
|
+
|
|
1360
|
+
# Methods that return Array<same-elem>
|
|
1361
|
+
if method in ("filter", "take", "skip", "slice", "sorted",
|
|
1362
|
+
"reversed", "unique", "shuffle", "concat"):
|
|
1363
|
+
return array_type(elem)
|
|
1364
|
+
|
|
1365
|
+
# .map(fn) / .flatMap(fn) → infer fn return type
|
|
1366
|
+
if method in ("map", "flatMap"):
|
|
1367
|
+
fn_ret = self._infer_fn_arg_return(call_node, arg_index=0, param_type=elem)
|
|
1368
|
+
inner = fn_ret if fn_ret != T_ANY else T_ANY
|
|
1369
|
+
if method == "flatMap" and inner.name == "Array":
|
|
1370
|
+
inner = inner.params[0] if inner.params else T_ANY
|
|
1371
|
+
return array_type(inner)
|
|
1372
|
+
|
|
1373
|
+
# Scalar reductions
|
|
1374
|
+
if method in ("len", "count", "index_of"):
|
|
1375
|
+
return T_INT
|
|
1376
|
+
if method == "sum":
|
|
1377
|
+
return elem if elem in (T_INT, T_FLOAT) else T_INT
|
|
1378
|
+
if method in ("any", "all", "contains", "is_empty"):
|
|
1379
|
+
return T_BOOL
|
|
1380
|
+
if method == "join":
|
|
1381
|
+
return T_STRING
|
|
1382
|
+
if method in ("push", "append", "extend", "clear", "insert",
|
|
1383
|
+
"remove", "remove_at"):
|
|
1384
|
+
return T_VOID
|
|
1385
|
+
|
|
1386
|
+
# Optional-returning methods
|
|
1387
|
+
if method in ("first", "last", "pop", "find", "get"):
|
|
1388
|
+
return optional_type(elem)
|
|
1389
|
+
|
|
1390
|
+
# .reduce → T_ANY (accumulator type unknown without annotation)
|
|
1391
|
+
if method == "reduce":
|
|
1392
|
+
return T_ANY
|
|
1393
|
+
|
|
1394
|
+
# .each(fn) → void (side-effect traversal)
|
|
1395
|
+
if method in ("each", "for_each"):
|
|
1396
|
+
return T_VOID
|
|
1397
|
+
|
|
1398
|
+
# ── String method inference ───────────────────────────────────────────
|
|
1399
|
+
if obj_type == T_STRING:
|
|
1400
|
+
if method in ("trim", "upper", "lower", "replace", "strip",
|
|
1401
|
+
"lstrip", "rstrip", "pad_left", "pad_right",
|
|
1402
|
+
"repeat", "reverse", "slice"):
|
|
1403
|
+
return T_STRING
|
|
1404
|
+
if method in ("len", "count", "index_of", "find"):
|
|
1405
|
+
return T_INT
|
|
1406
|
+
if method in ("starts_with", "ends_with", "contains", "is_empty",
|
|
1407
|
+
"matches"):
|
|
1408
|
+
return T_BOOL
|
|
1409
|
+
if method == "split":
|
|
1410
|
+
return array_type(T_STRING)
|
|
1411
|
+
if method == "chars":
|
|
1412
|
+
return array_type(T_STRING)
|
|
1413
|
+
if method == "to_int":
|
|
1414
|
+
return optional_type(T_INT)
|
|
1415
|
+
if method == "to_float":
|
|
1416
|
+
return optional_type(T_FLOAT)
|
|
1417
|
+
|
|
1418
|
+
# ── Int / Float methods ───────────────────────────────────────────────
|
|
1419
|
+
if obj_type in (T_INT, T_FLOAT):
|
|
1420
|
+
if method in ("abs", "floor", "ceil", "round", "sqrt",
|
|
1421
|
+
"clamp", "min", "max"):
|
|
1422
|
+
return obj_type
|
|
1423
|
+
if method == "to_string":
|
|
1424
|
+
return T_STRING
|
|
1425
|
+
|
|
1426
|
+
# ── Struct method return type ─────────────────────────────────────────
|
|
1427
|
+
if obj_type.name in self._struct_defs:
|
|
1428
|
+
struct = self._struct_defs[obj_type.name]
|
|
1429
|
+
for m in (struct.methods or []):
|
|
1430
|
+
if m.name == method:
|
|
1431
|
+
return self._resolve_type_ann(m.return_type)
|
|
1432
|
+
|
|
1433
|
+
return T_ANY
|
|
1434
|
+
|
|
1435
|
+
def _infer_fn_return_type(self, node) -> InScriptType:
|
|
1436
|
+
"""
|
|
1437
|
+
v1.8.4: Infer a function's return type from its body when no annotation exists.
|
|
1438
|
+
Only infers when ALL return paths agree on the same concrete type.
|
|
1439
|
+
Falls back to T_ANY if ambiguous.
|
|
1440
|
+
NOTE: errors are suppressed — inference is best-effort, never raises.
|
|
1441
|
+
"""
|
|
1442
|
+
from ast_nodes import ReturnStmt, BlockStmt
|
|
1443
|
+
|
|
1444
|
+
if not node.body:
|
|
1445
|
+
return T_VOID
|
|
1446
|
+
|
|
1447
|
+
# Stash and suppress the real error list — inference must never pollute it
|
|
1448
|
+
saved_errors = self._errors
|
|
1449
|
+
self._errors = []
|
|
1450
|
+
|
|
1451
|
+
try:
|
|
1452
|
+
# Push a temporary scope with params typed per their annotations
|
|
1453
|
+
self._push_scope("infer-fn")
|
|
1454
|
+
for p in (node.params or []):
|
|
1455
|
+
p_type = self._resolve_type_ann(getattr(p, 'type_ann', None))
|
|
1456
|
+
pname = getattr(p, 'name', None)
|
|
1457
|
+
if pname:
|
|
1458
|
+
self._scope.symbols[pname] = Symbol(
|
|
1459
|
+
pname, p_type, kind="var",
|
|
1460
|
+
line=getattr(p, 'line', 0), col=getattr(p, 'col', 0)
|
|
1461
|
+
)
|
|
1462
|
+
self._scope.symbols[pname].used = True
|
|
1463
|
+
|
|
1464
|
+
# Collect return types from first-level stmts + if/else branches
|
|
1465
|
+
types_seen = set()
|
|
1466
|
+
stmts = node.body.body if isinstance(node.body, BlockStmt) else [node.body]
|
|
1467
|
+
for stmt in stmts:
|
|
1468
|
+
if isinstance(stmt, ReturnStmt) and stmt.value is not None:
|
|
1469
|
+
try:
|
|
1470
|
+
t = self.visit(stmt.value)
|
|
1471
|
+
if is_literal_type(t): t = T_STRING
|
|
1472
|
+
types_seen.add(t)
|
|
1473
|
+
except Exception:
|
|
1474
|
+
types_seen.add(T_ANY)
|
|
1475
|
+
for branch_attr in ('then_branch', 'else_branch'):
|
|
1476
|
+
branch = getattr(stmt, branch_attr, None)
|
|
1477
|
+
if branch is None:
|
|
1478
|
+
continue
|
|
1479
|
+
inner = getattr(branch, 'body', [branch])
|
|
1480
|
+
if not isinstance(inner, list):
|
|
1481
|
+
inner = [inner]
|
|
1482
|
+
for s in inner:
|
|
1483
|
+
if isinstance(s, ReturnStmt) and s.value is not None:
|
|
1484
|
+
try:
|
|
1485
|
+
t = self.visit(s.value)
|
|
1486
|
+
if is_literal_type(t): t = T_STRING
|
|
1487
|
+
types_seen.add(t)
|
|
1488
|
+
except Exception:
|
|
1489
|
+
types_seen.add(T_ANY)
|
|
1490
|
+
|
|
1491
|
+
self._pop_scope()
|
|
1492
|
+
|
|
1493
|
+
if not types_seen:
|
|
1494
|
+
return T_VOID
|
|
1495
|
+
non_any = {t for t in types_seen if t != T_ANY}
|
|
1496
|
+
if len(non_any) == 1:
|
|
1497
|
+
return next(iter(non_any))
|
|
1498
|
+
if non_any == {T_INT, T_FLOAT}:
|
|
1499
|
+
return T_FLOAT
|
|
1223
1500
|
return T_ANY
|
|
1224
1501
|
|
|
1502
|
+
except Exception:
|
|
1503
|
+
return T_ANY
|
|
1504
|
+
finally:
|
|
1505
|
+
# Always restore the real error list
|
|
1506
|
+
self._errors = saved_errors
|
|
1507
|
+
|
|
1508
|
+
def _infer_fn_arg_return(self, call_node, arg_index: int,
|
|
1509
|
+
param_type: InScriptType) -> InScriptType:
|
|
1510
|
+
"""
|
|
1511
|
+
v1.8.4: Given a CallExpr and an argument position that expects a fn,
|
|
1512
|
+
try to infer that fn's return type by visiting its body with a temporary
|
|
1513
|
+
scope that binds the param to param_type.
|
|
1514
|
+
Returns T_ANY if inference fails.
|
|
1515
|
+
"""
|
|
1516
|
+
from ast_nodes import FunctionDecl, LambdaExpr
|
|
1517
|
+
if arg_index >= len(call_node.args):
|
|
1518
|
+
return T_ANY
|
|
1519
|
+
fn_arg = call_node.args[arg_index].value
|
|
1520
|
+
|
|
1521
|
+
# Handle both lambda `fn(x) { return x * 2 }` and named refs
|
|
1522
|
+
params = None; body = None
|
|
1523
|
+
if isinstance(fn_arg, FunctionDecl) or isinstance(fn_arg, LambdaExpr):
|
|
1524
|
+
params = getattr(fn_arg, 'params', [])
|
|
1525
|
+
body = getattr(fn_arg, 'body', None)
|
|
1526
|
+
else:
|
|
1527
|
+
return T_ANY
|
|
1528
|
+
|
|
1529
|
+
# Stash and suppress the real error list
|
|
1530
|
+
saved_errors = self._errors
|
|
1531
|
+
self._errors = []
|
|
1532
|
+
|
|
1533
|
+
try:
|
|
1534
|
+
# Push a temporary scope, bind first param to elem type
|
|
1535
|
+
self._push_scope("infer")
|
|
1536
|
+
if params:
|
|
1537
|
+
p0 = params[0]
|
|
1538
|
+
p_name = getattr(p0, 'name', None)
|
|
1539
|
+
if p_name:
|
|
1540
|
+
self._scope.symbols[p_name] = Symbol(
|
|
1541
|
+
p_name, param_type, kind="var",
|
|
1542
|
+
line=getattr(p0, 'line', 0), col=getattr(p0, 'col', 0)
|
|
1543
|
+
)
|
|
1544
|
+
self._scope.symbols[p_name].used = True
|
|
1545
|
+
|
|
1546
|
+
inferred = T_ANY
|
|
1547
|
+
if body is not None:
|
|
1548
|
+
inferred = self._infer_block_return_type(body)
|
|
1549
|
+
|
|
1550
|
+
self._pop_scope()
|
|
1551
|
+
return inferred
|
|
1552
|
+
except Exception:
|
|
1553
|
+
return T_ANY
|
|
1554
|
+
finally:
|
|
1555
|
+
self._errors = saved_errors
|
|
1556
|
+
|
|
1557
|
+
def _infer_block_return_type(self, block) -> InScriptType:
|
|
1558
|
+
"""
|
|
1559
|
+
v1.8.4: Walk a block/expression to find the first return statement's type.
|
|
1560
|
+
Also handles single-expression lambdas.
|
|
1561
|
+
"""
|
|
1562
|
+
from ast_nodes import BlockStmt, ReturnStmt
|
|
1563
|
+
stmts = []
|
|
1564
|
+
if isinstance(block, BlockStmt):
|
|
1565
|
+
stmts = block.body
|
|
1566
|
+
elif hasattr(block, '__iter__'):
|
|
1567
|
+
stmts = list(block)
|
|
1568
|
+
else:
|
|
1569
|
+
# Single expression (arrow lambda)
|
|
1570
|
+
try:
|
|
1571
|
+
return self.visit(block)
|
|
1572
|
+
except Exception:
|
|
1573
|
+
return T_ANY
|
|
1574
|
+
|
|
1575
|
+
for stmt in stmts:
|
|
1576
|
+
if isinstance(stmt, ReturnStmt) and stmt.value is not None:
|
|
1577
|
+
try:
|
|
1578
|
+
return self.visit(stmt.value)
|
|
1579
|
+
except Exception:
|
|
1580
|
+
return T_ANY
|
|
1581
|
+
# Recurse into if/while bodies for early returns
|
|
1582
|
+
if hasattr(stmt, 'then_branch'):
|
|
1583
|
+
t = self._infer_block_return_type(stmt.then_branch)
|
|
1584
|
+
if t != T_ANY:
|
|
1585
|
+
return t
|
|
1225
1586
|
return T_ANY
|
|
1226
1587
|
|
|
1227
1588
|
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.8.
|
|
27
|
+
VERSION = "1.8.4"
|
|
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.4"
|
|
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.4"
|
|
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
|