inscript-lang 1.8.1__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.1 → inscript_lang-1.8.3}/PKG-INFO +1 -1
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/analyzer.py +177 -30
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/ast_nodes.py +9 -3
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript.py +1 -1
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/interpreter.py +7 -3
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/parser.py +40 -12
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/pyproject.toml +1 -1
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/repl.py +1 -1
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/README.md +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/compiler.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/environment.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/errors.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_fmt.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_test.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/lexer.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/pygame_backend.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/setup.cfg +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/setup.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_extended.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_game.py +0 -0
- {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_values.py +0 -0
- {inscript_lang-1.8.1 → 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,
|
|
@@ -112,6 +115,20 @@ def union_members(t: InScriptType) -> list:
|
|
|
112
115
|
"""v1.8.1: Return list of member types (works on non-unions too)."""
|
|
113
116
|
return t.params if t.name == "Union" else [t]
|
|
114
117
|
|
|
118
|
+
def literal_type(value: str) -> InScriptType:
|
|
119
|
+
"""v1.8.2: A string-literal type like `"left"` or `"right"`."""
|
|
120
|
+
return InScriptType("__literal__", [value]) # params[0] is the string value
|
|
121
|
+
|
|
122
|
+
def fn_type(param_types: list, return_type: InScriptType) -> InScriptType:
|
|
123
|
+
"""v1.8.2: A function type `fn(int,string)->bool`."""
|
|
124
|
+
return InScriptType("__fn__", param_types + [return_type])
|
|
125
|
+
|
|
126
|
+
def is_literal_type(t: InScriptType) -> bool:
|
|
127
|
+
return t.name == "__literal__"
|
|
128
|
+
|
|
129
|
+
def literal_value(t: InScriptType) -> str:
|
|
130
|
+
return t.params[0] if t.params else ""
|
|
131
|
+
|
|
115
132
|
def is_numeric(t: InScriptType) -> bool:
|
|
116
133
|
return t in (T_INT, T_FLOAT)
|
|
117
134
|
|
|
@@ -127,6 +144,8 @@ def types_compatible(expected: InScriptType, got: InScriptType) -> bool:
|
|
|
127
144
|
"""True if `got` can be used where `expected` is required."""
|
|
128
145
|
if expected == T_ANY or got == T_ANY: return True
|
|
129
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
|
|
130
149
|
# int can widen to float
|
|
131
150
|
if expected == T_FLOAT and got == T_INT: return True
|
|
132
151
|
# null/nil can be assigned to any nullable or union containing nil
|
|
@@ -140,6 +159,25 @@ def types_compatible(expected: InScriptType, got: InScriptType) -> bool:
|
|
|
140
159
|
# v1.8.1: if got is a Union, every member must be compatible with expected
|
|
141
160
|
if got.name == "Union":
|
|
142
161
|
return all(types_compatible(expected, m) for m in got.params)
|
|
162
|
+
# v1.8.2: string literal type — `"left"` is compatible with string
|
|
163
|
+
if is_literal_type(expected):
|
|
164
|
+
# Expected a specific literal: got must be the same literal
|
|
165
|
+
if is_literal_type(got):
|
|
166
|
+
return literal_value(expected) == literal_value(got)
|
|
167
|
+
# A plain string variable can fill a literal slot (runtime check)
|
|
168
|
+
return got == T_STRING
|
|
169
|
+
if is_literal_type(got):
|
|
170
|
+
# A literal can always fill a plain string slot
|
|
171
|
+
if expected == T_STRING: return True
|
|
172
|
+
# A literal can fill a Union<string, ...> slot
|
|
173
|
+
if expected.name == "Union":
|
|
174
|
+
return any(types_compatible(m, got) for m in expected.params)
|
|
175
|
+
return False
|
|
176
|
+
# v1.8.2: fn types — compatible if same signature, or got is T_ANY/Function (lambda)
|
|
177
|
+
if expected.name == "__fn__":
|
|
178
|
+
if got == T_ANY: return True
|
|
179
|
+
if got.name in ("Function", "__fn__"): return True
|
|
180
|
+
return False
|
|
143
181
|
# Array<X> compatible with Array<any> and vice versa
|
|
144
182
|
if expected.name == "Array" and got.name == "Array": return True
|
|
145
183
|
# Dict<K,V> compatible with Dict<any,any>
|
|
@@ -233,6 +271,8 @@ class Analyzer(Visitor):
|
|
|
233
271
|
self._in_scene: bool = False
|
|
234
272
|
self._in_match_arm: bool = False
|
|
235
273
|
self._struct_defs: Dict[str, StructDecl] = {}
|
|
274
|
+
self._type_aliases: Dict[str, "InScriptType"] = {} # v1.8.2
|
|
275
|
+
self._interfaces: Dict[str, dict] = {} # v1.8.3: name → {method: (params, ret)}
|
|
236
276
|
|
|
237
277
|
# Pre-register built-in global functions
|
|
238
278
|
self._register_builtins()
|
|
@@ -325,21 +365,33 @@ class Analyzer(Visitor):
|
|
|
325
365
|
v = self._resolve_type_ann(ann.generics[0]) if ann.generics else T_ANY
|
|
326
366
|
return dict_type(k, v)
|
|
327
367
|
|
|
328
|
-
# v1.8.1: `T?` — nullable shorthand
|
|
329
|
-
# is_nullable=True, generics=[T])
|
|
368
|
+
# v1.8.1: `T?` — nullable shorthand
|
|
330
369
|
if ann.is_nullable or ann.name == "Optional":
|
|
331
370
|
inner = self._resolve_type_ann(ann.generics[0]) if ann.generics else T_ANY
|
|
332
371
|
return optional_type(inner)
|
|
333
372
|
|
|
334
|
-
# v1.8.1: `A | B | C` — union type
|
|
335
|
-
# generics=[A, B, C])
|
|
373
|
+
# v1.8.1: `A | B | C` — union type
|
|
336
374
|
if ann.name == "Union":
|
|
337
375
|
members = [self._resolve_type_ann(g) for g in ann.generics]
|
|
338
376
|
return union_type(*members)
|
|
339
377
|
|
|
378
|
+
# v1.8.2: string literal type — `"left"`
|
|
379
|
+
if ann.name == "__literal__":
|
|
380
|
+
return literal_type(ann.literal_value or "")
|
|
381
|
+
|
|
382
|
+
# v1.8.2: fn type — `fn(int) -> bool`
|
|
383
|
+
if ann.name == "__fn__":
|
|
384
|
+
params = [self._resolve_type_ann(p) for p in (ann.fn_params or [])]
|
|
385
|
+
ret = self._resolve_type_ann(ann.fn_return) if ann.fn_return else T_ANY
|
|
386
|
+
return fn_type(params, ret)
|
|
387
|
+
|
|
340
388
|
if ann.name in BUILTIN_TYPES:
|
|
341
389
|
return BUILTIN_TYPES[ann.name]
|
|
342
390
|
|
|
391
|
+
# v1.8.2: registered type aliases
|
|
392
|
+
if ann.name in self._type_aliases:
|
|
393
|
+
return self._type_aliases[ann.name]
|
|
394
|
+
|
|
343
395
|
# Check user-defined structs
|
|
344
396
|
if ann.name in self._struct_defs:
|
|
345
397
|
return InScriptType(ann.name)
|
|
@@ -474,7 +526,9 @@ class Analyzer(Visitor):
|
|
|
474
526
|
return self._scope.symbols
|
|
475
527
|
|
|
476
528
|
def _hoist_top_level(self, program: Program):
|
|
477
|
-
"""Register structs, scenes,
|
|
529
|
+
"""Register structs, scenes, top-level functions, and type aliases before checking bodies."""
|
|
530
|
+
from ast_nodes import TypeAliasDecl, InterfaceDecl
|
|
531
|
+
# First sub-pass: register type aliases, structs, and interfaces
|
|
478
532
|
for node in program.body:
|
|
479
533
|
if isinstance(node, StructDecl):
|
|
480
534
|
self._struct_defs[node.name] = node
|
|
@@ -483,7 +537,33 @@ class Analyzer(Visitor):
|
|
|
483
537
|
kind="struct", struct_node=node,
|
|
484
538
|
line=node.line, col=node.col
|
|
485
539
|
)
|
|
486
|
-
elif isinstance(node,
|
|
540
|
+
elif isinstance(node, TypeAliasDecl):
|
|
541
|
+
type_ann = getattr(node, 'type_ann', None)
|
|
542
|
+
if type_ann is not None:
|
|
543
|
+
resolved = self._resolve_type_ann(type_ann)
|
|
544
|
+
else:
|
|
545
|
+
resolved = BUILTIN_TYPES.get(node.target, InScriptType(node.target))
|
|
546
|
+
self._type_aliases[node.name] = resolved
|
|
547
|
+
self._scope.symbols[node.name] = Symbol(
|
|
548
|
+
node.name, resolved, kind="type",
|
|
549
|
+
line=node.line, col=node.col
|
|
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
|
+
)
|
|
564
|
+
# Second sub-pass: hoist functions, scenes, enums (can now reference aliases)
|
|
565
|
+
for node in program.body:
|
|
566
|
+
if isinstance(node, FunctionDecl):
|
|
487
567
|
ret = self._resolve_type_ann(node.return_type)
|
|
488
568
|
self._scope.symbols[node.name] = Symbol(
|
|
489
569
|
node.name, ret, kind="fn", fn_node=node,
|
|
@@ -497,7 +577,7 @@ class Analyzer(Visitor):
|
|
|
497
577
|
elif isinstance(node, EnumDecl):
|
|
498
578
|
self._scope.symbols[node.name] = Symbol(
|
|
499
579
|
node.name, InScriptType(node.name), kind="enum",
|
|
500
|
-
fn_node=node,
|
|
580
|
+
fn_node=node,
|
|
501
581
|
line=node.line, col=node.col
|
|
502
582
|
)
|
|
503
583
|
|
|
@@ -575,12 +655,20 @@ class Analyzer(Visitor):
|
|
|
575
655
|
ret_type.name not in ("void", "nil", "any", "") and
|
|
576
656
|
not node.is_native if hasattr(node, 'is_native') else True):
|
|
577
657
|
if node.body and not self._body_always_returns(node.body):
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
)
|
|
584
672
|
|
|
585
673
|
# Analyze body in a new scope
|
|
586
674
|
self._push_scope("fn")
|
|
@@ -651,6 +739,26 @@ class Analyzer(Visitor):
|
|
|
651
739
|
for method in node.methods:
|
|
652
740
|
self.visit_FunctionDecl(method)
|
|
653
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
|
+
|
|
654
762
|
self._pop_scope()
|
|
655
763
|
return struct_type
|
|
656
764
|
|
|
@@ -695,6 +803,34 @@ class Analyzer(Visitor):
|
|
|
695
803
|
)
|
|
696
804
|
return enum_type
|
|
697
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
|
+
|
|
821
|
+
def visit_TypeAliasDecl(self, node) -> InScriptType:
|
|
822
|
+
"""v1.8.2: Register a type alias so annotations can reference it by name."""
|
|
823
|
+
if hasattr(node, 'type_ann') and node.type_ann is not None:
|
|
824
|
+
resolved = self._resolve_type_ann(node.type_ann)
|
|
825
|
+
else:
|
|
826
|
+
# Legacy alias with only a target string
|
|
827
|
+
resolved = BUILTIN_TYPES.get(node.target, InScriptType(node.target))
|
|
828
|
+
self._type_aliases[node.name] = resolved
|
|
829
|
+
# Also register in scope so the name is "used" and not flagged undefined
|
|
830
|
+
self._scope.symbols[node.name] = Symbol(node.name, resolved, kind="type",
|
|
831
|
+
line=node.line, col=node.col)
|
|
832
|
+
return T_VOID
|
|
833
|
+
|
|
698
834
|
def visit_ImportDecl(self, node: ImportDecl) -> InScriptType:
|
|
699
835
|
"""Register imported symbols so the type checker knows about them."""
|
|
700
836
|
try:
|
|
@@ -890,47 +1026,56 @@ class Analyzer(Visitor):
|
|
|
890
1026
|
|
|
891
1027
|
def _check_match_exhaustiveness(self, node: MatchStmt) -> None:
|
|
892
1028
|
"""
|
|
893
|
-
|
|
894
|
-
|
|
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.
|
|
895
1032
|
"""
|
|
896
|
-
subject
|
|
1033
|
+
subject = node.subject
|
|
897
1034
|
enum_name = None
|
|
898
1035
|
|
|
899
1036
|
if isinstance(subject, IdentExpr):
|
|
900
1037
|
sym = self._scope.lookup(subject.name)
|
|
901
|
-
if sym and sym.type_
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
|
905
1044
|
|
|
906
1045
|
if enum_name is None:
|
|
907
|
-
return #
|
|
1046
|
+
return # subject is not a typed enum variable — skip
|
|
908
1047
|
|
|
909
|
-
enum_sym = self._scope.
|
|
1048
|
+
enum_sym = self._scope.lookup(enum_name)
|
|
910
1049
|
if not enum_sym or enum_sym.fn_node is None:
|
|
911
1050
|
return
|
|
912
|
-
enum_decl
|
|
1051
|
+
enum_decl = enum_sym.fn_node
|
|
913
1052
|
all_variants = {v.name for v in getattr(enum_decl, 'variants', [])}
|
|
914
1053
|
if not all_variants:
|
|
915
1054
|
return
|
|
916
1055
|
|
|
917
|
-
# 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)
|
|
918
1059
|
covered = set()
|
|
919
1060
|
for arm in node.arms:
|
|
920
1061
|
p = arm.pattern
|
|
921
1062
|
if p is None:
|
|
922
|
-
return # wildcard — exhaustive
|
|
923
|
-
|
|
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)
|
|
924
1070
|
and isinstance(p.obj, IdentExpr)
|
|
925
1071
|
and p.obj.name == enum_name):
|
|
926
1072
|
covered.add(p.attr)
|
|
927
1073
|
|
|
928
1074
|
missing = sorted(all_variants - covered)
|
|
929
1075
|
if missing:
|
|
930
|
-
self.
|
|
931
|
-
"exhaustive_match",
|
|
1076
|
+
self._error(
|
|
932
1077
|
f"Non-exhaustive match on '{enum_name}': "
|
|
933
|
-
f"
|
|
1078
|
+
f"missing variant(s) {missing}. Add the missing cases or a wildcard arm 'case _'",
|
|
934
1079
|
node.line
|
|
935
1080
|
)
|
|
936
1081
|
|
|
@@ -952,7 +1097,9 @@ class Analyzer(Visitor):
|
|
|
952
1097
|
|
|
953
1098
|
def visit_IntLiteralExpr(self, node: IntLiteralExpr) -> InScriptType: return T_INT
|
|
954
1099
|
def visit_FloatLiteralExpr(self, node: FloatLiteralExpr) -> InScriptType: return T_FLOAT
|
|
955
|
-
def visit_StringLiteralExpr(self, node: StringLiteralExpr) -> InScriptType:
|
|
1100
|
+
def visit_StringLiteralExpr(self, node: StringLiteralExpr) -> InScriptType:
|
|
1101
|
+
# v1.8.2: return a specific literal type so union-of-literals is enforced
|
|
1102
|
+
return literal_type(node.value)
|
|
956
1103
|
def visit_BoolLiteralExpr(self, node: BoolLiteralExpr) -> InScriptType: return T_BOOL
|
|
957
1104
|
def visit_NullLiteralExpr(self, node: NullLiteralExpr) -> InScriptType: return T_NULL
|
|
958
1105
|
|
|
@@ -49,6 +49,11 @@ class TypeAnnotation(Node):
|
|
|
49
49
|
is_nullable: bool = False
|
|
50
50
|
key_type: Optional["TypeAnnotation"] = None
|
|
51
51
|
nullable: bool = False
|
|
52
|
+
# v1.8.2: for string literal types `"left"` in unions / params
|
|
53
|
+
literal_value: Optional[str] = None # set when name == "__literal__"
|
|
54
|
+
# v1.8.2: for fn type aliases `fn(int) -> bool`
|
|
55
|
+
fn_params: Optional[list] = None # list of TypeAnnotation
|
|
56
|
+
fn_return: Optional["TypeAnnotation"] = None
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -717,9 +722,10 @@ class DecoratedDecl(Node):
|
|
|
717
722
|
|
|
718
723
|
@dataclass
|
|
719
724
|
class TypeAliasDecl(Node):
|
|
720
|
-
"""type ID = ExistingType
|
|
721
|
-
name:
|
|
722
|
-
target:
|
|
725
|
+
"""type ID = ExistingType | fn(...)->T | "lit1"|"lit2" — type alias."""
|
|
726
|
+
name: str
|
|
727
|
+
target: str # kept for backwards compat with interpreter
|
|
728
|
+
type_ann: object = None # TypeAnnotation (None for legacy string-only aliases)
|
|
723
729
|
|
|
724
730
|
@dataclass
|
|
725
731
|
class SelectStmt(Node):
|
|
@@ -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"
|
|
@@ -937,9 +937,13 @@ class Interpreter(Visitor):
|
|
|
937
937
|
return iface
|
|
938
938
|
|
|
939
939
|
def visit_TypeAliasDecl(self, node) -> Any:
|
|
940
|
-
"""
|
|
941
|
-
|
|
942
|
-
self._env.define(node.name, {
|
|
940
|
+
"""v1.8.2: Register alias so it can be used in typeof/is checks at runtime."""
|
|
941
|
+
type_ann = getattr(node, 'type_ann', None)
|
|
942
|
+
self._env.define(node.name, {
|
|
943
|
+
"__type_alias__": True,
|
|
944
|
+
"__target__": node.target,
|
|
945
|
+
"__type_ann__": type_ann,
|
|
946
|
+
})
|
|
943
947
|
return None
|
|
944
948
|
|
|
945
949
|
def visit_ImplDecl(self, node) -> Any:
|
|
@@ -216,23 +216,17 @@ class Parser:
|
|
|
216
216
|
# ─────────────────────────────────────────────
|
|
217
217
|
|
|
218
218
|
def parse_type_alias(self):
|
|
219
|
-
"""type ID =
|
|
219
|
+
"""type ID = TypeAnnotation — supports simple, union, literal union, fn types."""
|
|
220
220
|
line, col = self.current.line, self.current.col
|
|
221
221
|
self.advance() # consume 'type'
|
|
222
222
|
name = self.expect(TT.IDENT, "Expected alias name after 'type'").value
|
|
223
223
|
self.expect(TT.ASSIGN, "Expected '=' in type alias")
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# Handle qualified names like MyModule.MyType
|
|
229
|
-
while self.check(TT.DOT):
|
|
230
|
-
self.advance()
|
|
231
|
-
target_name += "." + (self.current.value or "")
|
|
232
|
-
self.advance()
|
|
233
|
-
# Return a simple VarDecl so the interpreter stores name in env as a type tag
|
|
224
|
+
# v1.8.2: parse a full type annotation (including string literal unions, fn types)
|
|
225
|
+
type_ann = self.parse_type_annotation()
|
|
226
|
+
# target: for backwards compat with interpreter, derive a string name
|
|
227
|
+
target = type_ann.name if type_ann.name not in ("__literal__", "__fn__", "Union") else name
|
|
234
228
|
from ast_nodes import TypeAliasDecl
|
|
235
|
-
return TypeAliasDecl(name=name, target=
|
|
229
|
+
return TypeAliasDecl(name=name, target=target, type_ann=type_ann, line=line, col=col)
|
|
236
230
|
|
|
237
231
|
def parse_import(self) -> ImportDecl:
|
|
238
232
|
"""
|
|
@@ -447,6 +441,40 @@ class Parser:
|
|
|
447
441
|
elif tok.type == TT.NIL: # v1.7.2: nil as type annotation
|
|
448
442
|
name = "nil"
|
|
449
443
|
self.advance()
|
|
444
|
+
# v1.8.2: string literal type — `"left"` in union / param
|
|
445
|
+
elif tok.type == TT.STRING:
|
|
446
|
+
lit = tok.value
|
|
447
|
+
self.advance()
|
|
448
|
+
ann = TypeAnnotation(name="__literal__", literal_value=lit,
|
|
449
|
+
line=line, col=col)
|
|
450
|
+
# Union suffix handled below — return early for nullable/union
|
|
451
|
+
if self.current.type == TT.QUESTION:
|
|
452
|
+
self.advance()
|
|
453
|
+
ann = TypeAnnotation(name="Optional", is_nullable=True,
|
|
454
|
+
generics=[ann], line=line, col=col)
|
|
455
|
+
if self.current.type == TT.BIT_OR:
|
|
456
|
+
union_types = [ann]
|
|
457
|
+
while self.current.type == TT.BIT_OR:
|
|
458
|
+
self.advance()
|
|
459
|
+
union_types.append(self.parse_type_annotation())
|
|
460
|
+
ann = TypeAnnotation(name="Union", generics=union_types, line=line, col=col)
|
|
461
|
+
return ann
|
|
462
|
+
# v1.8.2: fn type alias — `fn(int, string) -> bool`
|
|
463
|
+
elif tok.type == TT.FN:
|
|
464
|
+
self.advance() # consume 'fn'
|
|
465
|
+
self.expect(TT.LPAREN, "Expected '(' in fn type")
|
|
466
|
+
fn_params = []
|
|
467
|
+
if not self.check(TT.RPAREN):
|
|
468
|
+
fn_params.append(self.parse_type_annotation())
|
|
469
|
+
while self.match(TT.COMMA):
|
|
470
|
+
fn_params.append(self.parse_type_annotation())
|
|
471
|
+
self.expect(TT.RPAREN, "Expected ')' in fn type")
|
|
472
|
+
fn_return = None
|
|
473
|
+
if self.match(TT.ARROW):
|
|
474
|
+
fn_return = self.parse_type_annotation()
|
|
475
|
+
ann = TypeAnnotation(name="__fn__", fn_params=fn_params,
|
|
476
|
+
fn_return=fn_return, line=line, col=col)
|
|
477
|
+
return ann
|
|
450
478
|
else:
|
|
451
479
|
self._error(f"Expected type name, got '{tok.value}'")
|
|
452
480
|
ann = TypeAnnotation(name=name, line=line, col=col)
|
|
@@ -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
|