shellsim 0.1.18__tar.gz → 0.1.19__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.
- {shellsim-0.1.18 → shellsim-0.1.19}/Cargo.lock +1 -1
- {shellsim-0.1.18 → shellsim-0.1.19}/Cargo.toml +1 -1
- {shellsim-0.1.18 → shellsim-0.1.19}/PKG-INFO +1 -1
- {shellsim-0.1.18 → shellsim-0.1.19}/python/native/Cargo.lock +2 -2
- {shellsim-0.1.18 → shellsim-0.1.19}/python/native/Cargo.toml +1 -1
- {shellsim-0.1.18 → shellsim-0.1.19}/src/interp.rs +12 -1
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/parser.rs +20 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/protocol.rs +2 -2
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/core.rs +33 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/math.rs +43 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/random.py +17 -0
- shellsim-0.1.19/src/python/vm/format.rs +352 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/operations.rs +30 -39
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm.rs +2 -241
- {shellsim-0.1.18 → shellsim-0.1.19}/LICENSE +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/README.md +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/pyproject.toml +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/native/src/lib.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/__init__.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/__main__.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/_api.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/_cli.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/py.typed +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/python.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/rust-toolchain.toml +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/arith.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/bin/shellsim-python.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/clock.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/archives.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/arcmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/ast.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/eval.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/lexer.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/parser.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/bc.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/builtins.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/dd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/diff.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/echo.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/expr.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/find.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/format.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/fs.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/apply.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/commit.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/compare.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/config.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/conflict.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/diff.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/ignore.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/import.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/log.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/merge.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/mod.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/plumbing.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/rebase.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/refs.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/repo.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/stash.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/switch.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/worktree.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/grep.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/hashing.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/makecmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/mod.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/net.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/options.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/patch.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/pkg.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/printf.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/proc.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/regex_compat.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/rgcmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/sed.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/sort.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/streams.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/system.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/tarcmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/text.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/unavailable.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/util.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/wasm.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/xargs.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/zipcmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/corpus.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/descriptors.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/display.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/exec.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/expand.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/harness.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/harness_manager.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/hashes.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/host_ingest.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/jqcmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/lib.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/main.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/mcp.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/net.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/netcmd.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/process.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/profile.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/cat.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/env.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/find.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/head.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/make.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/nice.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/nohup.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/sleep.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/tee.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/timeout.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program/xargs.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/program.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/pseudo_fs.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/ast.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/bytecode.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/compiler.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/filesystem.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/heap.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/http.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/lexer.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/mapping.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/mod.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/native.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/number.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/object_model.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/process.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/slice.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/source.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/argparse.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/asyncio.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/base64.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/bisect.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/collections.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/dataclasses.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/enum.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/frozen.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/functools.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/hashlib.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/heapq.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/http.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/importlib.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/itertools.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/json.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/mod.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/numpy.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/os.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/pytest.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/re.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/abc.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/asyncio.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/base64.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/cmath.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/codecs.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/collections.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/complex.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/copy.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/csv.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/datetime.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/functools.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/glob.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/hashlib.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/http.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/http_client.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/importlib.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/io.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/json.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/logging.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/numpy_random.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/os.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/pathlib.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/pytest.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/shutil.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/signal.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/statistics.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/struct.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/subprocess.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/tempfile.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/textwrap.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/urllib.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/urllib_error.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/urllib_request.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/uuid.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/zipfile.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/zlib.py +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/string.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/struct.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/subprocess.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/sys.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/time.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/typing.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/unittest.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/vfs.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/zlib.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/string.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/token.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/calls.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/dispatch.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/host.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/iteration.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/namespace.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/native_runtime.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/objects.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/realtime.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/resources.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/sandbox.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/scenario.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/scheduler.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/shell.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/syscalls.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/telemetry.rs +0 -0
- {shellsim-0.1.18 → shellsim-0.1.19}/src/vfs.rs +0 -0
|
@@ -925,7 +925,7 @@ dependencies = [
|
|
|
925
925
|
|
|
926
926
|
[[package]]
|
|
927
927
|
name = "shellsim"
|
|
928
|
-
version = "0.1.
|
|
928
|
+
version = "0.1.19"
|
|
929
929
|
dependencies = [
|
|
930
930
|
"base64",
|
|
931
931
|
"crc32fast",
|
|
@@ -947,7 +947,7 @@ dependencies = [
|
|
|
947
947
|
|
|
948
948
|
[[package]]
|
|
949
949
|
name = "shellsim-python-bindings"
|
|
950
|
-
version = "0.1.
|
|
950
|
+
version = "0.1.19"
|
|
951
951
|
dependencies = [
|
|
952
952
|
"pyo3",
|
|
953
953
|
"serde",
|
|
@@ -162,7 +162,7 @@ pub struct ProcessState {
|
|
|
162
162
|
pub session_id: ProcessId,
|
|
163
163
|
/// Process group inherited across ordinary forks or established by a group leader.
|
|
164
164
|
pub process_group: ProcessId,
|
|
165
|
-
/// PID expanded by `$$`; preserved across Bash subshells.
|
|
165
|
+
/// PID expanded by `$$`; preserved across Bash subshells and reset when a shell image loads.
|
|
166
166
|
pub shell_pid: ProcessId,
|
|
167
167
|
/// shell + environment variables (we don't distinguish exported vs not for simplicity,
|
|
168
168
|
/// except that `env`/child python only sees exported ones, tracked in `exported`)
|
|
@@ -1102,6 +1102,17 @@ impl Environment {
|
|
|
1102
1102
|
state.command_hash.clear();
|
|
1103
1103
|
state.directory_stack.clear();
|
|
1104
1104
|
state.local_scopes.clear();
|
|
1105
|
+
// The new image is a fresh shell: `$$` names it, and loop, job, and getopts state from
|
|
1106
|
+
// the forked parent shell do not carry over.
|
|
1107
|
+
state.shell_pid = pid;
|
|
1108
|
+
state.jobs.clear();
|
|
1109
|
+
state.next_job_id = 1;
|
|
1110
|
+
state.loop_depth = 0;
|
|
1111
|
+
state.cond_depth = 0;
|
|
1112
|
+
state.getopts = GetoptsState {
|
|
1113
|
+
optind: 1,
|
|
1114
|
+
offset: 1,
|
|
1115
|
+
};
|
|
1105
1116
|
state.arg0 = image.arg0;
|
|
1106
1117
|
state.positional = image.positional;
|
|
1107
1118
|
state.opt_errexit = image.errexit;
|
|
@@ -2101,6 +2101,26 @@ mod tests {
|
|
|
2101
2101
|
parse(lex("sys.stdout.write(sys.argv[1])").unwrap()).unwrap();
|
|
2102
2102
|
}
|
|
2103
2103
|
|
|
2104
|
+
#[test]
|
|
2105
|
+
fn parses_comma_separated_imports_and_rejects_a_missing_module() {
|
|
2106
|
+
let program = parse(lex("import math, urllib.error as errors, random").unwrap()).unwrap();
|
|
2107
|
+
let StatementKind::Import { modules } = &program.statements[0].kind else {
|
|
2108
|
+
panic!("expected import statement")
|
|
2109
|
+
};
|
|
2110
|
+
assert_eq!(
|
|
2111
|
+
modules,
|
|
2112
|
+
&[
|
|
2113
|
+
("math".into(), "math".into()),
|
|
2114
|
+
("urllib.error".into(), "errors".into()),
|
|
2115
|
+
("random".into(), "random".into())
|
|
2116
|
+
]
|
|
2117
|
+
);
|
|
2118
|
+
for source in ["import math,", "import , math"] {
|
|
2119
|
+
let error = parse(lex(source).unwrap()).expect_err("missing module must fail");
|
|
2120
|
+
assert!(error.message.contains("expected a module name"));
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2104
2124
|
#[test]
|
|
2105
2125
|
fn parses_tuple_subscripts_and_rejects_empty_tuple_members() {
|
|
2106
2126
|
parse(lex("pair = values[1, 2]\nsingle = values[1,]").unwrap()).unwrap();
|
|
@@ -17,8 +17,8 @@ pub fn display(heap: &Heap, value: &Value) -> Result<String, String> {
|
|
|
17
17
|
if let Some(value) = string_value(heap, value)? {
|
|
18
18
|
return Ok(value);
|
|
19
19
|
}
|
|
20
|
-
if let Some((
|
|
21
|
-
return Ok(
|
|
20
|
+
if let Some((_, message)) = exception_parts(heap, value)? {
|
|
21
|
+
return Ok(message);
|
|
22
22
|
}
|
|
23
23
|
if let Some((_, args)) = user_exception_parts(heap, value)? {
|
|
24
24
|
return match args.as_slice() {
|
|
@@ -19,6 +19,7 @@ use super::super::number::PyNumber;
|
|
|
19
19
|
use super::super::slice::SlicePlan;
|
|
20
20
|
|
|
21
21
|
static BUILTINS: &[FunctionDef] = &[
|
|
22
|
+
builtin("__import__", builtin_import),
|
|
22
23
|
builtin("map", builtin_map),
|
|
23
24
|
builtin("filter", builtin_filter),
|
|
24
25
|
builtin("reversed", builtin_reversed),
|
|
@@ -2899,3 +2900,35 @@ fn slot_sequence_multiply(
|
|
|
2899
2900
|
_ => unreachable!("only concrete sequence slots call this helper"),
|
|
2900
2901
|
}
|
|
2901
2902
|
}
|
|
2903
|
+
|
|
2904
|
+
/// Import only through the simulated module loader, preserving dotted-import return behavior.
|
|
2905
|
+
fn builtin_import(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
|
|
2906
|
+
args.expect_positional("__import__", 1, 5)?;
|
|
2907
|
+
args.reject_unknown_keywords("__import__", &["globals", "locals", "fromlist", "level"])?;
|
|
2908
|
+
let OwnedPyString(name) = args.positional()[0].cast(runtime)?;
|
|
2909
|
+
let level = args
|
|
2910
|
+
.positional()
|
|
2911
|
+
.get(4)
|
|
2912
|
+
.copied()
|
|
2913
|
+
.or(args.keyword("__import__", "level")?.copied());
|
|
2914
|
+
if let Some(level) = level {
|
|
2915
|
+
if !matches!(
|
|
2916
|
+
integer_argument(runtime, &level, "level must be an integer")?,
|
|
2917
|
+
IntegerArgument::Finite(0)
|
|
2918
|
+
) {
|
|
2919
|
+
return Err(PyError::value_error("relative __import__ is not supported"));
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
let fromlist = args
|
|
2923
|
+
.positional()
|
|
2924
|
+
.get(3)
|
|
2925
|
+
.copied()
|
|
2926
|
+
.or(args.keyword("__import__", "fromlist")?.copied());
|
|
2927
|
+
let module = runtime.import_module(&name)?;
|
|
2928
|
+
if let Some(fromlist) = fromlist {
|
|
2929
|
+
if runtime.truth(&fromlist)? {
|
|
2930
|
+
return Ok(module);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
runtime.import_module(name.split('.').next().unwrap_or(&name))
|
|
2934
|
+
}
|
|
@@ -45,6 +45,11 @@ pub(super) static MODULE: ModuleDef = ModuleDef {
|
|
|
45
45
|
name: "ceil",
|
|
46
46
|
call: native_ceil,
|
|
47
47
|
},
|
|
48
|
+
FunctionDef {
|
|
49
|
+
module: "math",
|
|
50
|
+
name: "comb",
|
|
51
|
+
call: native_comb,
|
|
52
|
+
},
|
|
48
53
|
FunctionDef {
|
|
49
54
|
module: "math",
|
|
50
55
|
name: "cos",
|
|
@@ -195,6 +200,44 @@ fn native_ceil(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
|
|
|
195
200
|
native_call(runtime, args, "ceil")
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
/// Compute exact combinations with work and result storage bounded before multiplication.
|
|
204
|
+
fn native_comb(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
|
|
205
|
+
args.expect_positional("math.comb", 2, 2)?;
|
|
206
|
+
args.reject_keywords("math.comb")?;
|
|
207
|
+
let n = integer_argument(runtime, &args.positional()[0], "comb")?;
|
|
208
|
+
let k = integer_argument(runtime, &args.positional()[1], "comb")?;
|
|
209
|
+
if n.is_negative() {
|
|
210
|
+
return Err(PyError::value_error("n must be a non-negative integer"));
|
|
211
|
+
}
|
|
212
|
+
if k.is_negative() {
|
|
213
|
+
return Err(PyError::value_error("k must be a non-negative integer"));
|
|
214
|
+
}
|
|
215
|
+
if k > n {
|
|
216
|
+
return runtime.new_integer("0");
|
|
217
|
+
}
|
|
218
|
+
let complement = &n - &k;
|
|
219
|
+
let selected = if k <= complement { k } else { complement };
|
|
220
|
+
let count = selected
|
|
221
|
+
.to_usize()
|
|
222
|
+
.ok_or_else(|| PyError::resource_error("combination length is too large"))?;
|
|
223
|
+
if count > 100_000 {
|
|
224
|
+
return Err(PyError::resource_error("combination length is too large"));
|
|
225
|
+
}
|
|
226
|
+
if count == 0 {
|
|
227
|
+
return runtime.new_integer("1");
|
|
228
|
+
}
|
|
229
|
+
let bytes = bigint_bytes(&n)?
|
|
230
|
+
.checked_mul(count.saturating_add(1))
|
|
231
|
+
.ok_or_else(|| PyError::resource_error("combination result is too large"))?;
|
|
232
|
+
runtime.reserve_memory(bytes)?;
|
|
233
|
+
let mut result = BigInt::from(1_u8);
|
|
234
|
+
for index in 1..=count {
|
|
235
|
+
runtime.charge_cpu(result.bits().div_ceil(64).saturating_add(1))?;
|
|
236
|
+
result = result * (&n - &selected + index) / index;
|
|
237
|
+
}
|
|
238
|
+
runtime.new_integer(&result.to_string())
|
|
239
|
+
}
|
|
240
|
+
|
|
198
241
|
fn native_cos(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
|
|
199
242
|
native_call(runtime, args, "cos")
|
|
200
243
|
}
|
|
@@ -11,6 +11,7 @@ class Random:
|
|
|
11
11
|
if not isinstance(value, int):
|
|
12
12
|
raise TypeError("shellsim random seeds must be integers or None")
|
|
13
13
|
self._state = value % 2147483648
|
|
14
|
+
self._gauss_next = None
|
|
14
15
|
|
|
15
16
|
def _next(self):
|
|
16
17
|
self._state = (self._state * 1103515245 + 12345) % 2147483648
|
|
@@ -40,6 +41,18 @@ class Random:
|
|
|
40
41
|
def uniform(self, left, right):
|
|
41
42
|
return left + (right - left) * self.random()
|
|
42
43
|
|
|
44
|
+
def gauss(self, mu=0.0, sigma=1.0):
|
|
45
|
+
import math
|
|
46
|
+
|
|
47
|
+
value = self._gauss_next
|
|
48
|
+
self._gauss_next = None
|
|
49
|
+
if value is None:
|
|
50
|
+
angle = math.tau * self.random()
|
|
51
|
+
radius = math.sqrt(-2.0 * math.log(1.0 - self.random()))
|
|
52
|
+
value = math.cos(angle) * radius
|
|
53
|
+
self._gauss_next = math.sin(angle) * radius
|
|
54
|
+
return mu + value * sigma
|
|
55
|
+
|
|
43
56
|
def choice(self, population):
|
|
44
57
|
if len(population) == 0:
|
|
45
58
|
raise IndexError("cannot choose from an empty sequence")
|
|
@@ -85,6 +98,10 @@ def uniform(left, right):
|
|
|
85
98
|
return _default.uniform(left, right)
|
|
86
99
|
|
|
87
100
|
|
|
101
|
+
def gauss(mu=0.0, sigma=1.0):
|
|
102
|
+
return _default.gauss(mu, sigma)
|
|
103
|
+
|
|
104
|
+
|
|
88
105
|
def choice(population):
|
|
89
106
|
return _default.choice(population)
|
|
90
107
|
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
//! Python's format-specification mini-language for f-strings, `format`, and `str.format`.
|
|
2
|
+
//!
|
|
3
|
+
//! A specification is parsed once into [`FormatSpec`], then rendered for an integer, float, or
|
|
4
|
+
//! string. The supported grammar is `[align][sign][#][0][width][,][.precision][type]` with the
|
|
5
|
+
//! default space fill. Custom fill characters, `=` alignment, the space sign, `_` grouping, and
|
|
6
|
+
//! the `n`/`c` presentations are rejected explicitly rather than approximated.
|
|
7
|
+
|
|
8
|
+
use super::BigInt;
|
|
9
|
+
use num_bigint::Sign;
|
|
10
|
+
|
|
11
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
12
|
+
pub(super) enum Align {
|
|
13
|
+
Left,
|
|
14
|
+
Right,
|
|
15
|
+
Center,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// A parsed format specification. Widths and precisions are bounded by the caller's resource
|
|
19
|
+
/// reservation before rendering allocates padding.
|
|
20
|
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
21
|
+
pub(super) struct FormatSpec {
|
|
22
|
+
pub(super) align: Option<Align>,
|
|
23
|
+
pub(super) plus: bool,
|
|
24
|
+
pub(super) alternate: bool,
|
|
25
|
+
pub(super) zero: bool,
|
|
26
|
+
pub(super) width: usize,
|
|
27
|
+
pub(super) grouping: bool,
|
|
28
|
+
pub(super) precision: Option<usize>,
|
|
29
|
+
pub(super) presentation: Option<char>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl FormatSpec {
|
|
33
|
+
/// Parse `spec`, for example `">+12,.2f"` or `"#010x"`.
|
|
34
|
+
pub(super) fn parse(spec: &str) -> Result<Self, String> {
|
|
35
|
+
let unsupported = || format!("unsupported format specification {spec:?}");
|
|
36
|
+
let mut parsed = Self::default();
|
|
37
|
+
let mut rest = spec;
|
|
38
|
+
if let Some(align) = rest.chars().next().and_then(|first| match first {
|
|
39
|
+
'<' => Some(Align::Left),
|
|
40
|
+
'>' => Some(Align::Right),
|
|
41
|
+
'^' => Some(Align::Center),
|
|
42
|
+
_ => None,
|
|
43
|
+
}) {
|
|
44
|
+
parsed.align = Some(align);
|
|
45
|
+
rest = &rest[1..];
|
|
46
|
+
}
|
|
47
|
+
if let Some(stripped) = rest.strip_prefix('+') {
|
|
48
|
+
parsed.plus = true;
|
|
49
|
+
rest = stripped;
|
|
50
|
+
} else if let Some(stripped) = rest.strip_prefix('-') {
|
|
51
|
+
rest = stripped;
|
|
52
|
+
}
|
|
53
|
+
if let Some(stripped) = rest.strip_prefix('#') {
|
|
54
|
+
parsed.alternate = true;
|
|
55
|
+
rest = stripped;
|
|
56
|
+
}
|
|
57
|
+
if let Some(stripped) = rest.strip_prefix('0') {
|
|
58
|
+
parsed.zero = true;
|
|
59
|
+
rest = stripped;
|
|
60
|
+
}
|
|
61
|
+
let (width, stripped) = leading_number(rest).map_err(|_| unsupported())?;
|
|
62
|
+
parsed.width = width.unwrap_or(0);
|
|
63
|
+
rest = stripped;
|
|
64
|
+
if let Some(stripped) = rest.strip_prefix(',') {
|
|
65
|
+
parsed.grouping = true;
|
|
66
|
+
rest = stripped;
|
|
67
|
+
}
|
|
68
|
+
if let Some(stripped) = rest.strip_prefix('.') {
|
|
69
|
+
let (precision, stripped) = leading_number(stripped).map_err(|_| unsupported())?;
|
|
70
|
+
parsed.precision = Some(precision.ok_or_else(unsupported)?);
|
|
71
|
+
rest = stripped;
|
|
72
|
+
}
|
|
73
|
+
let mut chars = rest.chars();
|
|
74
|
+
parsed.presentation = chars.next();
|
|
75
|
+
if chars.next().is_some() {
|
|
76
|
+
return Err(unsupported());
|
|
77
|
+
}
|
|
78
|
+
Ok(parsed)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn sign(&self, negative: bool) -> &'static str {
|
|
82
|
+
if negative {
|
|
83
|
+
"-"
|
|
84
|
+
} else if self.plus {
|
|
85
|
+
"+"
|
|
86
|
+
} else {
|
|
87
|
+
""
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Pad a rendered number. Zero padding goes between the sign and the digits, and explicit
|
|
92
|
+
/// alignment takes precedence over it, as in CPython.
|
|
93
|
+
fn pad_number(&self, sign: &str, body: String) -> String {
|
|
94
|
+
let body = if self.grouping {
|
|
95
|
+
group_decimal(&body, self.zero_width(sign.len()))
|
|
96
|
+
} else {
|
|
97
|
+
body
|
|
98
|
+
};
|
|
99
|
+
let rendered = format!("{sign}{body}");
|
|
100
|
+
if self.align.is_none() && self.zero {
|
|
101
|
+
let padding = self.width.saturating_sub(rendered.len());
|
|
102
|
+
return format!("{sign}{}{body}", "0".repeat(padding));
|
|
103
|
+
}
|
|
104
|
+
align(&rendered, self.width, self.align.unwrap_or(Align::Right))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The minimum grouped digit-field width when zero padding applies.
|
|
108
|
+
fn zero_width(&self, sign: usize) -> usize {
|
|
109
|
+
if self.zero && self.align.is_none() {
|
|
110
|
+
self.width.saturating_sub(sign)
|
|
111
|
+
} else {
|
|
112
|
+
0
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn leading_number(text: &str) -> Result<(Option<usize>, &str), std::num::ParseIntError> {
|
|
118
|
+
let digits = text.bytes().take_while(u8::is_ascii_digit).count();
|
|
119
|
+
if digits == 0 {
|
|
120
|
+
return Ok((None, text));
|
|
121
|
+
}
|
|
122
|
+
Ok((Some(text[..digits].parse()?), &text[digits..]))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Format an integer with the `d`, `b`, `o`, `x`, `X`, or default presentation.
|
|
126
|
+
pub(super) fn format_integer(value: BigInt, spec: &FormatSpec) -> Result<String, String> {
|
|
127
|
+
if spec.precision.is_some() {
|
|
128
|
+
return Err("precision is not allowed in integer format".into());
|
|
129
|
+
}
|
|
130
|
+
let (radix, prefix, uppercase) = match spec.presentation {
|
|
131
|
+
None | Some('d') => (10, "", false),
|
|
132
|
+
Some('b') => (2, "0b", false),
|
|
133
|
+
Some('o') => (8, "0o", false),
|
|
134
|
+
Some('x') => (16, "0x", false),
|
|
135
|
+
Some('X') => (16, "0X", true),
|
|
136
|
+
Some(other) => return Err(format!("unsupported integer format type {other:?}")),
|
|
137
|
+
};
|
|
138
|
+
if spec.alternate && radix == 10 {
|
|
139
|
+
return Err("alternate form is not allowed for decimal integers".into());
|
|
140
|
+
}
|
|
141
|
+
if spec.grouping && radix != 10 {
|
|
142
|
+
return Err("comma grouping requires decimal presentation".into());
|
|
143
|
+
}
|
|
144
|
+
let negative = value.sign() == Sign::Minus;
|
|
145
|
+
let magnitude = if negative { -value } else { value };
|
|
146
|
+
let mut digits = magnitude.to_str_radix(radix);
|
|
147
|
+
if uppercase {
|
|
148
|
+
digits.make_ascii_uppercase();
|
|
149
|
+
}
|
|
150
|
+
let prefix = if spec.alternate { prefix } else { "" };
|
|
151
|
+
if !prefix.is_empty() && spec.zero && spec.align.is_none() {
|
|
152
|
+
let content = spec.sign(negative).len() + prefix.len() + digits.len();
|
|
153
|
+
let padding = spec.width.saturating_sub(content);
|
|
154
|
+
return Ok(format!(
|
|
155
|
+
"{}{prefix}{}{digits}",
|
|
156
|
+
spec.sign(negative),
|
|
157
|
+
"0".repeat(padding)
|
|
158
|
+
));
|
|
159
|
+
}
|
|
160
|
+
Ok(spec.pad_number(spec.sign(negative), format!("{prefix}{digits}")))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Format a float with the `f`, `e`, `E`, `g`, `G`, `%`, or default presentation. `repr` is
|
|
164
|
+
/// Python's shortest round-trip text, used when neither a presentation nor a precision is given.
|
|
165
|
+
pub(super) fn format_float(value: f64, repr: &str, spec: &FormatSpec) -> Result<String, String> {
|
|
166
|
+
if spec.alternate {
|
|
167
|
+
return Err("alternate form is not supported for floats".into());
|
|
168
|
+
}
|
|
169
|
+
let precision = spec.precision.unwrap_or(6);
|
|
170
|
+
let magnitude = value.abs();
|
|
171
|
+
let body = match spec.presentation {
|
|
172
|
+
_ if !value.is_finite() => non_finite(value, spec.presentation),
|
|
173
|
+
Some('f') => format!("{magnitude:.precision$}"),
|
|
174
|
+
Some('e') => scientific(magnitude, precision, 'e'),
|
|
175
|
+
Some('E') => scientific(magnitude, precision, 'E'),
|
|
176
|
+
Some('g') => format_general(magnitude, precision, false, false),
|
|
177
|
+
Some('G') => format_general(magnitude, precision, false, true),
|
|
178
|
+
Some('%') => format!("{:.precision$}%", magnitude * 100.0),
|
|
179
|
+
None => match spec.precision {
|
|
180
|
+
Some(precision) => format_general(magnitude, precision, true, false),
|
|
181
|
+
None => repr.trim_start_matches('-').to_string(),
|
|
182
|
+
},
|
|
183
|
+
Some(other) => return Err(format!("unsupported floating-point format type {other:?}")),
|
|
184
|
+
};
|
|
185
|
+
let body = match spec.presentation {
|
|
186
|
+
Some('%') if !value.is_finite() => format!("{body}%"),
|
|
187
|
+
_ => body,
|
|
188
|
+
};
|
|
189
|
+
Ok(spec.pad_number(spec.sign(value.is_sign_negative()), body))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Format a string with the `s` or default presentation.
|
|
193
|
+
pub(super) fn format_text(value: &str, spec: &FormatSpec) -> Result<String, String> {
|
|
194
|
+
if spec.plus || spec.alternate || spec.zero || spec.grouping || spec.precision.is_some() {
|
|
195
|
+
return Err("numeric format options are not allowed for strings".into());
|
|
196
|
+
}
|
|
197
|
+
if !matches!(spec.presentation, None | Some('s')) {
|
|
198
|
+
return Err(format!(
|
|
199
|
+
"unsupported string format type {:?}",
|
|
200
|
+
spec.presentation.unwrap_or_default()
|
|
201
|
+
));
|
|
202
|
+
}
|
|
203
|
+
Ok(align(value, spec.width, spec.align.unwrap_or(Align::Left)))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn non_finite(value: f64, presentation: Option<char>) -> String {
|
|
207
|
+
let label = if value.is_nan() { "nan" } else { "inf" };
|
|
208
|
+
if matches!(presentation, Some('E' | 'G')) {
|
|
209
|
+
label.to_uppercase()
|
|
210
|
+
} else {
|
|
211
|
+
label.into()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn scientific(value: f64, precision: usize, marker: char) -> String {
|
|
216
|
+
let rendered = format!("{value:.precision$e}");
|
|
217
|
+
let (mantissa, exponent) = rendered.split_once('e').expect("Rust scientific format");
|
|
218
|
+
let exponent = exponent.parse::<i32>().expect("Rust scientific exponent");
|
|
219
|
+
format!("{mantissa}{marker}{exponent:+03}")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Format significant digits of a nonnegative finite value using Python's fixed/scientific
|
|
223
|
+
/// thresholds. The default presentation keeps a trailing `.0` and switches to scientific one
|
|
224
|
+
/// digit earlier than `g`.
|
|
225
|
+
fn format_general(value: f64, precision: usize, default_type: bool, uppercase: bool) -> String {
|
|
226
|
+
let precision = precision.max(1);
|
|
227
|
+
let decimals = precision.saturating_sub(1);
|
|
228
|
+
let scientific = format!("{value:.decimals$e}");
|
|
229
|
+
let (mantissa, exponent) = scientific.split_once('e').expect("Rust scientific format");
|
|
230
|
+
let exponent = exponent.parse::<i32>().expect("Rust scientific exponent");
|
|
231
|
+
let threshold = if default_type {
|
|
232
|
+
precision.saturating_sub(1)
|
|
233
|
+
} else {
|
|
234
|
+
precision
|
|
235
|
+
};
|
|
236
|
+
let use_scientific = exponent < -4 || usize::try_from(exponent).is_ok_and(|e| e >= threshold);
|
|
237
|
+
if use_scientific {
|
|
238
|
+
let mantissa = trim_fraction(mantissa, false);
|
|
239
|
+
return format!(
|
|
240
|
+
"{mantissa}{}{exponent:+03}",
|
|
241
|
+
if uppercase { 'E' } else { 'e' }
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
let decimals = usize::try_from(precision as i128 - i128::from(exponent) - 1)
|
|
245
|
+
.expect("fixed precision is nonnegative");
|
|
246
|
+
trim_fraction(&format!("{value:.decimals$}"), default_type)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn trim_fraction(value: &str, preserve_decimal: bool) -> String {
|
|
250
|
+
let Some((integer, fraction)) = value.split_once('.') else {
|
|
251
|
+
return value.to_string();
|
|
252
|
+
};
|
|
253
|
+
let fraction = fraction.trim_end_matches('0');
|
|
254
|
+
if fraction.is_empty() {
|
|
255
|
+
if preserve_decimal {
|
|
256
|
+
format!("{integer}.0")
|
|
257
|
+
} else {
|
|
258
|
+
integer.to_string()
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
format!("{integer}.{fraction}")
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Insert thousands separators in the leading digits of an unsigned `body`, first zero-filling
|
|
266
|
+
/// those digits until the grouped field reaches `minimum` characters. As in CPython, a separator
|
|
267
|
+
/// can make the result one character wider than `minimum`.
|
|
268
|
+
fn group_decimal(body: &str, minimum: usize) -> String {
|
|
269
|
+
let digits = body.bytes().take_while(u8::is_ascii_digit).count();
|
|
270
|
+
if digits == 0 {
|
|
271
|
+
return body.into();
|
|
272
|
+
}
|
|
273
|
+
let rest = &body[digits..];
|
|
274
|
+
let mut filled = digits;
|
|
275
|
+
while filled + (filled - 1) / 3 + rest.len() < minimum {
|
|
276
|
+
filled += 1;
|
|
277
|
+
}
|
|
278
|
+
let padded = format!("{}{}", "0".repeat(filled - digits), &body[..digits]);
|
|
279
|
+
let mut result = String::with_capacity(filled + filled / 3 + rest.len());
|
|
280
|
+
for (index, digit) in padded.chars().enumerate() {
|
|
281
|
+
if index > 0 && (filled - index).is_multiple_of(3) {
|
|
282
|
+
result.push(',');
|
|
283
|
+
}
|
|
284
|
+
result.push(digit);
|
|
285
|
+
}
|
|
286
|
+
result.push_str(rest);
|
|
287
|
+
result
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn align(value: &str, width: usize, alignment: Align) -> String {
|
|
291
|
+
let padding = width.saturating_sub(value.chars().count());
|
|
292
|
+
let left = match alignment {
|
|
293
|
+
Align::Right => padding,
|
|
294
|
+
Align::Center => padding / 2,
|
|
295
|
+
Align::Left => 0,
|
|
296
|
+
};
|
|
297
|
+
let right = padding - left;
|
|
298
|
+
format!("{}{}{}", " ".repeat(left), value, " ".repeat(right))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[cfg(test)]
|
|
302
|
+
mod tests {
|
|
303
|
+
use super::*;
|
|
304
|
+
|
|
305
|
+
fn float(value: f64, spec: &str) -> String {
|
|
306
|
+
format_float(value, &value.to_string(), &FormatSpec::parse(spec).unwrap()).unwrap()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn integer(value: i64, spec: &str) -> Result<String, String> {
|
|
310
|
+
format_integer(value.into(), &FormatSpec::parse(spec)?)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn parses_every_supported_field() {
|
|
315
|
+
assert_eq!(
|
|
316
|
+
FormatSpec::parse(">+#012,.3f").unwrap(),
|
|
317
|
+
FormatSpec {
|
|
318
|
+
align: Some(Align::Right),
|
|
319
|
+
plus: true,
|
|
320
|
+
alternate: true,
|
|
321
|
+
zero: true,
|
|
322
|
+
width: 12,
|
|
323
|
+
grouping: true,
|
|
324
|
+
precision: Some(3),
|
|
325
|
+
presentation: Some('f'),
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
for invalid in ["*<5", "1,,.2", "5.2ff", ".f", " d"] {
|
|
329
|
+
assert!(FormatSpec::parse(invalid).is_err(), "{invalid}");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn grouping_zero_fills_before_inserting_separators() {
|
|
335
|
+
assert_eq!(float(-1234.5, "+12,.2f"), " -1,234.50");
|
|
336
|
+
assert_eq!(float(1234.0, "012,.0f"), "0,000,001,234");
|
|
337
|
+
assert_eq!(float(12345.0, ",.0"), "1e+04");
|
|
338
|
+
assert_eq!(integer(1234, "+012,d").unwrap(), "+000,001,234");
|
|
339
|
+
assert_eq!(integer(-1234567, ",").unwrap(), "-1,234,567");
|
|
340
|
+
assert!(integer(42, ",x").is_err());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn percent_and_signs_match_python() {
|
|
345
|
+
assert_eq!(float(0.125, ".1%"), "12.5%");
|
|
346
|
+
assert_eq!(float(-0.5, "+08.1%"), "-0050.0%");
|
|
347
|
+
assert_eq!(float(f64::INFINITY, "+%"), "+inf%");
|
|
348
|
+
assert_eq!(float(f64::NAN, "E"), "NAN");
|
|
349
|
+
assert_eq!(integer(255, "#06x").unwrap(), "0x00ff");
|
|
350
|
+
assert_eq!(integer(5, "^+5").unwrap(), " +5 ");
|
|
351
|
+
}
|
|
352
|
+
}
|