spice-lang 0.2.2__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.
- spice_lang-0.2.2/PKG-INFO +27 -0
- spice_lang-0.2.2/pyproject.toml +48 -0
- spice_lang-0.2.2/setup.cfg +4 -0
- spice_lang-0.2.2/spice/__init__.py +8 -0
- spice_lang-0.2.2/spice/annotations/__init__.py +15 -0
- spice_lang-0.2.2/spice/annotations/base.py +24 -0
- spice_lang-0.2.2/spice/annotations/builtins/__init__.py +6 -0
- spice_lang-0.2.2/spice/annotations/builtins/print_on_call.py +34 -0
- spice_lang-0.2.2/spice/annotations/registry.py +26 -0
- spice_lang-0.2.2/spice/cli/__init__.py +3 -0
- spice_lang-0.2.2/spice/cli/cli_handler.py +38 -0
- spice_lang-0.2.2/spice/compilation/__init__.py +8 -0
- spice_lang-0.2.2/spice/compilation/build_flags.py +105 -0
- spice_lang-0.2.2/spice/compilation/checks/__init__.py +22 -0
- spice_lang-0.2.2/spice/compilation/checks/annotation_stage.py +87 -0
- spice_lang-0.2.2/spice/compilation/checks/compile_time_check.py +16 -0
- spice_lang-0.2.2/spice/compilation/checks/final_checker.py +189 -0
- spice_lang-0.2.2/spice/compilation/checks/generic_bound_checker.py +133 -0
- spice_lang-0.2.2/spice/compilation/checks/interface_checker.py +149 -0
- spice_lang-0.2.2/spice/compilation/checks/method_overload_resolver.py +117 -0
- spice_lang-0.2.2/spice/compilation/checks/symbol_table_builder.py +223 -0
- spice_lang-0.2.2/spice/compilation/checks/type_checker.py +321 -0
- spice_lang-0.2.2/spice/compilation/cython_compiler.py +143 -0
- spice_lang-0.2.2/spice/compilation/pipeline.py +317 -0
- spice_lang-0.2.2/spice/compilation/spicefile.py +154 -0
- spice_lang-0.2.2/spice/compilation/symbol_table.py +104 -0
- spice_lang-0.2.2/spice/errors.py +39 -0
- spice_lang-0.2.2/spice/lexer/__init__.py +6 -0
- spice_lang-0.2.2/spice/lexer/follow_set.py +586 -0
- spice_lang-0.2.2/spice/lexer/tokenizer.py +255 -0
- spice_lang-0.2.2/spice/lexer/tokens.py +152 -0
- spice_lang-0.2.2/spice/parser/__init__.py +8 -0
- spice_lang-0.2.2/spice/parser/ast_nodes.py +633 -0
- spice_lang-0.2.2/spice/parser/expression_parser.py +741 -0
- spice_lang-0.2.2/spice/parser/parser.py +1435 -0
- spice_lang-0.2.2/spice/printils.py +34 -0
- spice_lang-0.2.2/spice/transformer/__init__.py +5 -0
- spice_lang-0.2.2/spice/transformer/transformer.py +1209 -0
- spice_lang-0.2.2/spice/utils/__init__.py +3 -0
- spice_lang-0.2.2/spice/utils/hashing.py +9 -0
- spice_lang-0.2.2/spice_lang.egg-info/PKG-INFO +27 -0
- spice_lang-0.2.2/spice_lang.egg-info/SOURCES.txt +60 -0
- spice_lang-0.2.2/spice_lang.egg-info/dependency_links.txt +1 -0
- spice_lang-0.2.2/spice_lang.egg-info/entry_points.txt +2 -0
- spice_lang-0.2.2/spice_lang.egg-info/requires.txt +12 -0
- spice_lang-0.2.2/spice_lang.egg-info/top_level.txt +1 -0
- spice_lang-0.2.2/tests/test_annotations.py +141 -0
- spice_lang-0.2.2/tests/test_class_keywords.py +445 -0
- spice_lang-0.2.2/tests/test_constructor_transform.py +162 -0
- spice_lang-0.2.2/tests/test_cython_emit.py +222 -0
- spice_lang-0.2.2/tests/test_data_classes.py +120 -0
- spice_lang-0.2.2/tests/test_enums.py +113 -0
- spice_lang-0.2.2/tests/test_final_checker.py +99 -0
- spice_lang-0.2.2/tests/test_generic_bounds.py +104 -0
- spice_lang-0.2.2/tests/test_generics.py +127 -0
- spice_lang-0.2.2/tests/test_lexer.py +329 -0
- spice_lang-0.2.2/tests/test_parcer.py +179 -0
- spice_lang-0.2.2/tests/test_pipeline_imports.py +57 -0
- spice_lang-0.2.2/tests/test_slice_parsing.py +236 -0
- spice_lang-0.2.2/tests/test_transformer.py +515 -0
- spice_lang-0.2.2/tests/test_type_checker.py +140 -0
- spice_lang-0.2.2/tests/testutils.py +206 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spice-lang
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Python but with all the spice
|
|
5
|
+
Author: Reclipse
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Requires-Dist: click>=8.0
|
|
18
|
+
Requires-Dist: colorama>=0.4
|
|
19
|
+
Requires-Dist: rites>=0.5.4
|
|
20
|
+
Requires-Dist: multipledispatch>=1.0.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest<9,>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=25.1; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
26
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
27
|
+
Requires-Dist: installer>=0.7; extra == "dev"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spice-lang"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Python but with all the spice"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Reclipse" }
|
|
11
|
+
]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"click>=8.0",
|
|
15
|
+
"colorama>=0.4",
|
|
16
|
+
"rites>=0.5.4",
|
|
17
|
+
"multipledispatch>=1.0.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Operating System :: OS Independent"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0,<9",
|
|
36
|
+
"pytest-cov>=7.0",
|
|
37
|
+
"black>=25.1",
|
|
38
|
+
"mypy>=1.0",
|
|
39
|
+
"build>=1.0",
|
|
40
|
+
"installer>=0.7",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
spicy = "spice.cli.cli_handler:from_cli"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["."]
|
|
48
|
+
include = ["spice*"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Spice compiler - A Python superset with static typing features."""
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
|
|
4
|
+
from spice import cli, lexer, parser, transformer, errors, printils, compilation
|
|
5
|
+
|
|
6
|
+
__version__ = version("spice-lang")
|
|
7
|
+
|
|
8
|
+
__all__ = ["cli", "lexer", "parser", "transformer", "compilation", "errors", "printils"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Spice annotation framework: compile-time processors and their registry.
|
|
2
|
+
|
|
3
|
+
`@name` annotations are runtime (emitted as Python decorators by the transformer);
|
|
4
|
+
`@!name` annotations are compile-time and handled here by registered processors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from spice.annotations.base import AnnotationProcessor
|
|
8
|
+
from spice.annotations.registry import register, get_processor, all_processors
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AnnotationProcessor",
|
|
12
|
+
"register",
|
|
13
|
+
"get_processor",
|
|
14
|
+
"all_processors",
|
|
15
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Base class for compile-time annotation processors."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnnotationProcessor(ABC):
|
|
7
|
+
"""A compile-time annotation handler.
|
|
8
|
+
|
|
9
|
+
Subclasses set `name` (matched against `@!name`) and optionally `targets`
|
|
10
|
+
(a tuple of AST node types it may annotate; empty means any declaration).
|
|
11
|
+
Annotation arguments bind to `process`'s keyword parameters, decorator-factory
|
|
12
|
+
style: `@!print_on_call(time_format="%H:%M:%S")` -> `process(..., time_format=...)`.
|
|
13
|
+
|
|
14
|
+
`process` mutates `node` in place (and may edit AST fields directly). The
|
|
15
|
+
annotation stage strips the annotation afterward.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
name: str = ""
|
|
19
|
+
targets: tuple = () # allowed node types; () = any
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def process(self, node, file, **kwargs) -> None:
|
|
23
|
+
"""Apply this annotation to `node`. `file` is the SpiceFile (utilities live there)."""
|
|
24
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Built-in `@!print_on_call` compile-time annotation.
|
|
2
|
+
|
|
3
|
+
Injects a timestamped log line at the top of the annotated function's body:
|
|
4
|
+
|
|
5
|
+
@!print_on_call(time_format="%H:%M:%S")
|
|
6
|
+
def greet(name: str) -> None { ... }
|
|
7
|
+
|
|
8
|
+
becomes (roughly):
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
|
|
12
|
+
def greet(name: str) -> None:
|
|
13
|
+
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] greet called")
|
|
14
|
+
...
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from spice.annotations import AnnotationProcessor, register
|
|
18
|
+
from spice.parser.ast_nodes import FunctionDeclaration
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@register
|
|
22
|
+
class PrintOnCall(AnnotationProcessor):
|
|
23
|
+
name = "print_on_call"
|
|
24
|
+
targets = (FunctionDeclaration,)
|
|
25
|
+
|
|
26
|
+
def process(self, node, file, *, time_format: str = "%H:%M:%S") -> None:
|
|
27
|
+
if node.body is None:
|
|
28
|
+
node.body = []
|
|
29
|
+
|
|
30
|
+
file.ensure_import("datetime")
|
|
31
|
+
|
|
32
|
+
timestamp = f"datetime.datetime.now().strftime({time_format!r})"
|
|
33
|
+
code = f'print(f"[{{{timestamp}}}] {node.name} called")'
|
|
34
|
+
node.body.insert(0, file.raw_python(code))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Registry of compile-time annotation processors, keyed by name."""
|
|
2
|
+
|
|
3
|
+
from spice.annotations.base import AnnotationProcessor
|
|
4
|
+
|
|
5
|
+
_REGISTRY: dict[str, AnnotationProcessor] = {}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(cls):
|
|
9
|
+
"""Class decorator: instantiate the processor and register it by `name`."""
|
|
10
|
+
instance = cls()
|
|
11
|
+
if not getattr(instance, "name", ""):
|
|
12
|
+
raise ValueError(f"Annotation processor {cls.__name__} must define a non-empty 'name'")
|
|
13
|
+
if instance.name in _REGISTRY:
|
|
14
|
+
raise ValueError(f"Duplicate annotation processor registered for '@!{instance.name}'")
|
|
15
|
+
_REGISTRY[instance.name] = instance
|
|
16
|
+
return cls
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_processor(name: str):
|
|
20
|
+
"""Return the registered processor for `name`, or None."""
|
|
21
|
+
return _REGISTRY.get(name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def all_processors() -> dict:
|
|
25
|
+
"""A copy of the current registry."""
|
|
26
|
+
return dict(_REGISTRY)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from spice.printils import spice_compiler_log, pipeline_log, spam_console
|
|
2
|
+
from spice.compilation import BuildFlags, SpiceFile, SpicePipeline
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
@click.argument('source', type=click.Path(exists=True))
|
|
11
|
+
@click.option('-o', '--output', type=click.Path(), help='Output file (default: <source>.py)')
|
|
12
|
+
@click.option('-e', '--emit', type=click.Choice(['py', 'pyx', 'exe']), default='py', help='Compilation target (default: py)')
|
|
13
|
+
@click.option('-k', '--keep-intermidiates', is_flag=True, help='Keep intermidiates generated during compilation (only applies to exe emit)')
|
|
14
|
+
@click.option('-c', '--check', is_flag=True, help='Check syntax without generating output')
|
|
15
|
+
@click.option('-w', '--watch', is_flag=True, help='Watch file for changes. This option disables verbosity.')
|
|
16
|
+
@click.option('-v', '--verbose', is_flag=True, help='Verbose output')
|
|
17
|
+
@click.option('--runtime-checks', is_flag=True, help='Add runtime type checking to output')
|
|
18
|
+
@click.version_option(package_name='spice-lang', prog_name='spicy')
|
|
19
|
+
def from_cli(source: str, output: Optional[str], emit: str, keep_intermidiates: bool, check: bool, watch: bool, verbose: bool, runtime_checks: bool):
|
|
20
|
+
"""Compile Spice (.spc) files to Python, Cython, or standalone executables."""
|
|
21
|
+
flags: BuildFlags = BuildFlags(
|
|
22
|
+
source=Path(source),
|
|
23
|
+
output=Path(output) if output else None,
|
|
24
|
+
emit=emit,
|
|
25
|
+
keep_intermidiates=keep_intermidiates,
|
|
26
|
+
check=check,
|
|
27
|
+
watch=watch,
|
|
28
|
+
verbose=verbose,
|
|
29
|
+
runtime_checks=runtime_checks
|
|
30
|
+
)
|
|
31
|
+
spam_console(flags.verbose)
|
|
32
|
+
|
|
33
|
+
spice_compiler_log.custom("spice", f"Compiling from entry point: {flags.source.resolve().as_posix()}")
|
|
34
|
+
|
|
35
|
+
spice_tree: SpiceFile = SpicePipeline.walk(flags.source, None, flags)
|
|
36
|
+
SpicePipeline.verify_and_write(spice_tree, flags)
|
|
37
|
+
|
|
38
|
+
spice_compiler_log.custom("spice", "Compilation finished successfully.")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Compilation module for Spice"""
|
|
2
|
+
|
|
3
|
+
from spice.compilation.build_flags import BuildFlags
|
|
4
|
+
from spice.compilation.spicefile import SpiceFile
|
|
5
|
+
from spice.compilation.pipeline import SpicePipeline
|
|
6
|
+
from spice.compilation import checks
|
|
7
|
+
|
|
8
|
+
__all__ = ["checks", "BuildFlags", "SpiceFile", "SpicePipeline"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Build configuration for the Spice compiler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _core_field(key: str) -> property:
|
|
10
|
+
"""Build a get/set wrapper (property) for a core field backed by `_data`."""
|
|
11
|
+
def getter(self: "BuildFlags") -> Any:
|
|
12
|
+
return self._data.get(key)
|
|
13
|
+
|
|
14
|
+
def setter(self: "BuildFlags", value: Any) -> None:
|
|
15
|
+
self._data[key] = value
|
|
16
|
+
|
|
17
|
+
getter.__name__ = key
|
|
18
|
+
return property(getter, setter, doc=f"Core build flag '{key}'.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BuildFlags:
|
|
22
|
+
"""Compiler build options.
|
|
23
|
+
|
|
24
|
+
Core fields:
|
|
25
|
+
source: Path to the source .spc file or a directory with __main__.spc
|
|
26
|
+
output: Output directory or file path
|
|
27
|
+
emit: Compilation target ('py', 'pyx', or 'exe')
|
|
28
|
+
keep_intermediates: Keep intermediates generated during exe compilation
|
|
29
|
+
check: Syntax validation without code generation
|
|
30
|
+
watch: File watching mode
|
|
31
|
+
verbose: Detailed logging of pipeline stages
|
|
32
|
+
runtime_checks: Inject runtime type checking decorators
|
|
33
|
+
|
|
34
|
+
Extra keys (for plugins/tools) live in the same dict and are reached with
|
|
35
|
+
get()/set(), item access (flags["key"]) or as_dict().
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
CORE_DEFAULTS: dict[str, Any] = {
|
|
39
|
+
"source": None,
|
|
40
|
+
"output": None,
|
|
41
|
+
"emit": "py",
|
|
42
|
+
"keep_intermediates": False,
|
|
43
|
+
"check": False,
|
|
44
|
+
"watch": False,
|
|
45
|
+
"verbose": False,
|
|
46
|
+
"runtime_checks": False,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
source: Path,
|
|
52
|
+
output: Optional[Path] = None,
|
|
53
|
+
emit: str = "py",
|
|
54
|
+
keep_intermidiates: bool = False,
|
|
55
|
+
check: bool = False,
|
|
56
|
+
watch: bool = False,
|
|
57
|
+
verbose: bool = False,
|
|
58
|
+
runtime_checks: bool = False,
|
|
59
|
+
**extra: Any,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._data: dict[str, Any] = dict(self.CORE_DEFAULTS)
|
|
62
|
+
self._data.update({
|
|
63
|
+
"source": source,
|
|
64
|
+
"output": output,
|
|
65
|
+
"emit": emit,
|
|
66
|
+
"keep_intermediates": keep_intermidiates,
|
|
67
|
+
"check": check,
|
|
68
|
+
"watch": watch,
|
|
69
|
+
"verbose": verbose,
|
|
70
|
+
"runtime_checks": runtime_checks,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
self._data.update(extra)
|
|
74
|
+
|
|
75
|
+
source = _core_field("source")
|
|
76
|
+
output = _core_field("output")
|
|
77
|
+
emit = _core_field("emit")
|
|
78
|
+
keep_intermediates = _core_field("keep_intermediates")
|
|
79
|
+
check = _core_field("check")
|
|
80
|
+
watch = _core_field("watch")
|
|
81
|
+
verbose = _core_field("verbose")
|
|
82
|
+
runtime_checks = _core_field("runtime_checks")
|
|
83
|
+
|
|
84
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
85
|
+
return self._data.get(key, default)
|
|
86
|
+
|
|
87
|
+
def set(self, key: str, value: Any) -> "BuildFlags":
|
|
88
|
+
self._data[key] = value
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def __getitem__(self, key: str) -> Any:
|
|
92
|
+
return self._data[key]
|
|
93
|
+
|
|
94
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
95
|
+
self._data[key] = value
|
|
96
|
+
|
|
97
|
+
def __contains__(self, key: str) -> bool:
|
|
98
|
+
return key in self._data
|
|
99
|
+
|
|
100
|
+
def as_dict(self) -> dict[str, Any]:
|
|
101
|
+
"""A shallow copy of the backing dict (core fields + extras)."""
|
|
102
|
+
return dict(self._data)
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
return f"BuildFlags({self._data!r})"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Compile time checks for Spice source code."""
|
|
2
|
+
|
|
3
|
+
from spice.compilation.checks.final_checker import FinalChecker
|
|
4
|
+
from spice.compilation.checks.interface_checker import InterfaceChecker, CheckError
|
|
5
|
+
from spice.compilation.checks.method_overload_resolver import MethodOverloadResolver
|
|
6
|
+
from spice.compilation.checks.compile_time_check import CompileTimeCheck
|
|
7
|
+
from spice.compilation.checks.symbol_table_builder import SymbolTableBuilder
|
|
8
|
+
from spice.compilation.checks.type_checker import TypeChecker
|
|
9
|
+
from spice.compilation.checks.generic_bound_checker import GenericBoundChecker
|
|
10
|
+
from spice.compilation.checks.annotation_stage import AnnotationStage
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CheckError",
|
|
14
|
+
"FinalChecker",
|
|
15
|
+
"InterfaceChecker",
|
|
16
|
+
"MethodOverloadResolver",
|
|
17
|
+
"SymbolTableBuilder",
|
|
18
|
+
"TypeChecker",
|
|
19
|
+
"GenericBoundChecker",
|
|
20
|
+
"CompileTimeCheck",
|
|
21
|
+
"AnnotationStage",
|
|
22
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Annotation lowering stage.
|
|
2
|
+
|
|
3
|
+
Runs compile-time (`@!`) annotation processors over the AST, strips the
|
|
4
|
+
processed annotations, and leaves runtime (`@`) annotations in place for the
|
|
5
|
+
transformer to emit as decorators. Implemented as a `CompileTimeCheck` so it
|
|
6
|
+
plugs into the pipeline and reports fatal errors the usual way.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
|
|
11
|
+
from spice.compilation.checks.compile_time_check import CompileTimeCheck
|
|
12
|
+
from spice.parser.ast_nodes import LiteralExpression
|
|
13
|
+
from spice.annotations import get_processor
|
|
14
|
+
import spice.annotations.builtins
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnnotationStage(CompileTimeCheck):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.errors = []
|
|
20
|
+
|
|
21
|
+
def check(self, file) -> bool:
|
|
22
|
+
self.errors = []
|
|
23
|
+
|
|
24
|
+
# Comp time processors are able to modify the code so we need to materialize nodes first
|
|
25
|
+
for node in list(file.walk()):
|
|
26
|
+
if getattr(node, "annotations", None):
|
|
27
|
+
self._process_node(node, file)
|
|
28
|
+
|
|
29
|
+
self.errors.extend(getattr(file, "diagnostics", []))
|
|
30
|
+
return not self.errors
|
|
31
|
+
|
|
32
|
+
def _process_node(self, node, file) -> None:
|
|
33
|
+
remaining = []
|
|
34
|
+
for ann in node.annotations:
|
|
35
|
+
if ann.retention != "compile_time":
|
|
36
|
+
remaining.append(ann) # runtime: left for the transformer
|
|
37
|
+
continue
|
|
38
|
+
self._apply(ann, node, file) # compile-time: applied, then stripped
|
|
39
|
+
node.annotations = remaining
|
|
40
|
+
|
|
41
|
+
def _apply(self, ann, node, file) -> None:
|
|
42
|
+
proc = get_processor(ann.name)
|
|
43
|
+
if proc is None:
|
|
44
|
+
self.errors.append(f"Unknown compile-time annotation '@!{ann.name}' (line {ann.line})")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
if proc.targets and not isinstance(node, proc.targets):
|
|
48
|
+
allowed = ", ".join(t.__name__ for t in proc.targets)
|
|
49
|
+
self.errors.append(
|
|
50
|
+
f"'@!{ann.name}' cannot annotate {type(node).__name__}; allowed: {allowed} (line {ann.line})"
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
values = [self._literal(a) for a in ann.args]
|
|
56
|
+
kwargs = {key: self._literal(value) for key, value in ann.kwargs.items()}
|
|
57
|
+
except TypeError as exc:
|
|
58
|
+
self.errors.append(f"'@!{ann.name}': {exc} (line {ann.line})")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Validate arity / names against process(node, file, **bound) before any side effects.
|
|
62
|
+
try:
|
|
63
|
+
inspect.signature(proc.process).bind(node, file, *values, **kwargs)
|
|
64
|
+
except TypeError as exc:
|
|
65
|
+
self.errors.append(f"'@!{ann.name}': {exc} (line {ann.line})")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
proc.process(node, file, *values, **kwargs)
|
|
69
|
+
|
|
70
|
+
def _literal(self, expr):
|
|
71
|
+
"""Evaluate a compile-time-literal argument expression to a Python value."""
|
|
72
|
+
if not isinstance(expr, LiteralExpression):
|
|
73
|
+
raise TypeError(f"argument must be a compile-time literal, got {type(expr).__name__}")
|
|
74
|
+
|
|
75
|
+
lt = expr.literal_type
|
|
76
|
+
value = expr.value
|
|
77
|
+
if lt == "number":
|
|
78
|
+
text = str(value)
|
|
79
|
+
return float(text) if ("." in text or "e" in text.lower()) else int(text)
|
|
80
|
+
if lt in ("string", "fstring"):
|
|
81
|
+
s = str(value)
|
|
82
|
+
if len(s) >= 2 and s[0] == s[-1] and s[0] in "\"'":
|
|
83
|
+
s = s[1:-1]
|
|
84
|
+
return s
|
|
85
|
+
if lt == "boolean":
|
|
86
|
+
return bool(value)
|
|
87
|
+
return value
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from spice.compilation.spicefile import SpiceFile
|
|
3
|
+
|
|
4
|
+
class CompileTimeCheck(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
def check(self, file: SpiceFile) -> bool:
|
|
7
|
+
"""Perform the compile-time check on the given SpiceFile.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
file (SpiceFile): The Spice file to check.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
bool: True if the check passes, False otherwise.
|
|
14
|
+
"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from dataclasses import fields, is_dataclass
|
|
2
|
+
from typing import Dict, List, Optional, Set, TYPE_CHECKING, Union
|
|
3
|
+
|
|
4
|
+
from spice.parser.ast_nodes import (
|
|
5
|
+
ASTNode,
|
|
6
|
+
AssignmentExpression,
|
|
7
|
+
ClassDeclaration,
|
|
8
|
+
ExpressionStatement,
|
|
9
|
+
FinalDeclaration,
|
|
10
|
+
FunctionDeclaration,
|
|
11
|
+
IdentifierExpression,
|
|
12
|
+
)
|
|
13
|
+
from spice.compilation.checks.compile_time_check import CompileTimeCheck
|
|
14
|
+
from spice.compilation.checks.interface_checker import CheckError
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from spice.compilation.spicefile import SpiceFile
|
|
18
|
+
|
|
19
|
+
class FinalChecker(CompileTimeCheck):
|
|
20
|
+
"""Compile-time checker for final variable reassignments."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._reset_state()
|
|
24
|
+
|
|
25
|
+
def _reset_state(self):
|
|
26
|
+
self.final_variables: Dict[str, Set[str]] = {'global': set()}
|
|
27
|
+
self.current_scope = 'global'
|
|
28
|
+
self.errors: List[Union[str, CheckError]] = []
|
|
29
|
+
self.class_nodes: Dict[str, ClassDeclaration] = {}
|
|
30
|
+
self.final_methods_by_class: Dict[str, Dict[str, FunctionDeclaration]] = {}
|
|
31
|
+
|
|
32
|
+
def check(self, file: "SpiceFile") -> bool:
|
|
33
|
+
self._reset_state()
|
|
34
|
+
self._collect_class_metadata(file.ast.body)
|
|
35
|
+
for node in file.ast.body:
|
|
36
|
+
self._visit_node(node)
|
|
37
|
+
return not self.errors
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def enter_scope(self, scope_name: str):
|
|
41
|
+
"""Enter a new scope (function/class)."""
|
|
42
|
+
self.current_scope = scope_name
|
|
43
|
+
if scope_name not in self.final_variables:
|
|
44
|
+
self.final_variables[scope_name] = set()
|
|
45
|
+
|
|
46
|
+
def exit_scope(self):
|
|
47
|
+
"""Exit current scope."""
|
|
48
|
+
self.current_scope = 'global'
|
|
49
|
+
|
|
50
|
+
def register_final(self, var_name: str):
|
|
51
|
+
"""Register a variable as final in current scope."""
|
|
52
|
+
if self.current_scope not in self.final_variables:
|
|
53
|
+
self.final_variables[self.current_scope] = set()
|
|
54
|
+
self.final_variables[self.current_scope].add(var_name)
|
|
55
|
+
|
|
56
|
+
def check_assignment(self, var_name: str, line: int = 0, column: int = 0):
|
|
57
|
+
"""Check if assignment to variable is allowed."""
|
|
58
|
+
scope_vars = self.final_variables.get(self.current_scope, set())
|
|
59
|
+
global_vars = self.final_variables.get('global', set())
|
|
60
|
+
|
|
61
|
+
if var_name in scope_vars or var_name in global_vars:
|
|
62
|
+
self.errors.append(CheckError(
|
|
63
|
+
message=f"Cannot reassign final variable '{var_name}'",
|
|
64
|
+
line=line,
|
|
65
|
+
column=column
|
|
66
|
+
))
|
|
67
|
+
return False
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def _visit_node(self, node):
|
|
71
|
+
"""Visit a node and check for violations."""
|
|
72
|
+
if isinstance(node, FinalDeclaration):
|
|
73
|
+
if isinstance(node.target, IdentifierExpression):
|
|
74
|
+
self.register_final(node.target.name)
|
|
75
|
+
|
|
76
|
+
elif isinstance(node, ExpressionStatement):
|
|
77
|
+
self._visit_expression(node.expression)
|
|
78
|
+
elif isinstance(node, AssignmentExpression):
|
|
79
|
+
self._handle_assignment(node)
|
|
80
|
+
elif isinstance(node, FunctionDeclaration):
|
|
81
|
+
old_scope = self.current_scope
|
|
82
|
+
self.enter_scope(node.name)
|
|
83
|
+
for stmt in node.body:
|
|
84
|
+
self._visit_node(stmt)
|
|
85
|
+
self.current_scope = old_scope
|
|
86
|
+
|
|
87
|
+
elif isinstance(node, ClassDeclaration):
|
|
88
|
+
old_scope = self.current_scope
|
|
89
|
+
self.enter_scope(node.name)
|
|
90
|
+
self._check_final_method_overrides(node)
|
|
91
|
+
for member in node.body:
|
|
92
|
+
self._visit_node(member)
|
|
93
|
+
self.current_scope = old_scope
|
|
94
|
+
|
|
95
|
+
if hasattr(node, 'body') and isinstance(node.body, list):
|
|
96
|
+
for child in node.body:
|
|
97
|
+
self._visit_node(child)
|
|
98
|
+
|
|
99
|
+
def _visit_expression(self, expr: Optional[ASTNode]):
|
|
100
|
+
"""Traverse expressions searching for assignments."""
|
|
101
|
+
if expr is None:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if isinstance(expr, AssignmentExpression):
|
|
105
|
+
self._handle_assignment(expr)
|
|
106
|
+
|
|
107
|
+
if is_dataclass(expr):
|
|
108
|
+
for field in fields(expr):
|
|
109
|
+
value = getattr(expr, field.name)
|
|
110
|
+
self._visit_expression_field(value)
|
|
111
|
+
|
|
112
|
+
def _visit_expression_field(self, value):
|
|
113
|
+
if isinstance(value, list):
|
|
114
|
+
for item in value:
|
|
115
|
+
if isinstance(item, ASTNode):
|
|
116
|
+
self._visit_expression(item)
|
|
117
|
+
elif isinstance(value, ASTNode):
|
|
118
|
+
self._visit_expression(value)
|
|
119
|
+
|
|
120
|
+
def _handle_assignment(self, node: AssignmentExpression):
|
|
121
|
+
if isinstance(node.target, IdentifierExpression):
|
|
122
|
+
self.check_assignment(node.target.name, node.line, node.column)
|
|
123
|
+
self._visit_expression(node.value)
|
|
124
|
+
|
|
125
|
+
def _collect_class_metadata(self, nodes: List[ASTNode]):
|
|
126
|
+
"""Collect class declarations and their final methods for later checks."""
|
|
127
|
+
for node in nodes:
|
|
128
|
+
if isinstance(node, ClassDeclaration):
|
|
129
|
+
self.class_nodes[node.name] = node
|
|
130
|
+
|
|
131
|
+
final_methods = {
|
|
132
|
+
member.name: member
|
|
133
|
+
for member in node.body
|
|
134
|
+
if isinstance(member, FunctionDeclaration) and member.is_final
|
|
135
|
+
}
|
|
136
|
+
if final_methods:
|
|
137
|
+
self.final_methods_by_class[node.name] = final_methods
|
|
138
|
+
|
|
139
|
+
self._collect_class_metadata(node.body)
|
|
140
|
+
elif hasattr(node, 'body') and isinstance(node.body, list):
|
|
141
|
+
self._collect_class_metadata(node.body)
|
|
142
|
+
|
|
143
|
+
def _check_final_method_overrides(self, class_node: ClassDeclaration):
|
|
144
|
+
"""Ensure subclasses do not override final methods declared in parents."""
|
|
145
|
+
inherited_final_methods = self._collect_inherited_final_methods(class_node)
|
|
146
|
+
if not inherited_final_methods:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
for member in class_node.body:
|
|
150
|
+
if isinstance(member, FunctionDeclaration) and member.name in inherited_final_methods:
|
|
151
|
+
base_name = inherited_final_methods[member.name]
|
|
152
|
+
self.errors.append(CheckError(
|
|
153
|
+
message=f"Class '{class_node.name}' cannot override final method '{member.name}' defined in '{base_name}'",
|
|
154
|
+
line=member.line,
|
|
155
|
+
column=member.column
|
|
156
|
+
))
|
|
157
|
+
|
|
158
|
+
def _collect_inherited_final_methods(self, class_node: ClassDeclaration) -> Dict[str, str]:
|
|
159
|
+
"""Gather final methods from all parent classes."""
|
|
160
|
+
inherited: Dict[str, str] = {}
|
|
161
|
+
|
|
162
|
+
for base_name in class_node.bases:
|
|
163
|
+
base_methods = self._collect_final_methods_from_base(base_name, set())
|
|
164
|
+
for method_name, origin in base_methods.items():
|
|
165
|
+
inherited.setdefault(method_name, origin)
|
|
166
|
+
|
|
167
|
+
return inherited
|
|
168
|
+
|
|
169
|
+
def _collect_final_methods_from_base(self, base_name: str, visited: Set[str]) -> Dict[str, str]:
|
|
170
|
+
"""Collect final methods from the given base class and its parents."""
|
|
171
|
+
if base_name in visited:
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
visited.add(base_name)
|
|
175
|
+
methods: Dict[str, str] = {
|
|
176
|
+
method_name: base_name
|
|
177
|
+
for method_name in self.final_methods_by_class.get(base_name, {})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
base_node = self.class_nodes.get(base_name)
|
|
181
|
+
if not base_node:
|
|
182
|
+
return methods
|
|
183
|
+
|
|
184
|
+
for ancestor in base_node.bases:
|
|
185
|
+
ancestor_methods = self._collect_final_methods_from_base(ancestor, set(visited))
|
|
186
|
+
for method_name, origin in ancestor_methods.items():
|
|
187
|
+
methods.setdefault(method_name, origin)
|
|
188
|
+
|
|
189
|
+
return methods
|