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.
Files changed (30) hide show
  1. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/PKG-INFO +1 -1
  2. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/analyzer.py +177 -30
  3. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/ast_nodes.py +9 -3
  4. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript.py +1 -1
  5. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/PKG-INFO +1 -1
  6. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/interpreter.py +7 -3
  7. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/parser.py +40 -12
  8. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/pyproject.toml +1 -1
  9. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/repl.py +1 -1
  10. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/README.md +0 -0
  11. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/compiler.py +0 -0
  12. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/environment.py +0 -0
  13. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/errors.py +0 -0
  14. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_fmt.py +0 -0
  15. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/SOURCES.txt +0 -0
  16. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/dependency_links.txt +0 -0
  17. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/entry_points.txt +0 -0
  18. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/requires.txt +0 -0
  19. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_lang.egg-info/top_level.txt +0 -0
  20. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/inscript_test.py +0 -0
  21. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/lexer.py +0 -0
  22. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/pygame_backend.py +0 -0
  23. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/setup.cfg +0 -0
  24. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/setup.py +0 -0
  25. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib.py +0 -0
  26. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_extended.py +0 -0
  27. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_game.py +0 -0
  29. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/stdlib_values.py +0 -0
  30. {inscript_lang-1.8.1 → inscript_lang-1.8.3}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 1.8.1
3
+ Version: 1.8.3
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -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, parsed as TypeAnnotation(name="Optional",
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, parsed as TypeAnnotation(name="Union",
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, and top-level functions before checking bodies."""
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, FunctionDecl):
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, # store EnumDecl for exhaustiveness checks
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
- self._warn(
579
- "missing-return",
580
- f"Function '{node.name}' declares return type '{ret_type.name}' "
581
- f"but not all code paths return a value",
582
- node.line
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
- If the match subject is a typed variable whose type is a known enum,
894
- warn when not all variants are covered and there is no wildcard arm.
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 = node.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_ and sym.type_.name in self._scope.symbols:
902
- candidate = self._scope.symbols[sym.type_.name]
903
- if candidate.kind == "enum":
904
- enum_name = sym.type_.name
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 # can't determine enum — skip
1046
+ return # subject is not a typed enum variable — skip
908
1047
 
909
- enum_sym = self._scope.symbols.get(enum_name)
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 = enum_sym.fn_node
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: case EnumName.Variant
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
- if (isinstance(p, GetAttrExpr)
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._warn(
931
- "exhaustive_match",
1076
+ self._error(
932
1077
  f"Non-exhaustive match on '{enum_name}': "
933
- f"variants {missing} not covered. Add missing cases or a wildcard 'case _'",
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: return T_STRING
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 type alias (annotation only at runtime)."""
721
- name: str
722
- target: str
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.1"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 1.8.1
3
+ Version: 1.8.3
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -937,9 +937,13 @@ class Interpreter(Visitor):
937
937
  return iface
938
938
 
939
939
  def visit_TypeAliasDecl(self, node) -> Any:
940
- """type ID = ExistingType store alias so type annotations can reference it."""
941
- # Runtime effect: register the alias name so it can be used in is/as checks
942
- self._env.define(node.name, {"__type_alias__": True, "__target__": node.target})
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 = ExistingTypetype alias declaration (annotation only)."""
219
+ """type ID = TypeAnnotationsupports 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
- # Consume the type expression (could be int, string, ident, etc.)
225
- target_tok = self.current
226
- target_name = target_tok.value or target_tok.type.name
227
- self.advance()
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=target_name, line=line, col=col)
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.1"
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.1"
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