snail-lang 0.5.0__tar.gz → 0.5.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.
Files changed (56) hide show
  1. {snail_lang-0.5.0 → snail_lang-0.5.2}/Cargo.lock +7 -7
  2. {snail_lang-0.5.0 → snail_lang-0.5.2}/PKG-INFO +1 -1
  3. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-ast/Cargo.toml +1 -1
  4. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-core/Cargo.toml +1 -1
  5. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-error/Cargo.toml +1 -1
  6. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/Cargo.toml +1 -1
  7. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/Cargo.toml +1 -1
  8. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-python/Cargo.toml +1 -1
  9. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-python/src/lib.rs +74 -8
  10. {snail_lang-0.5.0 → snail_lang-0.5.2}/pyproject.toml +1 -1
  11. snail_lang-0.5.2/python/snail/__init__.py +25 -0
  12. {snail_lang-0.5.0 → snail_lang-0.5.2}/python/snail/cli.py +103 -33
  13. snail_lang-0.5.2/python/snail/runtime/__init__.py +167 -0
  14. {snail_lang-0.5.0 → snail_lang-0.5.2}/python/snail/runtime/structured_accessor.py +3 -4
  15. snail_lang-0.5.0/python/snail/__init__.py +0 -10
  16. snail_lang-0.5.0/python/snail/runtime/__init__.py +0 -75
  17. {snail_lang-0.5.0 → snail_lang-0.5.2}/Cargo.toml +0 -0
  18. {snail_lang-0.5.0 → snail_lang-0.5.2}/LICENSE +0 -0
  19. {snail_lang-0.5.0 → snail_lang-0.5.2}/README.md +0 -0
  20. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-ast/README.md +0 -0
  21. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-ast/src/ast.rs +0 -0
  22. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-ast/src/awk.rs +0 -0
  23. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-ast/src/lib.rs +0 -0
  24. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-core/README.md +0 -0
  25. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-core/src/lib.rs +0 -0
  26. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-error/README.md +0 -0
  27. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-error/src/lib.rs +0 -0
  28. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/README.md +0 -0
  29. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/awk.rs +0 -0
  30. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/constants.rs +0 -0
  31. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/expr.rs +0 -0
  32. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/helpers.rs +0 -0
  33. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/lib.rs +0 -0
  34. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/operators.rs +0 -0
  35. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/program.rs +0 -0
  36. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/py_ast.rs +0 -0
  37. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-lower/src/stmt.rs +0 -0
  38. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/README.md +0 -0
  39. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/awk.rs +0 -0
  40. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/expr.rs +0 -0
  41. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/lib.rs +0 -0
  42. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/literal.rs +0 -0
  43. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/snail.pest +0 -0
  44. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/stmt.rs +0 -0
  45. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/string.rs +0 -0
  46. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/src/util.rs +0 -0
  47. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/tests/common.rs +0 -0
  48. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/tests/errors.rs +0 -0
  49. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/tests/parser.rs +0 -0
  50. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/tests/statements.rs +0 -0
  51. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/tests/syntax_expressions.rs +0 -0
  52. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  53. {snail_lang-0.5.0 → snail_lang-0.5.2}/crates/snail-python/build.rs +0 -0
  54. {snail_lang-0.5.0 → snail_lang-0.5.2}/python/snail/runtime/compact_try.py +0 -0
  55. {snail_lang-0.5.0 → snail_lang-0.5.2}/python/snail/runtime/regex.py +0 -0
  56. {snail_lang-0.5.0 → snail_lang-0.5.2}/python/snail/runtime/subprocess.py +0 -0
@@ -485,11 +485,11 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
485
485
 
486
486
  [[package]]
487
487
  name = "snail-ast"
488
- version = "0.5.0"
488
+ version = "0.5.2"
489
489
 
490
490
  [[package]]
491
491
  name = "snail-core"
492
- version = "0.5.0"
492
+ version = "0.5.2"
493
493
  dependencies = [
494
494
  "pyo3",
495
495
  "snail-ast",
@@ -500,14 +500,14 @@ dependencies = [
500
500
 
501
501
  [[package]]
502
502
  name = "snail-error"
503
- version = "0.5.0"
503
+ version = "0.5.2"
504
504
  dependencies = [
505
505
  "snail-ast",
506
506
  ]
507
507
 
508
508
  [[package]]
509
509
  name = "snail-lower"
510
- version = "0.5.0"
510
+ version = "0.5.2"
511
511
  dependencies = [
512
512
  "pyo3",
513
513
  "snail-ast",
@@ -516,7 +516,7 @@ dependencies = [
516
516
 
517
517
  [[package]]
518
518
  name = "snail-parser"
519
- version = "0.5.0"
519
+ version = "0.5.2"
520
520
  dependencies = [
521
521
  "pest",
522
522
  "pest_derive",
@@ -526,7 +526,7 @@ dependencies = [
526
526
 
527
527
  [[package]]
528
528
  name = "snail-proptest"
529
- version = "0.5.0"
529
+ version = "0.5.2"
530
530
  dependencies = [
531
531
  "proptest",
532
532
  "pyo3",
@@ -540,7 +540,7 @@ dependencies = [
540
540
 
541
541
  [[package]]
542
542
  name = "snail-python"
543
- version = "0.5.0"
543
+ version = "0.5.2"
544
544
  dependencies = [
545
545
  "pyo3",
546
546
  "snail-core",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snail-lang
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Requires-Dist: jmespath>=1.0.1
5
5
  Requires-Dist: maturin>=1.5 ; extra == 'dev'
6
6
  Requires-Dist: pytest ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-core"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  edition.workspace = true
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-error"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-lower"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-parser"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-python"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  edition.workspace = true
5
5
  build = "build.rs"
6
6
 
@@ -8,9 +8,25 @@ use snail_core::{
8
8
  CompileMode, compile_snail_source_with_auto_print, format_snail_error, parse_awk_program,
9
9
  parse_program,
10
10
  };
11
+ use std::sync::OnceLock;
12
+ use std::time::Instant;
11
13
 
12
14
  const SNAIL_TRACE_PREFIX: &str = "snail:";
13
15
 
16
+ fn profile_enabled() -> bool {
17
+ static ENABLED: OnceLock<bool> = OnceLock::new();
18
+ *ENABLED.get_or_init(|| std::env::var_os("SNAIL_PROFILE_NATIVE").is_some())
19
+ }
20
+
21
+ fn log_profile(label: &str, elapsed: std::time::Duration) {
22
+ if profile_enabled() {
23
+ eprintln!(
24
+ "[snail][native] {label}: {:.3} ms",
25
+ elapsed.as_secs_f64() * 1000.0
26
+ );
27
+ }
28
+ }
29
+
14
30
  fn parse_mode(mode: &str) -> PyResult<CompileMode> {
15
31
  match mode {
16
32
  "snail" => Ok(CompileMode::Snail),
@@ -89,12 +105,23 @@ fn compile_source(
89
105
  auto_print: bool,
90
106
  filename: &str,
91
107
  ) -> Result<PyObject, PyErr> {
108
+ let profile = profile_enabled();
109
+ let total_start = Instant::now();
110
+ let compile_start = Instant::now();
92
111
  let module = compile_snail_source_with_auto_print(py, source, mode, auto_print)
93
112
  .map_err(|err| PySyntaxError::new_err(format_snail_error(&err, filename)))?;
113
+ if profile {
114
+ log_profile("compile_snail_source", compile_start.elapsed());
115
+ }
116
+ let ast_start = Instant::now();
94
117
  let ast = py.import_bound("ast")?;
95
118
  let fixed = ast
96
119
  .getattr("fix_missing_locations")?
97
120
  .call1((module.clone_ref(py),))?;
121
+ if profile {
122
+ log_profile("fix_missing_locations", ast_start.elapsed());
123
+ log_profile("compile_source_total", total_start.elapsed());
124
+ }
98
125
  Ok(fixed.into_py(py))
99
126
  }
100
127
 
@@ -135,14 +162,25 @@ fn compile_py(
135
162
  auto_print: bool,
136
163
  filename: &str,
137
164
  ) -> PyResult<PyObject> {
165
+ let profile = profile_enabled();
166
+ let total_start = Instant::now();
138
167
  let mode = parse_mode(mode)?;
139
168
  let python_ast = compile_source(py, source, mode, auto_print, filename)?;
140
169
  let display = display_filename(filename);
170
+ let linecache_start = Instant::now();
141
171
  register_linecache(py, &display, source)?;
172
+ if profile {
173
+ log_profile("register_linecache", linecache_start.elapsed());
174
+ }
175
+ let compile_start = Instant::now();
142
176
  let builtins = py.import_bound("builtins")?;
143
177
  let code = builtins
144
178
  .getattr("compile")?
145
179
  .call1((python_ast, display, "exec"))?;
180
+ if profile {
181
+ log_profile("py_compile", compile_start.elapsed());
182
+ log_profile("compile_py_total", total_start.elapsed());
183
+ }
146
184
  Ok(code.unbind())
147
185
  }
148
186
 
@@ -171,33 +209,56 @@ fn exec_py(
171
209
  auto_import: bool,
172
210
  filename: &str,
173
211
  ) -> PyResult<i32> {
212
+ let profile = profile_enabled();
213
+ let total_start = Instant::now();
174
214
  let mode = parse_mode(mode)?;
175
215
  let python_ast = compile_source(py, source, mode, auto_print, filename)?;
176
216
  let display = display_filename(filename);
217
+ let linecache_start = Instant::now();
177
218
  register_linecache(py, &display, source)?;
219
+ if profile {
220
+ log_profile("register_linecache", linecache_start.elapsed());
221
+ }
222
+ let compile_start = Instant::now();
178
223
  let builtins = py.import_bound("builtins")?;
179
224
  let code = builtins
180
225
  .getattr("compile")?
181
226
  .call1((python_ast, display, "exec"))?;
227
+ if profile {
228
+ log_profile("py_compile", compile_start.elapsed());
229
+ }
230
+ let globals_start = Instant::now();
182
231
  let globals = prepare_globals(py, strip_display_prefix(filename), &argv, auto_import)?;
232
+ if profile {
233
+ log_profile("prepare_globals", globals_start.elapsed());
234
+ }
183
235
 
184
- match builtins.getattr("exec")?.call1((code.as_any(), &globals)) {
236
+ let exec_start = Instant::now();
237
+ let exec_result = builtins.getattr("exec")?.call1((code.as_any(), &globals));
238
+ if profile {
239
+ log_profile("py_exec", exec_start.elapsed());
240
+ }
241
+ let result = match exec_result {
185
242
  Ok(_) => Ok(0),
186
243
  Err(err) => {
187
244
  if err.is_instance_of::<PySystemExit>(py) {
188
245
  let code = err.value_bound(py).getattr("code")?;
189
246
  if code.is_none() {
190
- return Ok(0);
191
- }
192
- if let Ok(exit_code) = code.extract::<i32>() {
193
- return Ok(exit_code);
247
+ Ok(0)
248
+ } else if let Ok(exit_code) = code.extract::<i32>() {
249
+ Ok(exit_code)
250
+ } else {
251
+ Ok(1)
194
252
  }
195
- Ok(1)
196
253
  } else {
197
254
  Err(err)
198
255
  }
199
256
  }
257
+ };
258
+ if profile {
259
+ log_profile("exec_py_total", total_start.elapsed());
200
260
  }
261
+ result
201
262
  }
202
263
 
203
264
  #[pyfunction(name = "parse")]
@@ -214,15 +275,20 @@ fn parse_py(source: &str, mode: &str, filename: &str) -> PyResult<()> {
214
275
  }
215
276
 
216
277
  #[pymodule]
217
- fn _native(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
278
+ fn _native(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
279
+ let profile = profile_enabled();
280
+ let total_start = Instant::now();
218
281
  module.add_function(wrap_pyfunction!(compile_py, module)?)?;
219
282
  module.add_function(wrap_pyfunction!(compile_ast_py, module)?)?;
220
283
  module.add_function(wrap_pyfunction!(exec_py, module)?)?;
221
284
  module.add_function(wrap_pyfunction!(parse_py, module)?)?;
222
- module.add("__build_info__", build_info_dict(_py)?)?;
285
+ module.add("__build_info__", build_info_dict(py)?)?;
223
286
  module.add(
224
287
  "__all__",
225
288
  vec!["compile", "compile_ast", "exec", "parse", "__build_info__"],
226
289
  )?;
290
+ if profile {
291
+ log_profile("module_init", total_start.elapsed());
292
+ }
227
293
  Ok(())
228
294
  }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "snail-lang"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Snail programming language interpreter"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,25 @@
1
+ from ._native import __build_info__, compile, compile_ast, exec, parse
2
+
3
+
4
+ def _resolve_version() -> str:
5
+ try:
6
+ from importlib.metadata import version
7
+
8
+ return version("snail-lang")
9
+ except Exception: # pragma: no cover - during development
10
+ return "0.0.0"
11
+
12
+
13
+ def __getattr__(name: str):
14
+ if name == "__version__":
15
+ value = _resolve_version()
16
+ globals()["__version__"] = value
17
+ return value
18
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
19
+
20
+
21
+ def __dir__() -> list[str]:
22
+ return sorted(list(globals().keys()) + ["__version__"])
23
+
24
+
25
+ __all__ = ["compile", "compile_ast", "exec", "parse", "__version__", "__build_info__"]
@@ -1,23 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- import argparse
4
- import ast
5
- import builtins
6
3
  import os
7
4
  import sys
8
- import traceback
9
- from pathlib import Path
10
5
 
11
- from . import __build_info__, __version__, compile_ast, exec
6
+ from . import __build_info__, compile_ast, exec
12
7
 
13
-
14
- def _build_parser() -> argparse.ArgumentParser:
15
- return argparse.ArgumentParser(
16
- prog="snail",
17
- description="Snail programming language interpreter",
18
- usage="snail [options] -f <file> [args]...\n snail [options] <code> [args]...",
19
- add_help=True,
20
- )
8
+ _USAGE = (
9
+ "snail [options] -f <file> [args]...\n"
10
+ " snail [options] <code> [args]..."
11
+ )
12
+ _DESCRIPTION = "Snail programming language interpreter"
21
13
 
22
14
 
23
15
  def _display_filename(filename: str) -> str:
@@ -26,10 +18,7 @@ def _display_filename(filename: str) -> str:
26
18
  return f"snail:{filename}"
27
19
 
28
20
 
29
- def _trim_internal_prefix(
30
- stack: traceback.StackSummary,
31
- internal_files: set[str],
32
- ) -> None:
21
+ def _trim_internal_prefix(stack, internal_files: set[str]) -> None:
33
22
  if not stack:
34
23
  return
35
24
  trim_count = 0
@@ -48,10 +37,7 @@ def _trim_internal_prefix(
48
37
  del stack[:trim_count]
49
38
 
50
39
 
51
- def _trim_traceback_exception(
52
- tb_exc: traceback.TracebackException,
53
- internal_files: set[str],
54
- ) -> None:
40
+ def _trim_traceback_exception(tb_exc, internal_files: set[str]) -> None:
55
41
  _trim_internal_prefix(tb_exc.stack, internal_files)
56
42
  cause = getattr(tb_exc, "__cause__", None)
57
43
  if cause is not None:
@@ -76,6 +62,8 @@ def _install_trimmed_excepthook() -> None:
76
62
  ) -> None:
77
63
  if exc_type is KeyboardInterrupt:
78
64
  return original_excepthook(exc_type, exc, tb)
65
+ import traceback
66
+
79
67
  tb_exc = traceback.TracebackException(
80
68
  exc_type,
81
69
  exc,
@@ -95,6 +83,77 @@ def _install_trimmed_excepthook() -> None:
95
83
  sys.excepthook = _snail_excepthook
96
84
 
97
85
 
86
+ class _Args:
87
+ def __init__(self) -> None:
88
+ self.file: str | None = None
89
+ self.awk = False
90
+ self.no_print = False
91
+ self.no_auto_import = False
92
+ self.debug = False
93
+ self.version = False
94
+ self.help = False
95
+ self.args: list[str] = []
96
+
97
+
98
+ def _print_help(file=sys.stdout) -> None:
99
+ print(f"usage: {_USAGE}", file=file)
100
+ print("", file=file)
101
+ print(_DESCRIPTION, file=file)
102
+ print("", file=file)
103
+ print("options:", file=file)
104
+ print(" -f <file> read Snail source from file", file=file)
105
+ print(" -a, --awk awk mode", file=file)
106
+ print(" -P, --no-print disable auto-print of last expression", file=file)
107
+ print(" -I, --no-auto-import disable auto-imports", file=file)
108
+ print(" --debug parse and compile, then print, do not run", file=file)
109
+ print(" -v, --version show version and exit", file=file)
110
+ print(" -h, --help show this help message and exit", file=file)
111
+
112
+
113
+ def _parse_args(argv: list[str]) -> _Args:
114
+ args = _Args()
115
+ idx = 0
116
+ while idx < len(argv):
117
+ token = argv[idx]
118
+ if token == "--":
119
+ args.args = argv[idx + 1 :]
120
+ return args
121
+ if token == "-" or not token.startswith("-"):
122
+ args.args = argv[idx:]
123
+ return args
124
+ if token in ("-h", "--help"):
125
+ args.help = True
126
+ return args
127
+ if token in ("-v", "--version"):
128
+ args.version = True
129
+ idx += 1
130
+ continue
131
+ if token in ("-a", "--awk"):
132
+ args.awk = True
133
+ idx += 1
134
+ continue
135
+ if token in ("-P", "--no-print"):
136
+ args.no_print = True
137
+ idx += 1
138
+ continue
139
+ if token in ("-I", "--no-auto-import"):
140
+ args.no_auto_import = True
141
+ idx += 1
142
+ continue
143
+ if token == "--debug":
144
+ args.debug = True
145
+ idx += 1
146
+ continue
147
+ if token == "-f":
148
+ if idx + 1 >= len(argv):
149
+ raise ValueError("option -f requires an argument")
150
+ args.file = argv[idx + 1]
151
+ idx += 2
152
+ continue
153
+ raise ValueError(f"unknown option: {token}")
154
+ return args
155
+
156
+
98
157
  def _format_version(version: str, build_info: dict[str, object] | None) -> str:
99
158
  display_version = version if version.startswith("v") else f"v{version}"
100
159
  if not build_info:
@@ -114,28 +173,36 @@ def _format_version(version: str, build_info: dict[str, object] | None) -> str:
114
173
  return f"{display_version} ({git_rev})"
115
174
 
116
175
 
176
+ def _get_version() -> str:
177
+ from . import __version__ as version
178
+
179
+ return version
180
+
181
+
117
182
  def main(argv: list[str] | None = None) -> int:
118
183
  if argv is None:
119
184
  _install_trimmed_excepthook()
185
+ argv = sys.argv[1:]
120
186
 
121
- parser = _build_parser()
122
- parser.add_argument("-f", dest="file", metavar="file")
123
- parser.add_argument("-a", "--awk", action="store_true")
124
- parser.add_argument("-P", "--no-print", action="store_true")
125
- parser.add_argument("-I", "--no-auto-import", action="store_true")
126
- parser.add_argument("--debug", action="store_true", help="Parse and compile, then print, do not run")
127
- parser.add_argument("-v", "--version", action="store_true")
128
- parser.add_argument("args", nargs=argparse.REMAINDER)
129
-
130
- namespace = parser.parse_args(argv)
187
+ try:
188
+ namespace = _parse_args(argv)
189
+ except ValueError as exc:
190
+ _print_help(file=sys.stderr)
191
+ print(f"error: {exc}", file=sys.stderr)
192
+ return 2
131
193
 
194
+ if namespace.help:
195
+ _print_help()
196
+ return 0
132
197
  if namespace.version:
133
- print(_format_version(__version__, __build_info__))
198
+ print(_format_version(_get_version(), __build_info__))
134
199
  return 0
135
200
 
136
201
  mode = "awk" if namespace.awk else "snail"
137
202
 
138
203
  if namespace.file:
204
+ from pathlib import Path
205
+
139
206
  path = Path(namespace.file)
140
207
  try:
141
208
  source = path.read_text()
@@ -153,6 +220,9 @@ def main(argv: list[str] | None = None) -> int:
153
220
  args = ["--", *namespace.args[1:]]
154
221
 
155
222
  if namespace.debug:
223
+ import ast
224
+ import builtins
225
+
156
226
  python_ast = compile_ast(
157
227
  source,
158
228
  mode=mode,
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+
5
+ __all__ = ["install_helpers", "AutoImportDict", "AUTO_IMPORT_NAMES"]
6
+
7
+ # Names that can be auto-imported when first referenced.
8
+ # Maps name -> (module, attribute) where attribute is None for whole-module imports.
9
+ AUTO_IMPORT_NAMES: dict[str, tuple[str, str | None]] = {
10
+ # Whole module imports: import X
11
+ "sys": ("sys", None),
12
+ "os": ("os", None),
13
+ # Attribute imports: from X import Y
14
+ "Path": ("pathlib", "Path"),
15
+ }
16
+
17
+
18
+ class AutoImportDict(dict):
19
+ """A dict subclass that lazily imports allowed names on first access.
20
+
21
+ When a key lookup fails, if the key is in AUTO_IMPORT_NAMES,
22
+ the corresponding module/attribute is imported and stored in the dict.
23
+ Supports both whole-module imports (import sys) and attribute imports
24
+ (from pathlib import Path).
25
+ """
26
+
27
+ def __missing__(self, key: str) -> object:
28
+ if key in AUTO_IMPORT_NAMES:
29
+ module_name, attr_name = AUTO_IMPORT_NAMES[key]
30
+ module = importlib.import_module(module_name)
31
+ value = getattr(module, attr_name) if attr_name else module
32
+ self[key] = value
33
+ return value
34
+ raise KeyError(key)
35
+
36
+
37
+ _compact_try = None
38
+ _regex_search = None
39
+ _regex_compile = None
40
+ _subprocess_capture = None
41
+ _subprocess_status = None
42
+ _jmespath_query = None
43
+ _js = None
44
+
45
+
46
+ def _get_compact_try():
47
+ global _compact_try
48
+ if _compact_try is None:
49
+ from .compact_try import compact_try
50
+
51
+ _compact_try = compact_try
52
+ return _compact_try
53
+
54
+
55
+ def _get_regex_search():
56
+ global _regex_search
57
+ if _regex_search is None:
58
+ from .regex import regex_search
59
+
60
+ _regex_search = regex_search
61
+ return _regex_search
62
+
63
+
64
+ def _get_regex_compile():
65
+ global _regex_compile
66
+ if _regex_compile is None:
67
+ from .regex import regex_compile
68
+
69
+ _regex_compile = regex_compile
70
+ return _regex_compile
71
+
72
+
73
+ def _get_subprocess_capture():
74
+ global _subprocess_capture
75
+ if _subprocess_capture is None:
76
+ from .subprocess import SubprocessCapture
77
+
78
+ _subprocess_capture = SubprocessCapture
79
+ return _subprocess_capture
80
+
81
+
82
+ def _get_subprocess_status():
83
+ global _subprocess_status
84
+ if _subprocess_status is None:
85
+ from .subprocess import SubprocessStatus
86
+
87
+ _subprocess_status = SubprocessStatus
88
+ return _subprocess_status
89
+
90
+
91
+ def _get_jmespath_query():
92
+ global _jmespath_query
93
+ if _jmespath_query is None:
94
+ from .structured_accessor import __snail_jmespath_query
95
+
96
+ _jmespath_query = __snail_jmespath_query
97
+ return _jmespath_query
98
+
99
+
100
+ def _get_js():
101
+ global _js
102
+ if _js is None:
103
+ from .structured_accessor import js
104
+
105
+ _js = js
106
+ return _js
107
+
108
+
109
+ def _lazy_compact_try(expr_fn, fallback_fn=None):
110
+ return _get_compact_try()(expr_fn, fallback_fn)
111
+
112
+
113
+ def _lazy_regex_search(value, pattern):
114
+ return _get_regex_search()(value, pattern)
115
+
116
+
117
+ def _lazy_regex_compile(pattern):
118
+ return _get_regex_compile()(pattern)
119
+
120
+
121
+ def _lazy_subprocess_capture(cmd: str):
122
+ return _get_subprocess_capture()(cmd)
123
+
124
+
125
+ def _lazy_subprocess_status(cmd: str):
126
+ return _get_subprocess_status()(cmd)
127
+
128
+
129
+ def _lazy_jmespath_query(query: str):
130
+ return _get_jmespath_query()(query)
131
+
132
+
133
+ def _lazy_js(input_data=None):
134
+ return _get_js()(input_data)
135
+
136
+
137
+ def __snail_partial(func, /, *args, **kwargs):
138
+ import functools
139
+
140
+ return functools.partial(func, *args, **kwargs)
141
+
142
+
143
+ def __snail_contains__(left, right):
144
+ method = getattr(right, "__snail_contains__", None)
145
+ if method is not None:
146
+ return method(left)
147
+ return left in right
148
+
149
+
150
+ def __snail_contains_not__(left, right):
151
+ method = getattr(right, "__snail_contains__", None)
152
+ if method is not None:
153
+ return not bool(method(left))
154
+ return left not in right
155
+
156
+
157
+ def install_helpers(globals_dict: dict) -> None:
158
+ globals_dict["__snail_compact_try"] = _lazy_compact_try
159
+ globals_dict["__snail_regex_search"] = _lazy_regex_search
160
+ globals_dict["__snail_regex_compile"] = _lazy_regex_compile
161
+ globals_dict["__SnailSubprocessCapture"] = _lazy_subprocess_capture
162
+ globals_dict["__SnailSubprocessStatus"] = _lazy_subprocess_status
163
+ globals_dict["__snail_jmespath_query"] = _lazy_jmespath_query
164
+ globals_dict["__snail_partial"] = __snail_partial
165
+ globals_dict["__snail_contains__"] = __snail_contains__
166
+ globals_dict["__snail_contains_not__"] = __snail_contains_not__
167
+ globals_dict["js"] = _lazy_js
@@ -4,17 +4,16 @@ import json as _json
4
4
  import os as _os
5
5
  import sys as _sys
6
6
 
7
- import jmespath
8
-
9
-
10
7
  def __snail_jmespath_query(query: str):
11
8
  """Create a callable that applies JMESPath query.
12
9
 
13
10
  Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
14
11
  """
15
12
 
13
+ import jmespath as _jmespath
14
+
16
15
  def apply(data):
17
- return jmespath.search(query, data)
16
+ return _jmespath.search(query, data)
18
17
 
19
18
  return apply
20
19
 
@@ -1,10 +0,0 @@
1
- from ._native import __build_info__, compile, compile_ast, exec, parse
2
-
3
- try:
4
- from importlib.metadata import version
5
-
6
- __version__ = version("snail-lang")
7
- except Exception: # pragma: no cover - during development
8
- __version__ = "0.0.0"
9
-
10
- __all__ = ["compile", "compile_ast", "exec", "parse", "__version__", "__build_info__"]
@@ -1,75 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import functools
4
- import importlib
5
- from typing import Any
6
-
7
- from .compact_try import compact_try
8
- from .regex import regex_compile, regex_search
9
- from .structured_accessor import (
10
- __snail_jmespath_query,
11
- js,
12
- )
13
- from .subprocess import SubprocessCapture, SubprocessStatus
14
-
15
- __all__ = ["install_helpers", "AutoImportDict", "AUTO_IMPORT_NAMES"]
16
-
17
- # Names that can be auto-imported when first referenced.
18
- # Maps name -> (module, attribute) where attribute is None for whole-module imports.
19
- AUTO_IMPORT_NAMES: dict[str, tuple[str, str | None]] = {
20
- # Whole module imports: import X
21
- "sys": ("sys", None),
22
- "os": ("os", None),
23
- # Attribute imports: from X import Y
24
- "Path": ("pathlib", "Path"),
25
- }
26
-
27
-
28
- class AutoImportDict(dict):
29
- """A dict subclass that lazily imports allowed names on first access.
30
-
31
- When a key lookup fails, if the key is in AUTO_IMPORT_NAMES,
32
- the corresponding module/attribute is imported and stored in the dict.
33
- Supports both whole-module imports (import sys) and attribute imports
34
- (from pathlib import Path).
35
- """
36
-
37
- def __missing__(self, key: str) -> Any:
38
- if key in AUTO_IMPORT_NAMES:
39
- module_name, attr_name = AUTO_IMPORT_NAMES[key]
40
- module = importlib.import_module(module_name)
41
- value = getattr(module, attr_name) if attr_name else module
42
- self[key] = value
43
- return value
44
- raise KeyError(key)
45
-
46
-
47
- def __snail_partial(func, /, *args, **kwargs):
48
- return functools.partial(func, *args, **kwargs)
49
-
50
-
51
- def __snail_contains__(left, right):
52
- method = getattr(right, "__snail_contains__", None)
53
- if method is not None:
54
- return method(left)
55
- return left in right
56
-
57
-
58
- def __snail_contains_not__(left, right):
59
- method = getattr(right, "__snail_contains__", None)
60
- if method is not None:
61
- return not bool(method(left))
62
- return left not in right
63
-
64
-
65
- def install_helpers(globals_dict: dict) -> None:
66
- globals_dict["__snail_compact_try"] = compact_try
67
- globals_dict["__snail_regex_search"] = regex_search
68
- globals_dict["__snail_regex_compile"] = regex_compile
69
- globals_dict["__SnailSubprocessCapture"] = SubprocessCapture
70
- globals_dict["__SnailSubprocessStatus"] = SubprocessStatus
71
- globals_dict["__snail_jmespath_query"] = __snail_jmespath_query
72
- globals_dict["__snail_partial"] = __snail_partial
73
- globals_dict["__snail_contains__"] = __snail_contains__
74
- globals_dict["__snail_contains_not__"] = __snail_contains_not__
75
- globals_dict["js"] = js
File without changes
File without changes
File without changes