inscript-lang 1.9.2__tar.gz → 1.9.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.9.2 → inscript_lang-1.9.4}/PKG-INFO +1 -1
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/analyzer.py +6 -1
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript.py +904 -1
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/pyproject.toml +1 -1
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/repl.py +1 -1
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/setup.py +19 -4
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/README.md +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/ast_nodes.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/compiler.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/environment.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/errors.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_fmt.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/inscript_test.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/interpreter.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/lexer.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/parser.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/pygame_backend.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/setup.cfg +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/stdlib.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/stdlib_extended.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/stdlib_game.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/stdlib_values.py +0 -0
- {inscript_lang-1.9.2 → inscript_lang-1.9.4}/vm.py +0 -0
|
@@ -616,7 +616,8 @@ class Analyzer(Visitor):
|
|
|
616
616
|
)
|
|
617
617
|
# Infer type from initializer if no annotation
|
|
618
618
|
if declared_type == T_ANY and node.initializer:
|
|
619
|
-
|
|
619
|
+
# v1.8.2: literal_type("hello") is a string value — store as T_STRING
|
|
620
|
+
declared_type = T_STRING if is_literal_type(init_type) else init_type
|
|
620
621
|
|
|
621
622
|
self._define(Symbol(
|
|
622
623
|
node.name, declared_type,
|
|
@@ -1155,6 +1156,10 @@ class Analyzer(Visitor):
|
|
|
1155
1156
|
right = self.visit(node.right)
|
|
1156
1157
|
op = node.op
|
|
1157
1158
|
|
|
1159
|
+
# v1.8.2: literal string types act as T_STRING in all operations
|
|
1160
|
+
if is_literal_type(left): left = T_STRING
|
|
1161
|
+
if is_literal_type(right): right = T_STRING
|
|
1162
|
+
|
|
1158
1163
|
# Comparison operators always return bool
|
|
1159
1164
|
if op in ("==", "!=", "<", ">", "<=", ">="):
|
|
1160
1165
|
return T_BOOL
|
|
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
|
|
|
24
24
|
SemanticError, InScriptRuntimeError,
|
|
25
25
|
MultiError, InScriptWarning)
|
|
26
26
|
|
|
27
|
-
VERSION = "1.9.
|
|
27
|
+
VERSION = "1.9.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"
|
|
@@ -435,6 +435,882 @@ def _print_inscript_profile(pr, filename: str = "<script>"):
|
|
|
435
435
|
print(f" {'':>10} {total_time:>8.3f} 100.0% TOTAL (hotspots)\n")
|
|
436
436
|
|
|
437
437
|
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
441
|
+
# v1.9.3 — Documentation Generation
|
|
442
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
import re as _re
|
|
445
|
+
|
|
446
|
+
# ── Doc comment parser ────────────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
def _parse_doc_comments(source: str) -> list:
|
|
449
|
+
"""
|
|
450
|
+
v1.9.3: Extract doc-comment blocks from InScript source.
|
|
451
|
+
|
|
452
|
+
A doc block is one or more consecutive `///` lines immediately followed
|
|
453
|
+
by a `fn`, `struct`, `enum`, `interface`, or `type` declaration.
|
|
454
|
+
|
|
455
|
+
Returns a list of dicts:
|
|
456
|
+
{
|
|
457
|
+
"kind": "fn"|"struct"|"enum"|"interface"|"type",
|
|
458
|
+
"name": str,
|
|
459
|
+
"description": str, # first non-tag lines joined
|
|
460
|
+
"params": [{"name":str, "type":str, "desc":str}],
|
|
461
|
+
"returns": {"type":str, "desc":str} | None,
|
|
462
|
+
"examples": [str], # code blocks from @example tags
|
|
463
|
+
"deprecated": str | None, # message from @deprecated
|
|
464
|
+
"since": str | None, # version string from @since
|
|
465
|
+
"raw_tags": [{"tag":str, "text":str}], # all unrecognized tags
|
|
466
|
+
"line": int,
|
|
467
|
+
}
|
|
468
|
+
"""
|
|
469
|
+
lines = source.splitlines()
|
|
470
|
+
results = []
|
|
471
|
+
i = 0
|
|
472
|
+
|
|
473
|
+
DECL_RE = _re.compile(
|
|
474
|
+
r'^\s*(?:pub\s+)?(?:async\s+)?'
|
|
475
|
+
r'(fn|struct|enum|interface|type)\s+(\w+)'
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
while i < len(lines):
|
|
479
|
+
# Collect consecutive /// lines
|
|
480
|
+
if not lines[i].lstrip().startswith("///"):
|
|
481
|
+
i += 1
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
block_start = i
|
|
485
|
+
raw_comment = []
|
|
486
|
+
while i < len(lines) and lines[i].lstrip().startswith("///"):
|
|
487
|
+
stripped = lines[i].lstrip()
|
|
488
|
+
text = stripped[3:].lstrip(" ") # strip leading "/// "
|
|
489
|
+
raw_comment.append(text)
|
|
490
|
+
i += 1
|
|
491
|
+
|
|
492
|
+
# Skip blank lines between doc block and declaration
|
|
493
|
+
j = i
|
|
494
|
+
while j < len(lines) and not lines[j].strip():
|
|
495
|
+
j += 1
|
|
496
|
+
|
|
497
|
+
if j >= len(lines):
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
m = DECL_RE.match(lines[j])
|
|
501
|
+
if not m:
|
|
502
|
+
continue # doc block not followed by a declaration
|
|
503
|
+
|
|
504
|
+
kind = m.group(1)
|
|
505
|
+
name = m.group(2)
|
|
506
|
+
|
|
507
|
+
# Parse the doc block
|
|
508
|
+
description_lines = []
|
|
509
|
+
params = []
|
|
510
|
+
returns = None
|
|
511
|
+
examples = []
|
|
512
|
+
deprecated = None
|
|
513
|
+
since = None
|
|
514
|
+
raw_tags = []
|
|
515
|
+
example_buf = []
|
|
516
|
+
in_example = False
|
|
517
|
+
|
|
518
|
+
for raw in raw_comment:
|
|
519
|
+
if raw.startswith("@example"):
|
|
520
|
+
in_example = True
|
|
521
|
+
example_buf = []
|
|
522
|
+
continue
|
|
523
|
+
if in_example:
|
|
524
|
+
if raw.startswith("@"):
|
|
525
|
+
examples.append("\n".join(example_buf).strip())
|
|
526
|
+
example_buf = []
|
|
527
|
+
in_example = False
|
|
528
|
+
# fall through to process this @tag
|
|
529
|
+
else:
|
|
530
|
+
example_buf.append(raw)
|
|
531
|
+
continue
|
|
532
|
+
if raw.startswith("@param "):
|
|
533
|
+
rest = raw[7:].strip()
|
|
534
|
+
parts = rest.split(None, 2)
|
|
535
|
+
pname = parts[0] if len(parts) > 0 else ""
|
|
536
|
+
ptype = parts[1] if len(parts) > 1 else ""
|
|
537
|
+
pdesc = parts[2] if len(parts) > 2 else ""
|
|
538
|
+
# strip optional type wrapping like (int) or [int]
|
|
539
|
+
ptype = ptype.strip("()[]")
|
|
540
|
+
params.append({"name": pname, "type": ptype, "desc": pdesc})
|
|
541
|
+
elif raw.startswith("@returns ") or raw.startswith("@return "):
|
|
542
|
+
rest = raw.split(None, 1)[1].strip() if " " in raw else ""
|
|
543
|
+
parts = rest.split(None, 1)
|
|
544
|
+
rtype = parts[0].strip("()[]") if parts else ""
|
|
545
|
+
rdesc = parts[1] if len(parts) > 1 else ""
|
|
546
|
+
returns = {"type": rtype, "desc": rdesc}
|
|
547
|
+
elif raw.startswith("@deprecated"):
|
|
548
|
+
deprecated = raw[11:].strip()
|
|
549
|
+
elif raw.startswith("@since "):
|
|
550
|
+
since = raw[7:].strip()
|
|
551
|
+
elif raw.startswith("@"):
|
|
552
|
+
tag_parts = raw.split(None, 1)
|
|
553
|
+
raw_tags.append({
|
|
554
|
+
"tag": tag_parts[0][1:],
|
|
555
|
+
"text": tag_parts[1] if len(tag_parts) > 1 else ""
|
|
556
|
+
})
|
|
557
|
+
else:
|
|
558
|
+
description_lines.append(raw)
|
|
559
|
+
|
|
560
|
+
if in_example and example_buf:
|
|
561
|
+
examples.append("\n".join(example_buf).strip())
|
|
562
|
+
|
|
563
|
+
results.append({
|
|
564
|
+
"kind": kind,
|
|
565
|
+
"name": name,
|
|
566
|
+
"description": "\n".join(description_lines).strip(),
|
|
567
|
+
"params": params,
|
|
568
|
+
"returns": returns,
|
|
569
|
+
"examples": examples,
|
|
570
|
+
"deprecated": deprecated,
|
|
571
|
+
"since": since,
|
|
572
|
+
"raw_tags": raw_tags,
|
|
573
|
+
"line": block_start + 1,
|
|
574
|
+
})
|
|
575
|
+
i = j + 1
|
|
576
|
+
|
|
577
|
+
return results
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# ── Markdown renderer ─────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
def _render_doc_markdown(entries: list, source_file: str = "") -> str:
|
|
583
|
+
"""
|
|
584
|
+
v1.9.3: Render extracted doc entries as Markdown.
|
|
585
|
+
|
|
586
|
+
Format:
|
|
587
|
+
# API Reference — filename
|
|
588
|
+
## `fn name`
|
|
589
|
+
Description
|
|
590
|
+
### Parameters / Returns / Examples / Deprecated
|
|
591
|
+
"""
|
|
592
|
+
fname = os.path.basename(source_file) if source_file else "API"
|
|
593
|
+
stem = fname.replace(".ins", "")
|
|
594
|
+
|
|
595
|
+
lines = [
|
|
596
|
+
f"# API Reference — {stem}",
|
|
597
|
+
f"",
|
|
598
|
+
f"> Generated by InScript {VERSION} ",
|
|
599
|
+
f"> Source: `{source_file}`" if source_file else "",
|
|
600
|
+
f"",
|
|
601
|
+
"---",
|
|
602
|
+
"",
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
for entry in entries:
|
|
606
|
+
kind = entry["kind"]
|
|
607
|
+
name = entry["name"]
|
|
608
|
+
|
|
609
|
+
# Section header
|
|
610
|
+
lines.append(f"## `{kind} {name}`")
|
|
611
|
+
if entry.get("since"):
|
|
612
|
+
lines.append(f"*Since v{entry['since']}* ")
|
|
613
|
+
if entry.get("deprecated"):
|
|
614
|
+
lines.append(f"")
|
|
615
|
+
lines.append(f"> **Deprecated:** {entry['deprecated']}")
|
|
616
|
+
lines.append("")
|
|
617
|
+
|
|
618
|
+
# Description
|
|
619
|
+
if entry["description"]:
|
|
620
|
+
lines.append(entry["description"])
|
|
621
|
+
lines.append("")
|
|
622
|
+
|
|
623
|
+
# Parameters table
|
|
624
|
+
if entry["params"]:
|
|
625
|
+
lines.append("### Parameters")
|
|
626
|
+
lines.append("")
|
|
627
|
+
lines.append("| Name | Type | Description |")
|
|
628
|
+
lines.append("|------|------|-------------|")
|
|
629
|
+
for p in entry["params"]:
|
|
630
|
+
ptype = f"`{p['type']}`" if p["type"] else "—"
|
|
631
|
+
lines.append(f"| `{p['name']}` | {ptype} | {p['desc']} |")
|
|
632
|
+
lines.append("")
|
|
633
|
+
|
|
634
|
+
# Returns
|
|
635
|
+
if entry.get("returns"):
|
|
636
|
+
r = entry["returns"]
|
|
637
|
+
lines.append("### Returns")
|
|
638
|
+
lines.append("")
|
|
639
|
+
rtype = f"`{r['type']}`" if r["type"] else ""
|
|
640
|
+
rdesc = r["desc"]
|
|
641
|
+
if rtype and rdesc:
|
|
642
|
+
lines.append(f"{rtype} — {rdesc}")
|
|
643
|
+
elif rtype:
|
|
644
|
+
lines.append(rtype)
|
|
645
|
+
else:
|
|
646
|
+
lines.append(rdesc)
|
|
647
|
+
lines.append("")
|
|
648
|
+
|
|
649
|
+
# Examples (runnable code blocks)
|
|
650
|
+
if entry["examples"]:
|
|
651
|
+
lines.append("### Example")
|
|
652
|
+
lines.append("")
|
|
653
|
+
for ex in entry["examples"]:
|
|
654
|
+
lines.append("```inscript")
|
|
655
|
+
lines.append(ex)
|
|
656
|
+
lines.append("```")
|
|
657
|
+
lines.append("")
|
|
658
|
+
|
|
659
|
+
lines.append("---")
|
|
660
|
+
lines.append("")
|
|
661
|
+
|
|
662
|
+
return "\n".join(lines)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# ── File / directory driver ───────────────────────────────────────────────────
|
|
666
|
+
|
|
667
|
+
def _doc_file(filepath: str, output_path: str = None) -> tuple:
|
|
668
|
+
"""
|
|
669
|
+
v1.9.3: Extract doc comments from a single .ins file and return
|
|
670
|
+
(entries, markdown_str). Writes to output_path if given.
|
|
671
|
+
"""
|
|
672
|
+
try:
|
|
673
|
+
with open(filepath, encoding="utf-8") as f:
|
|
674
|
+
source = f.read()
|
|
675
|
+
except OSError as e:
|
|
676
|
+
print(f"[InScript doc] Cannot read '{filepath}': {e}", file=sys.stderr)
|
|
677
|
+
return [], ""
|
|
678
|
+
|
|
679
|
+
entries = _parse_doc_comments(source)
|
|
680
|
+
markdown = _render_doc_markdown(entries, source_file=filepath)
|
|
681
|
+
|
|
682
|
+
if output_path:
|
|
683
|
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
684
|
+
try:
|
|
685
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
686
|
+
f.write(markdown)
|
|
687
|
+
except OSError as e:
|
|
688
|
+
print(f"[InScript doc] Cannot write '{output_path}': {e}", file=sys.stderr)
|
|
689
|
+
|
|
690
|
+
return entries, markdown
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _generate_docs(path: str, output_dir: str = None) -> int:
|
|
694
|
+
"""
|
|
695
|
+
v1.9.3: `inscript doc FILE|DIR [--out DIR]`
|
|
696
|
+
|
|
697
|
+
• FILE → print Markdown to stdout (or write to --out/filename.md)
|
|
698
|
+
• DIR → walk all .ins files, write docs/api/<name>.md (or --out/<name>.md)
|
|
699
|
+
|
|
700
|
+
Returns 0 on success, 1 on error.
|
|
701
|
+
"""
|
|
702
|
+
if os.path.isfile(path):
|
|
703
|
+
entries, markdown = _doc_file(path)
|
|
704
|
+
if output_dir:
|
|
705
|
+
stem = os.path.basename(path).replace(".ins", "")
|
|
706
|
+
out_md = os.path.join(output_dir, stem + ".md")
|
|
707
|
+
_doc_file(path, output_path=out_md)
|
|
708
|
+
n = len(entries)
|
|
709
|
+
print(f"[InScript doc] '{path}' → '{out_md}' ({n} item{'s' if n!=1 else ''})")
|
|
710
|
+
else:
|
|
711
|
+
print(markdown)
|
|
712
|
+
return 0
|
|
713
|
+
|
|
714
|
+
elif os.path.isdir(path):
|
|
715
|
+
out_base = output_dir or os.path.join(path, "docs", "api")
|
|
716
|
+
ins_files = sorted(_find_ins_files(path))
|
|
717
|
+
if not ins_files:
|
|
718
|
+
print(f"[InScript doc] No .ins files found in '{path}'")
|
|
719
|
+
return 0
|
|
720
|
+
total_entries = 0
|
|
721
|
+
for fpath in ins_files:
|
|
722
|
+
rel = os.path.relpath(fpath, path)
|
|
723
|
+
stem = rel.replace(os.sep, "_").replace(".ins", "")
|
|
724
|
+
out_md = os.path.join(out_base, stem + ".md")
|
|
725
|
+
entries, _ = _doc_file(fpath, output_path=out_md)
|
|
726
|
+
total_entries += len(entries)
|
|
727
|
+
n = len(entries)
|
|
728
|
+
print(f"[InScript doc] {rel} → {os.path.relpath(out_md, path)} ({n} item{'s' if n!=1 else ''})")
|
|
729
|
+
print(f"[InScript doc] Done. {len(ins_files)} file(s), {total_entries} documented item(s).")
|
|
730
|
+
return 0
|
|
731
|
+
else:
|
|
732
|
+
print(f"[InScript doc] Not found: '{path}'", file=sys.stderr)
|
|
733
|
+
return 1
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
738
|
+
# v1.9.4 — Spec Freeze & Final Polish
|
|
739
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
740
|
+
|
|
741
|
+
# ── Error code catalogue ──────────────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
ERROR_CATALOGUE = {
|
|
744
|
+
"E0001": ("LexError", "Unexpected character or malformed token"),
|
|
745
|
+
"E0002": ("UnterminatedString", "String literal has no closing quote"),
|
|
746
|
+
"E0003": ("UnterminatedComment", "Block comment /* ... */ never closed"),
|
|
747
|
+
"E0010": ("ParseError", "Unexpected token or malformed syntax"),
|
|
748
|
+
"E0011": ("MissingClosingBrace", "Expected '}' to close block"),
|
|
749
|
+
"E0012": ("MissingClosingParen", "Expected ')' to close expression"),
|
|
750
|
+
"E0013": ("MissingClosingBracket", "Expected ']' to close array or index"),
|
|
751
|
+
"E0014": ("InvalidAssignTarget", "Left-hand side of assignment is not assignable"),
|
|
752
|
+
"E0015": ("MissingReturnType", "Arrow '->' present but return type missing"),
|
|
753
|
+
"E0020": ("SemanticError", "General semantic / type error"),
|
|
754
|
+
"E0021": ("TypeMismatch", "Value type does not match declared type"),
|
|
755
|
+
"E0022": ("UndefinedName", "Name referenced before declaration"),
|
|
756
|
+
"E0023": ("DuplicateDeclaration", "Name already declared in this scope"),
|
|
757
|
+
"E0024": ("ConstReassign", "Cannot reassign a constant"),
|
|
758
|
+
"E0025": ("ReturnTypeMismatch", "Returned value type differs from declared return type"),
|
|
759
|
+
"E0026": ("ReturnOutsideFn", "return statement outside a function"),
|
|
760
|
+
"E0027": ("BreakOutsideLoop", "break statement outside a loop"),
|
|
761
|
+
"E0028": ("ContinueOutsideLoop", "continue statement outside a loop"),
|
|
762
|
+
"E0029": ("UnknownType", "Type name not found in scope"),
|
|
763
|
+
"E0030": ("RuntimeError", "General runtime error"),
|
|
764
|
+
"E0031": ("DivisionByZero", "Integer or float division by zero"),
|
|
765
|
+
"E0032": ("IndexOutOfBounds", "Array index outside valid range"),
|
|
766
|
+
"E0033": ("KeyNotFound", "Dict key does not exist"),
|
|
767
|
+
"E0034": ("StackOverflow", "Call depth exceeded (infinite recursion)"),
|
|
768
|
+
"E0035": ("TypeError", "Operation applied to incompatible types"),
|
|
769
|
+
"E0036": ("ArithmeticError", "Invalid arithmetic operation"),
|
|
770
|
+
"E0037": ("InvalidCast", "Cannot cast value to target type"),
|
|
771
|
+
"E0038": ("NilDereference", "Method or field access on nil"),
|
|
772
|
+
"E0039": ("ThrowSignal", "Unhandled throw (not an error unless uncaught)"),
|
|
773
|
+
"E0040": ("UncaughtThrow", "throw reached top level without a try/catch"),
|
|
774
|
+
"E0041": ("ImportError", "Module or file could not be imported"),
|
|
775
|
+
"E0042": ("ExportError", "Export of undefined name"),
|
|
776
|
+
"E0043": ("NamespaceError", "Invalid namespace access"),
|
|
777
|
+
"E0044": ("ArgumentCount", "Wrong number of arguments to function call"),
|
|
778
|
+
"E0045": ("MissingField", "Struct initializer missing required field"),
|
|
779
|
+
"E0046": ("UnknownField", "Struct initializer references unknown field"),
|
|
780
|
+
"E0047": ("InterfaceNotImpl", "Struct claims to implement interface but methods missing"),
|
|
781
|
+
"E0048": ("EnumNotExhaustive", "match on enum is missing one or more variants"),
|
|
782
|
+
"E0049": ("NilAccess", "Optional field or nil value accessed without guard"),
|
|
783
|
+
"E0050": ("InvalidReturn", "Return value in void function"),
|
|
784
|
+
"E0051": ("UndeclaredInterface", "implements clause references unknown interface"),
|
|
785
|
+
"E0052": ("CircularImport", "Import creates a circular dependency"),
|
|
786
|
+
"E0053": ("ConstInLoop", "const declaration inside a loop"),
|
|
787
|
+
"E0054": ("NeverNotDiverging", "Function declared -> never but has a non-throwing path"),
|
|
788
|
+
"E0055": ("NullKeyword", "'null' keyword removed in v1.7.4 — use 'nil'"),
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _print_error_catalogue(filter_prefix: str = None) -> int:
|
|
793
|
+
"""
|
|
794
|
+
v1.9.4: Print the full error code catalogue, optionally filtered by prefix.
|
|
795
|
+
"""
|
|
796
|
+
print(f"InScript Error Code Catalogue (InScript {VERSION})")
|
|
797
|
+
print(f"{'='*58}")
|
|
798
|
+
count = 0
|
|
799
|
+
for code, (name, desc) in sorted(ERROR_CATALOGUE.items()):
|
|
800
|
+
if filter_prefix and not code.startswith(filter_prefix):
|
|
801
|
+
continue
|
|
802
|
+
print(f" {code} {name:<28} {desc}")
|
|
803
|
+
count += 1
|
|
804
|
+
print(f"{'='*58}")
|
|
805
|
+
print(f" {count} error code(s)")
|
|
806
|
+
return 0
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
# ── changelog generator ───────────────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
def _parse_version_tuple(v: str) -> tuple:
|
|
812
|
+
"""Parse 'v1.6.0' or '1.6.0' → (1, 6, 0)."""
|
|
813
|
+
import re
|
|
814
|
+
m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", v.strip())
|
|
815
|
+
if not m:
|
|
816
|
+
return (0, 0, 0)
|
|
817
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _generate_changelog_range(spec: str, changelog_path: str = None) -> int:
|
|
821
|
+
"""
|
|
822
|
+
v1.9.4: `inscript changelog FROM..TO`
|
|
823
|
+
|
|
824
|
+
Reads CHANGELOG.md and extracts entries whose version falls in [FROM, TO].
|
|
825
|
+
Prints filtered changelog to stdout.
|
|
826
|
+
|
|
827
|
+
Examples:
|
|
828
|
+
inscript changelog v1.6.0..v1.9.4
|
|
829
|
+
inscript changelog v1.8.0..
|
|
830
|
+
inscript changelog ..v1.7.4
|
|
831
|
+
"""
|
|
832
|
+
import re
|
|
833
|
+
|
|
834
|
+
# Parse the range spec
|
|
835
|
+
if ".." in spec:
|
|
836
|
+
lo_str, hi_str = spec.split("..", 1)
|
|
837
|
+
lo = _parse_version_tuple(lo_str) if lo_str.strip() else (0, 0, 0)
|
|
838
|
+
hi = _parse_version_tuple(hi_str) if hi_str.strip() else (999, 999, 999)
|
|
839
|
+
else:
|
|
840
|
+
# Single version — exact match
|
|
841
|
+
lo = hi = _parse_version_tuple(spec)
|
|
842
|
+
if lo == (0, 0, 0):
|
|
843
|
+
print("[InScript changelog] Invalid range. Use e.g. v1.6.0..v1.9.4 or v1.9.3",
|
|
844
|
+
file=sys.stderr)
|
|
845
|
+
return 1
|
|
846
|
+
|
|
847
|
+
if lo == (0, 0, 0) and hi == (999, 999, 999):
|
|
848
|
+
print("[InScript changelog] Invalid range. Use e.g. v1.6.0..v1.9.4",
|
|
849
|
+
file=sys.stderr)
|
|
850
|
+
return 1
|
|
851
|
+
|
|
852
|
+
# Find CHANGELOG.md
|
|
853
|
+
if changelog_path is None:
|
|
854
|
+
candidates = [
|
|
855
|
+
os.path.join(os.getcwd(), "CHANGELOG.md"),
|
|
856
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "CHANGELOG.md"),
|
|
857
|
+
]
|
|
858
|
+
changelog_path = next((p for p in candidates if os.path.exists(p)), None)
|
|
859
|
+
|
|
860
|
+
if not changelog_path or not os.path.exists(changelog_path):
|
|
861
|
+
print("[InScript changelog] CHANGELOG.md not found. "
|
|
862
|
+
"Run from the project root.", file=sys.stderr)
|
|
863
|
+
return 1
|
|
864
|
+
|
|
865
|
+
with open(changelog_path, encoding="utf-8") as f:
|
|
866
|
+
content = f.read()
|
|
867
|
+
|
|
868
|
+
# Split into per-version sections by "## [X.Y.Z]" headings
|
|
869
|
+
section_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\]", re.MULTILINE)
|
|
870
|
+
positions = [(m.start(), m.group(1)) for m in section_re.finditer(content)]
|
|
871
|
+
|
|
872
|
+
if not positions:
|
|
873
|
+
print("[InScript changelog] No version sections found in CHANGELOG.md",
|
|
874
|
+
file=sys.stderr)
|
|
875
|
+
return 1
|
|
876
|
+
|
|
877
|
+
# Extract sections that fall within [lo, hi]
|
|
878
|
+
selected = []
|
|
879
|
+
for i, (start, ver_str) in enumerate(positions):
|
|
880
|
+
vt = _parse_version_tuple(ver_str)
|
|
881
|
+
if lo <= vt <= hi:
|
|
882
|
+
end = positions[i + 1][0] if i + 1 < len(positions) else len(content)
|
|
883
|
+
selected.append((vt, ver_str, content[start:end].rstrip()))
|
|
884
|
+
|
|
885
|
+
if not selected:
|
|
886
|
+
lo_s = ".".join(str(x) for x in lo)
|
|
887
|
+
hi_s = ".".join(str(x) for x in hi)
|
|
888
|
+
print(f"[InScript changelog] No versions found in range {lo_s}..{hi_s}")
|
|
889
|
+
return 0
|
|
890
|
+
|
|
891
|
+
# Print newest-first
|
|
892
|
+
selected.sort(key=lambda x: x[0], reverse=True)
|
|
893
|
+
lo_s = ".".join(str(x) for x in lo) if lo != (0,0,0) else "start"
|
|
894
|
+
hi_s = ".".join(str(x) for x in hi) if hi != (999,999,999) else "latest"
|
|
895
|
+
print(f"# InScript Changelog {lo_s}..{hi_s}\n")
|
|
896
|
+
for _, _, section_text in selected:
|
|
897
|
+
print(section_text)
|
|
898
|
+
print()
|
|
899
|
+
|
|
900
|
+
print(f"---")
|
|
901
|
+
print(f"{len(selected)} version(s) shown.")
|
|
902
|
+
return 0
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
# ── performance benchmark suite ───────────────────────────────────────────────
|
|
906
|
+
|
|
907
|
+
BENCHMARK_PROGRAMS = {
|
|
908
|
+
"fib_30": (
|
|
909
|
+
"Fibonacci fib(30) — recursive, tests call overhead",
|
|
910
|
+
"""
|
|
911
|
+
fn fib(n) {
|
|
912
|
+
if n <= 1 { return n }
|
|
913
|
+
return fib(n - 1) + fib(n - 2)
|
|
914
|
+
}
|
|
915
|
+
fib(30)
|
|
916
|
+
"""
|
|
917
|
+
),
|
|
918
|
+
"loop_100k": (
|
|
919
|
+
"Counter loop 100,000 iterations — tests loop + arith throughput",
|
|
920
|
+
"""
|
|
921
|
+
let i = 0
|
|
922
|
+
let sum = 0
|
|
923
|
+
while i < 100000 {
|
|
924
|
+
sum = sum + i
|
|
925
|
+
i = i + 1
|
|
926
|
+
}
|
|
927
|
+
sum
|
|
928
|
+
"""
|
|
929
|
+
),
|
|
930
|
+
"struct_heavy": (
|
|
931
|
+
"Struct allocation + field access, 10,000 objects",
|
|
932
|
+
"""
|
|
933
|
+
struct Vec2 { x: float = 0.0; y: float = 0.0 }
|
|
934
|
+
fn make_vecs(n) {
|
|
935
|
+
let i = 0
|
|
936
|
+
let last_x = 0.0
|
|
937
|
+
while i < n {
|
|
938
|
+
let v = Vec2{x: i * 1.0, y: i * 2.0}
|
|
939
|
+
last_x = v.x + v.y
|
|
940
|
+
i = i + 1
|
|
941
|
+
}
|
|
942
|
+
return last_x
|
|
943
|
+
}
|
|
944
|
+
make_vecs(10000)
|
|
945
|
+
"""
|
|
946
|
+
),
|
|
947
|
+
"string_concat": (
|
|
948
|
+
"String concatenation, 1,000 iterations",
|
|
949
|
+
"""
|
|
950
|
+
let s = ""
|
|
951
|
+
let i = 0
|
|
952
|
+
while i < 1000 {
|
|
953
|
+
s = s + "x"
|
|
954
|
+
i = i + 1
|
|
955
|
+
}
|
|
956
|
+
len(s)
|
|
957
|
+
"""
|
|
958
|
+
),
|
|
959
|
+
"array_ops": (
|
|
960
|
+
"Array push + iteration, 5,000 elements",
|
|
961
|
+
"""
|
|
962
|
+
let arr = []
|
|
963
|
+
let i = 0
|
|
964
|
+
while i < 5000 {
|
|
965
|
+
arr.push(i)
|
|
966
|
+
i = i + 1
|
|
967
|
+
}
|
|
968
|
+
arr.len()
|
|
969
|
+
"""
|
|
970
|
+
),
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _run_benchmarks(filter_name: str = None, iterations: int = 3) -> int:
|
|
975
|
+
"""
|
|
976
|
+
v1.9.4: `inscript benchmark [NAME]` — run the built-in performance suite.
|
|
977
|
+
|
|
978
|
+
Runs each benchmark `iterations` times and reports median time.
|
|
979
|
+
"""
|
|
980
|
+
import time, statistics
|
|
981
|
+
|
|
982
|
+
from interpreter import Interpreter
|
|
983
|
+
from lexer import Lexer
|
|
984
|
+
from parser import Parser
|
|
985
|
+
|
|
986
|
+
programs = BENCHMARK_PROGRAMS
|
|
987
|
+
if filter_name:
|
|
988
|
+
programs = {k: v for k, v in programs.items() if filter_name in k}
|
|
989
|
+
if not programs:
|
|
990
|
+
print(f"[InScript benchmark] No benchmark matching '{filter_name}'",
|
|
991
|
+
file=sys.stderr)
|
|
992
|
+
return 1
|
|
993
|
+
|
|
994
|
+
col_w = max(len(k) for k in programs) + 2
|
|
995
|
+
print(f"\nInScript Benchmark Suite (v{VERSION}) [{iterations} runs each]")
|
|
996
|
+
print(f"{'='*(col_w + 52)}")
|
|
997
|
+
print(f" {'Benchmark':<{col_w}} {'Median':>10} {'Min':>10} {'Max':>10} Description")
|
|
998
|
+
print(f" {'-'*(col_w)} {'-'*10} {'-'*10} {'-'*10} {'-'*30}")
|
|
999
|
+
|
|
1000
|
+
results = {}
|
|
1001
|
+
for name, (desc, src) in programs.items():
|
|
1002
|
+
times = []
|
|
1003
|
+
for _ in range(iterations):
|
|
1004
|
+
try:
|
|
1005
|
+
interp = Interpreter()
|
|
1006
|
+
tokens = Lexer(src).tokenize()
|
|
1007
|
+
program = Parser(tokens).parse()
|
|
1008
|
+
t0 = time.perf_counter()
|
|
1009
|
+
interp.run(program)
|
|
1010
|
+
times.append(time.perf_counter() - t0)
|
|
1011
|
+
except Exception as e:
|
|
1012
|
+
print(f" {'ERROR':<{col_w}} {name}: {e}", file=sys.stderr)
|
|
1013
|
+
times.append(float("inf"))
|
|
1014
|
+
|
|
1015
|
+
median = statistics.median(times)
|
|
1016
|
+
mn = min(times)
|
|
1017
|
+
mx = max(times)
|
|
1018
|
+
|
|
1019
|
+
def fmt(t):
|
|
1020
|
+
if t == float("inf"): return " ERROR "
|
|
1021
|
+
if t < 1: return f"{t*1000:9.1f}ms"
|
|
1022
|
+
return f"{t:9.3f}s "
|
|
1023
|
+
|
|
1024
|
+
results[name] = median
|
|
1025
|
+
print(f" {name:<{col_w}} {fmt(median)} {fmt(mn)} {fmt(mx)} {desc[:40]}")
|
|
1026
|
+
|
|
1027
|
+
print(f"{'='*(col_w + 52)}")
|
|
1028
|
+
fastest = min(results, key=results.get)
|
|
1029
|
+
slowest = max(k for k,v in results.items() if v != float("inf"))
|
|
1030
|
+
print(f" Fastest: {fastest} ({results[fastest]*1000:.1f}ms median)")
|
|
1031
|
+
print()
|
|
1032
|
+
return 0
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# ── stdlib documentation ──────────────────────────────────────────────────────
|
|
1036
|
+
|
|
1037
|
+
STDLIB_SIGNATURES = {
|
|
1038
|
+
# Built-in functions
|
|
1039
|
+
"print": ("(value: any) -> void", "Print value to stdout", "print(42)"),
|
|
1040
|
+
"println": ("(value: any) -> void", "Print value with newline", "println('hello')"),
|
|
1041
|
+
"len": ("(x: array|string) -> int", "Length of array or string", "len([1,2,3]) // 3"),
|
|
1042
|
+
"range": ("(n: int) -> [int]", "Generate 0..n-1 integer array", "range(5) // [0,1,2,3,4]"),
|
|
1043
|
+
"typeof": ("(x: any) -> string", "Runtime type name of value", "typeof(42) // 'int'"),
|
|
1044
|
+
"str": ("(x: any) -> string", "Convert value to string", "str(3.14) // '3.14'"),
|
|
1045
|
+
"int": ("(x: any) -> int", "Parse/cast to integer", "int('42') // 42"),
|
|
1046
|
+
"float": ("(x: any) -> float", "Parse/cast to float", "float('3.14') // 3.14"),
|
|
1047
|
+
"bool": ("(x: any) -> bool", "Cast to boolean", "bool(0) // false"),
|
|
1048
|
+
"abs": ("(x: int|float) -> same", "Absolute value", "abs(-5) // 5"),
|
|
1049
|
+
"min": ("(a, b) -> same", "Smaller of two values", "min(3, 7) // 3"),
|
|
1050
|
+
"max": ("(a, b) -> same", "Larger of two values", "max(3, 7) // 7"),
|
|
1051
|
+
"clamp": ("(v, lo, hi) -> same", "Clamp v to [lo, hi]", "clamp(15, 0, 10) // 10"),
|
|
1052
|
+
"floor": ("(x: float) -> int", "Floor to nearest integer", "floor(3.7) // 3"),
|
|
1053
|
+
"ceil": ("(x: float) -> int", "Ceiling to nearest integer", "ceil(3.2) // 4"),
|
|
1054
|
+
"round": ("(x: float) -> int", "Round to nearest integer", "round(3.5) // 4"),
|
|
1055
|
+
"sqrt": ("(x: float) -> float", "Square root", "sqrt(9.0) // 3.0"),
|
|
1056
|
+
"pow": ("(base, exp) -> float", "Raise base to exponent", "pow(2.0, 8.0) // 256.0"),
|
|
1057
|
+
"sin": ("(x: float) -> float", "Sine (radians)", "sin(0.0) // 0.0"),
|
|
1058
|
+
"cos": ("(x: float) -> float", "Cosine (radians)", "cos(0.0) // 1.0"),
|
|
1059
|
+
"random": ("() -> float", "Random float in [0.0, 1.0)", "random()"),
|
|
1060
|
+
"random_int": ("(lo: int, hi: int) -> int","Random int in [lo, hi]", "random_int(1, 6)"),
|
|
1061
|
+
"assert": ("(cond: bool, msg: string = '') -> void",
|
|
1062
|
+
"Assert condition or throw", "assert(x > 0, 'must be positive')"),
|
|
1063
|
+
"panic": ("(msg: string) -> never", "Throw with message, always fails", "panic('unreachable')"),
|
|
1064
|
+
"exit": ("(code: int = 0) -> never","Exit program with status code", "exit(1)"),
|
|
1065
|
+
"input": ("(prompt: string = '') -> string",
|
|
1066
|
+
"Read line from stdin", "let name = input('Name: ')"),
|
|
1067
|
+
# Array methods
|
|
1068
|
+
"Array.push": ("(v: T) -> void", "Append element", "[].push(1)"),
|
|
1069
|
+
"Array.pop": ("() -> T?", "Remove and return last element", "arr.pop()"),
|
|
1070
|
+
"Array.len": ("() -> int", "Number of elements", "arr.len()"),
|
|
1071
|
+
"Array.map": ("(fn: (T)->U) -> [U]", "Transform each element", "arr.map(fn(x){return x*2})"),
|
|
1072
|
+
"Array.filter": ("(fn: (T)->bool) -> [T]", "Keep matching elements", "arr.filter(fn(x){return x>0})"),
|
|
1073
|
+
"Array.find": ("(fn: (T)->bool) -> T?", "First matching element or nil", "arr.find(fn(x){return x>3})"),
|
|
1074
|
+
"Array.any": ("(fn: (T)->bool) -> bool","True if any element matches", "arr.any(fn(x){return x>0})"),
|
|
1075
|
+
"Array.all": ("(fn: (T)->bool) -> bool","True if all elements match", "arr.all(fn(x){return x>0})"),
|
|
1076
|
+
"Array.each": ("(fn: (T)->void) -> void","Iterate (side-effect only)", "arr.each(fn(x){print(x)})"),
|
|
1077
|
+
"Array.reduce": ("(fn: (U,T)->U, init: U) -> U",
|
|
1078
|
+
"Fold array to single value", "arr.reduce(fn(acc,x){return acc+x}, 0)"),
|
|
1079
|
+
"Array.sorted": ("() -> [T]", "Return sorted copy", "arr.sorted()"),
|
|
1080
|
+
"Array.reversed":"() -> [T]",
|
|
1081
|
+
"Array.join": ("(sep: string) -> string","Join elements with separator", "arr.join(', ')"),
|
|
1082
|
+
"Array.contains":"(v: T) -> bool",
|
|
1083
|
+
"Array.take": ("(n: int) -> [T]", "First n elements", "arr.take(3)"),
|
|
1084
|
+
"Array.skip": ("(n: int) -> [T]", "Skip first n elements", "arr.skip(2)"),
|
|
1085
|
+
# String methods
|
|
1086
|
+
"String.len": ("() -> int", "Length in characters", '"hello".len() // 5'),
|
|
1087
|
+
"String.upper": ("() -> string", "Uppercase copy", '"hi".upper() // "HI"'),
|
|
1088
|
+
"String.lower": ("() -> string", "Lowercase copy", '"HI".lower() // "hi"'),
|
|
1089
|
+
"String.trim": ("() -> string", "Strip leading/trailing whitespace",'" x ".trim() // "x"'),
|
|
1090
|
+
"String.split": ('(sep: string) -> [string]', "Split on separator", '"a,b".split(",") // ["a","b"]'),
|
|
1091
|
+
"String.contains": ("(sub: string) -> bool", "True if substring found", '"hello".contains("ell")'),
|
|
1092
|
+
"String.starts_with":("(pre: string) -> bool", "True if starts with prefix", '"hello".starts_with("he")'),
|
|
1093
|
+
"String.ends_with": ("(suf: string) -> bool", "True if ends with suffix", '"hello".ends_with("lo")'),
|
|
1094
|
+
"String.replace": ("(old: string, new: string) -> string",
|
|
1095
|
+
"Replace first occurrence", '"hello".replace("l","r")'),
|
|
1096
|
+
"String.chars": ("() -> [string]", "Split into individual characters","'ab'.chars() // ['a','b']"),
|
|
1097
|
+
"String.to_int": ("() -> int?", "Parse as integer or nil", '"42".to_int() // 42'),
|
|
1098
|
+
"String.to_float": ("() -> float?", "Parse as float or nil", '"3.14".to_float()'),
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _print_stdlib_docs(filter_str: str = None) -> int:
|
|
1103
|
+
"""
|
|
1104
|
+
v1.9.4: `inscript stdlib [FILTER]` — print stdlib function signatures.
|
|
1105
|
+
"""
|
|
1106
|
+
entries = STDLIB_SIGNATURES
|
|
1107
|
+
if filter_str:
|
|
1108
|
+
entries = {k: v for k, v in entries.items()
|
|
1109
|
+
if filter_str.lower() in k.lower()}
|
|
1110
|
+
if not entries:
|
|
1111
|
+
print(f"[InScript stdlib] No matches for '{filter_str}'")
|
|
1112
|
+
return 0
|
|
1113
|
+
|
|
1114
|
+
print(f"\nInScript Standard Library (v{VERSION})")
|
|
1115
|
+
print(f"{'='*70}")
|
|
1116
|
+
|
|
1117
|
+
last_group = None
|
|
1118
|
+
for name, info in sorted(entries.items()):
|
|
1119
|
+
group = name.split(".")[0] if "." in name else "Built-ins"
|
|
1120
|
+
if group != last_group:
|
|
1121
|
+
print(f"\n ── {group} ─────")
|
|
1122
|
+
last_group = group
|
|
1123
|
+
|
|
1124
|
+
if isinstance(info, str):
|
|
1125
|
+
# Abbreviated entry (just signature string)
|
|
1126
|
+
print(f" {name:<22} {info}")
|
|
1127
|
+
elif len(info) == 2:
|
|
1128
|
+
sig, desc = info
|
|
1129
|
+
print(f" {name:<22} {sig}")
|
|
1130
|
+
print(f" {'':22} # {desc}")
|
|
1131
|
+
else:
|
|
1132
|
+
sig, desc, example = info
|
|
1133
|
+
print(f" {name:<22} {sig}")
|
|
1134
|
+
print(f" {'':22} # {desc}")
|
|
1135
|
+
print(f" {'':22} {example}")
|
|
1136
|
+
|
|
1137
|
+
print(f"\n{'='*70}")
|
|
1138
|
+
total = len(entries)
|
|
1139
|
+
print(f" {total} stdlib item(s)")
|
|
1140
|
+
return 0
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
# ── spec document generator ───────────────────────────────────────────────────
|
|
1144
|
+
|
|
1145
|
+
LANGUAGE_SPEC = """# InScript Language Specification
|
|
1146
|
+
## Version {version}
|
|
1147
|
+
|
|
1148
|
+
> **Spec Freeze**: No new syntax will be added after v1.9.4.
|
|
1149
|
+
> Only bug fixes and performance improvements until v2.0.0.
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## 1. Lexical Structure
|
|
1154
|
+
|
|
1155
|
+
### 1.1 Comments
|
|
1156
|
+
- Line comments: `// text`
|
|
1157
|
+
- Block comments: `/* text */` (nestable)
|
|
1158
|
+
- Doc comments: `/// text` (parsed by `inscript doc`)
|
|
1159
|
+
|
|
1160
|
+
### 1.2 Keywords
|
|
1161
|
+
```
|
|
1162
|
+
let const fn return if else while for in match case
|
|
1163
|
+
struct enum interface type impl mixin extends implements
|
|
1164
|
+
true false nil throw try catch break continue defer yield
|
|
1165
|
+
pub async await scene import export with super self is as
|
|
1166
|
+
div not and or
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
### 1.3 Literals
|
|
1170
|
+
- **Integer**: `42`, `1_000_000`
|
|
1171
|
+
- **Float**: `3.14`, `1.0e-3`
|
|
1172
|
+
- **String**: `"hello"`, `'world'`, escape sequences `\n \t \\ \"`
|
|
1173
|
+
- **Bool**: `true`, `false`
|
|
1174
|
+
- **Nil**: `nil`
|
|
1175
|
+
|
|
1176
|
+
---
|
|
1177
|
+
|
|
1178
|
+
## 2. Types
|
|
1179
|
+
|
|
1180
|
+
### 2.1 Primitive Types
|
|
1181
|
+
`int` `float` `bool` `string` `nil` `void` `any` `never`
|
|
1182
|
+
|
|
1183
|
+
### 2.2 Composite Types
|
|
1184
|
+
- **Array**: `[T]` — e.g. `[int]`, `[string]`
|
|
1185
|
+
- **Dict**: `{K: V}` — e.g. `{string: int}`
|
|
1186
|
+
- **Optional**: `T?` — sugar for `T | nil`
|
|
1187
|
+
- **Union**: `A | B | C`
|
|
1188
|
+
- **Function**: `fn(T1, T2) -> R`
|
|
1189
|
+
|
|
1190
|
+
### 2.3 User-Defined Types
|
|
1191
|
+
- **Struct**: `struct Name {{ fields; methods }}`
|
|
1192
|
+
- **Enum**: `enum Name {{ Variant1; Variant2 }}`
|
|
1193
|
+
- **Interface**: `interface Name {{ fn method() -> R }}`
|
|
1194
|
+
- **Type alias**: `type ID = int`
|
|
1195
|
+
|
|
1196
|
+
---
|
|
1197
|
+
|
|
1198
|
+
## 3. Declarations
|
|
1199
|
+
|
|
1200
|
+
### 3.1 Variables
|
|
1201
|
+
```inscript
|
|
1202
|
+
let x: int = 42
|
|
1203
|
+
const MAX: int = 100
|
|
1204
|
+
let items: [string] = []
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
### 3.2 Functions
|
|
1208
|
+
```inscript
|
|
1209
|
+
fn add(a: int, b: int) -> int {{
|
|
1210
|
+
return a + b
|
|
1211
|
+
}}
|
|
1212
|
+
fn greet(name: string = "World") {{
|
|
1213
|
+
print("Hello, " + name)
|
|
1214
|
+
}}
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
### 3.3 Structs
|
|
1218
|
+
```inscript
|
|
1219
|
+
struct Player {{
|
|
1220
|
+
name: string = ""
|
|
1221
|
+
health: int = 100
|
|
1222
|
+
fn take_damage(dmg: int) {{
|
|
1223
|
+
self.health = self.health - dmg
|
|
1224
|
+
}}
|
|
1225
|
+
}}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
### 3.4 Enums
|
|
1229
|
+
```inscript
|
|
1230
|
+
enum Direction {{ North; South; East; West }}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### 3.5 Interfaces
|
|
1234
|
+
```inscript
|
|
1235
|
+
interface Drawable {{
|
|
1236
|
+
fn draw() -> void
|
|
1237
|
+
}}
|
|
1238
|
+
struct Sprite implements Drawable {{
|
|
1239
|
+
fn draw() {{ print("drawing") }}
|
|
1240
|
+
}}
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
---
|
|
1244
|
+
|
|
1245
|
+
## 4. Control Flow
|
|
1246
|
+
|
|
1247
|
+
```inscript
|
|
1248
|
+
if cond {{ }} else if cond {{ }} else {{ }}
|
|
1249
|
+
while cond {{ }}
|
|
1250
|
+
for item in collection {{ }}
|
|
1251
|
+
match value {{
|
|
1252
|
+
case Pattern {{ }}
|
|
1253
|
+
case _ {{ }} // wildcard
|
|
1254
|
+
}}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
---
|
|
1258
|
+
|
|
1259
|
+
## 5. Error Handling
|
|
1260
|
+
|
|
1261
|
+
```inscript
|
|
1262
|
+
try {{
|
|
1263
|
+
risky()
|
|
1264
|
+
}} catch e {{
|
|
1265
|
+
print("caught: " + e)
|
|
1266
|
+
}}
|
|
1267
|
+
throw "something went wrong"
|
|
1268
|
+
fn fatal() -> never {{ throw "fatal" }}
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
## 6. Module System
|
|
1274
|
+
|
|
1275
|
+
```inscript
|
|
1276
|
+
import "path/to/module"
|
|
1277
|
+
export fn public_api() {{ }}
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
---
|
|
1281
|
+
|
|
1282
|
+
## 7. Breaking Changes (v2.0.0 preview)
|
|
1283
|
+
|
|
1284
|
+
- `null` removed — use `nil`
|
|
1285
|
+
- `div` operator removed — use `//`
|
|
1286
|
+
- Bare `array` type without element type is an error
|
|
1287
|
+
- `--no-typecheck` CLI flag removed — use `--unsafe-no-check`
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
*Generated by InScript {version}*
|
|
1292
|
+
"""
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _generate_spec(output_path: str = None) -> int:
|
|
1296
|
+
"""
|
|
1297
|
+
v1.9.4: `inscript spec [--out FILE]` — print the language spec document.
|
|
1298
|
+
"""
|
|
1299
|
+
content = LANGUAGE_SPEC.replace("{version}", VERSION)
|
|
1300
|
+
if output_path:
|
|
1301
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|
1302
|
+
try:
|
|
1303
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
1304
|
+
f.write(content)
|
|
1305
|
+
print(f"[InScript spec] Spec written to '{output_path}'")
|
|
1306
|
+
return 0
|
|
1307
|
+
except OSError as e:
|
|
1308
|
+
print(f"[InScript spec] Cannot write: {e}", file=sys.stderr)
|
|
1309
|
+
return 1
|
|
1310
|
+
else:
|
|
1311
|
+
print(content)
|
|
1312
|
+
return 0
|
|
1313
|
+
|
|
438
1314
|
def run_source(source: str, filename: str = "<stdin>",
|
|
439
1315
|
type_check: bool = True,
|
|
440
1316
|
no_warn: bool = False,
|
|
@@ -694,6 +1570,20 @@ Examples:
|
|
|
694
1570
|
help="v1.9.2: Validate inscript.toml (default: current dir)")
|
|
695
1571
|
parser.add_argument("--lock", metavar="DIR", nargs="?", const=".",
|
|
696
1572
|
help="v1.9.2: Generate inscript.lock from inscript.toml")
|
|
1573
|
+
parser.add_argument("--doc", metavar="FILE_OR_DIR",
|
|
1574
|
+
help="v1.9.3: Generate Markdown docs from /// doc comments")
|
|
1575
|
+
parser.add_argument("--doc-out", metavar="DIR", default=None,
|
|
1576
|
+
help="v1.9.3: Output directory for --doc (default: stdout / docs/api/)")
|
|
1577
|
+
parser.add_argument("--errors", metavar="PREFIX", nargs="?", const="",
|
|
1578
|
+
help="v1.9.4: Print error code catalogue (optional prefix filter, e.g. E003)")
|
|
1579
|
+
parser.add_argument("--changelog", metavar="RANGE",
|
|
1580
|
+
help="v1.9.4: Print changelog for version range e.g. v1.6.0..v1.9.4")
|
|
1581
|
+
parser.add_argument("--benchmark", metavar="NAME", nargs="?", const="",
|
|
1582
|
+
help="v1.9.4: Run performance benchmark suite (optional name filter)")
|
|
1583
|
+
parser.add_argument("--stdlib", metavar="FILTER", nargs="?", const="",
|
|
1584
|
+
help="v1.9.4: Print stdlib function signatures (optional filter)")
|
|
1585
|
+
parser.add_argument("--spec", metavar="FILE", nargs="?", const=None,
|
|
1586
|
+
help="v1.9.4: Print language spec (optional --spec FILE to write to file)")
|
|
697
1587
|
parser.add_argument("--strict", action="store_true",
|
|
698
1588
|
help="v1.6.0: Strict mode — all warnings become errors, no implicit any")
|
|
699
1589
|
parser.add_argument("--tokens", action="store_true", help="Print lexer token stream")
|
|
@@ -859,6 +1749,19 @@ Examples:
|
|
|
859
1749
|
return _validate_manifest(args.validate)
|
|
860
1750
|
if getattr(args, 'lock', None) is not None:
|
|
861
1751
|
return _generate_lockfile(args.lock)
|
|
1752
|
+
if getattr(args, 'doc', None):
|
|
1753
|
+
return _generate_docs(args.doc, output_dir=getattr(args, 'doc_out', None))
|
|
1754
|
+
if getattr(args, 'errors', None) is not None:
|
|
1755
|
+
return _print_error_catalogue(filter_prefix=args.errors or None)
|
|
1756
|
+
if getattr(args, 'changelog', None):
|
|
1757
|
+
return _generate_changelog_range(args.changelog)
|
|
1758
|
+
if getattr(args, 'benchmark', None) is not None:
|
|
1759
|
+
return _run_benchmarks(filter_name=args.benchmark or None)
|
|
1760
|
+
if getattr(args, 'stdlib', None) is not None:
|
|
1761
|
+
return _print_stdlib_docs(filter_str=args.stdlib or None)
|
|
1762
|
+
if getattr(args, 'spec', None) is not None or '--spec' in sys.argv:
|
|
1763
|
+
out = args.spec if hasattr(args, 'spec') and isinstance(args.spec, str) else None
|
|
1764
|
+
return _generate_spec(output_path=out)
|
|
862
1765
|
|
|
863
1766
|
# v1.9.1: --no-typecheck is deprecated — emit a warning and honour it
|
|
864
1767
|
if getattr(args, 'no_typecheck', False):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "1.9.
|
|
7
|
+
version = "1.9.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.9.
|
|
43
|
+
VERSION = "1.9.4"
|
|
44
44
|
|
|
45
45
|
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
46
46
|
def _c(code, text):
|
|
@@ -4,14 +4,26 @@ InScript — setup.py
|
|
|
4
4
|
pyproject.toml is the canonical config for v1.0.23+
|
|
5
5
|
This file is kept for compatibility with older pip versions.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
VERSION is read dynamically from inscript.py so setup.py, pyproject.toml,
|
|
8
|
+
and inscript.py always agree — no more manual sync needed.
|
|
9
9
|
"""
|
|
10
|
-
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from setuptools import setup
|
|
13
|
+
|
|
14
|
+
def _read_version():
|
|
15
|
+
src = Path(__file__).parent / "inscript.py"
|
|
16
|
+
m = re.search(r'^VERSION\s*=\s*["\']([^"\']+)["\']',
|
|
17
|
+
src.read_text(encoding="utf-8"), re.M)
|
|
18
|
+
if not m:
|
|
19
|
+
raise RuntimeError("VERSION not found in inscript.py")
|
|
20
|
+
return m.group(1)
|
|
21
|
+
|
|
22
|
+
VERSION = _read_version()
|
|
11
23
|
|
|
12
24
|
setup(
|
|
13
25
|
name = "inscript-lang",
|
|
14
|
-
version =
|
|
26
|
+
version = VERSION,
|
|
15
27
|
author = "Shreyasi Sarkar",
|
|
16
28
|
description = "InScript — a game-focused scripting language for 2D games",
|
|
17
29
|
long_description = open("README.md", encoding="utf-8").read(),
|
|
@@ -24,6 +36,9 @@ setup(
|
|
|
24
36
|
"Intended Audience :: Developers",
|
|
25
37
|
"License :: OSI Approved :: MIT License",
|
|
26
38
|
"Programming Language :: Python :: 3",
|
|
39
|
+
"Programming Language :: Python :: 3.10",
|
|
40
|
+
"Programming Language :: Python :: 3.11",
|
|
41
|
+
"Programming Language :: Python :: 3.12",
|
|
27
42
|
"Topic :: Games/Entertainment",
|
|
28
43
|
"Topic :: Software Development :: Interpreters",
|
|
29
44
|
],
|
|
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
|