Compare commits
28
Commits
d9a226bd2d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114d92063c | ||
|
|
2d0e2a7430 | ||
|
|
c0f72a8d0a | ||
|
|
047c8acda9 | ||
|
|
0b22ebc58b | ||
|
|
8aa1e49658 | ||
|
|
970a446f4e | ||
|
|
e4ded3acc7 | ||
|
|
d668594478 | ||
|
|
a57b51d365 | ||
|
|
437fd30313 | ||
|
|
8183d0a9fa | ||
|
|
eca642d8d8 | ||
|
|
4f0219dd11 | ||
|
|
60978f0fcf | ||
|
|
569b4a3665 | ||
|
|
00f1453ec8 | ||
|
|
0b440109dd | ||
|
|
3fdc5a90f0 | ||
|
|
56b72d8ba5 | ||
|
|
d05465b299 | ||
|
|
51b69bc291 | ||
|
|
3ef86e6479 | ||
|
|
974e2db019 | ||
|
|
3bfe881fba | ||
|
|
d60672f575 | ||
|
|
728292c5f6 | ||
|
|
c4d894315f |
@@ -25,6 +25,8 @@ jobs:
|
||||
run: python3 tools/validate_package_security.py
|
||||
- name: Validate compiler warnings
|
||||
run: python3 tools/validate_compiler_warnings.py
|
||||
- name: Validate trusted command documentation
|
||||
run: python3 tools/validate_trusted_command_docs.py
|
||||
- name: Validate CI workflow coverage
|
||||
run: python3 tools/validate_ci_workflow.py
|
||||
- name: Check lessons
|
||||
|
||||
+31
-4
@@ -13,18 +13,39 @@ This file is the beginner-safe status map for the current package. It separates
|
||||
|
||||
The v1.18.26 review identified missing-argument reads in standard-library built-ins. The current runtime now validates required argument counts centrally before any builtin indexes `args[]`. Covered calls include `math.abs`, `math.clamp`, `random.seed`, `random.int`, text helpers, CSV helpers, path helpers, and collection helpers. Missing arguments produce a beginner-facing `needs N arguments` runtime error.
|
||||
|
||||
Focused regression coverage: `tests/38_builtin_arity.claro` and `tests/39_random_inverted_range.claro`.
|
||||
Focused regression coverage: `tests/38_builtin_arity.claro`, `tests/39_random_inverted_range.claro`, and `tests/40_call_depth_guard.claro`.
|
||||
|
||||
The runtime now rejects inverted `random.int` ranges before modulo arithmetic with: `random.int needs the lower bound to be less than or equal to the upper bound.` This prevents invalid ranges from producing incorrect values or a divide-by-zero signal.
|
||||
|
||||
Verified in this checkout on 2026-09-22:
|
||||
User-defined function and object-method calls now have a documented maximum active call depth of 256. Exceeding it produces: `Claro function call depth exceeded the safe limit of 256. Simplify the recursion or add a stopping condition.` Ordinary non-recursive beginner programs are unaffected.
|
||||
|
||||
- `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /tmp/claro-h2-asan -lm` plus `ASAN_OPTIONS=detect_leaks=0 /tmp/claro-h2-asan tests/39_random_inverted_range.claro`: expected diagnostic; no AddressSanitizer or UndefinedBehaviorSanitizer report.
|
||||
Verified in this checkout on 2026-09-23:
|
||||
|
||||
- `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /tmp/claro-call-depth-asan -lm` plus `ASAN_OPTIONS=detect_leaks=0 /tmp/claro-call-depth-asan tests/40_call_depth_guard.claro`: expected diagnostic; no AddressSanitizer or UndefinedBehaviorSanitizer report.
|
||||
- `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /tmp/claro-arity-asan -lm` plus `ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=1 /tmp/claro-arity-asan tests/38_builtin_arity.claro`: all four missing-argument diagnostics; no sanitizer report.
|
||||
- `make -s all`: rebuilt both `claro` and `claro.exe` from current source.
|
||||
- `./claro test`: `PASS: 0 failure(s)`.
|
||||
- `./claro doctor`: all checks `OK`.
|
||||
- `./claro validate`: validation passed.
|
||||
|
||||
This slice does not yet fix recursion depth, memory cleanup, `LASTEXIT`, HTTP buffering/status handling, or other review findings. Claro remains a trusted-script interpreter, not a sandbox.
|
||||
The memory cleanup slices release the previous deep value when a runtime variable or map entry is overwritten, release temporary split argument arrays and strings from `DO`, `CALL`, `TEXT ... CONTAINS`, and `RANDOM`, release loaded program paths, lines, and pointer arrays at the end of each script run, and now release the remaining runtime-owned variables, functions, modules, classes, import paths, captured output, return value, and error strings before the interpreter exits. The expression evaluator now releases discarded intermediate `Value` operands and command boundaries release evaluated `SET`/`SAY` values after copying or printing them. `FOR EACH` now releases its copied collection after iteration, preventing discarded list/map copies from accumulating in long scripts. `IF` and `DO ... TIMES` now release their temporary control-expression values after branch/loop selection. `ASK` now releases evaluated prompt values and temporary input values after converting/copying them into runtime storage. Focused coverage is `tools/validate_memory_cleanup.py`, `tools/validate_expression_cleanup.py`, `tools/validate_control_flow_cleanup.py`, `tools/validate_control_expression_cleanup.py`, and `tools/validate_ask_prompt_cleanup.py`; each focused cleanup validator runs an ASan/UBSan build with LeakSanitizer checking. Verified on 2026-09-24: `python3 tools/validate_ask_prompt_cleanup.py` passes (2,000 prompt/input operations under ASan/UBSan/LSan); `make -s all`, `./claro test`, `./claro doctor`, and `./claro validate` all pass. A malformed-input ASan/UBSan/LSan smoke run did not pass: it reports two leaked `rt_error` message strings (140 bytes total) at `src/claro.c:142`. This unrelated existing diagnostic-path leak remains unaddressed and blocks claiming sanitizer-clean malformed-input handling. The `COUNT ... AS` command now releases its evaluated list/map value after extracting the item count. `python3 tools/validate_count_expression_cleanup.py` first reproduced a 28,000-byte leak across 2,000 list expressions, then passed with LeakSanitizer enabled after the fix. `GET ... AT ... AS ...` now releases its copied collection and empty-result temporary after storing the selected item; `python3 tools/validate_get_expression_cleanup.py` first reproduced 34,000 bytes of leaks across 2,000 empty-list lookups, then passed with LeakSanitizer enabled after the fix. Remaining memory-growth areas include other expression temporaries and command boundaries. The `RUN COMMAND` path remains trusted shell execution, not a sandbox.
|
||||
|
||||
The `FIND ... IN ... AS ...` command now releases its evaluated search value and copied list/map after storing the result. Focused coverage is `tools/validate_find_expression_cleanup.py`; it runs 2,000 searches under ASan/UBSan with LeakSanitizer enabled. Verified on 2026-09-24: `python3 tools/validate_find_expression_cleanup.py` passes with no reported leaks; `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This slice is runtime-verified under sanitizers; broader malformed-input sanitizer coverage remains blocked by the previously documented `rt_error` message leak.
|
||||
|
||||
`REMOVE` now releases its evaluated needle and copied list/map value after updating runtime storage. `python3 tools/validate_remove_expression_cleanup.py` passed with 2,000 repeated string-needle operations under ASan/UBSan/LSan. Removed elements from populated copied lists remain a separate ownership case. This slice is runtime-verified under sanitizers; malformed-input diagnostics still have the previously recorded `rt_error` leak.
|
||||
|
||||
`ADD ... TO ...` now releases both the evaluated item and copied list after `list_add` and `rt_set` have made their owned copies. `python3 tools/validate_add_expression_cleanup.py` first reproduced leaks under ASan/UBSan/LSan (2,000 repeated string concatenations), then passed after the ownership cleanup. Full checks passed: `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, all existing focused cleanup validators, and `git diff --check`. This slice is runtime-verified under sanitizers; malformed-input diagnostics still have the previously recorded `rt_error` leak.
|
||||
|
||||
`PUT ... KEY ... VALUE ...` now releases the evaluated map, key, and value copies after runtime storage has copied them. `python3 tools/validate_put_expression_cleanup.py` passes with 2,000 repeated string writes under ASan/UBSan/LSan. Verified 2026-09-26: `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, the focused sanitizer validator, and `git diff --check` pass. Separate malformed-argument sanitizer smoke tests for the existing builtin-arity and inverted-range diagnostics do not report out-of-bounds access, but LeakSanitizer reports the already documented `rt_error` diagnostic-string leaks (552 bytes/14 allocations for arity cases; 79 bytes/2 allocations for the inverted-range case). This slice is runtime-verified; those error-path leaks remain a separate blocker to claiming sanitizer-clean diagnostics.
|
||||
|
||||
The `RUN COMMAND` path now decodes POSIX `pclose()` wait status before storing `LASTEXIT`, so a child that exits with code 3 exposes `3` rather than the encoded status 768. Focused coverage is `tests/42_last_exit_code.claro`. HTTP responses now have a 1,048,576-byte cap and marker-like response bodies are preserved while extracting the final HTTP status marker. Focused coverage is `tools/validate_http_hardening.py`. Remaining memory-growth areas include other expression temporaries. Claro remains a trusted-script interpreter, not a sandbox.
|
||||
|
||||
`RUN COMMAND` is documented and validated as a trusted-code capability: it executes shell commands with the user's permissions and is not a sandbox. Claro does not claim untrusted-script safety or use a fragile blacklist sanitizer. Focused documentation coverage is `tools/validate_trusted_command_docs.py`.
|
||||
|
||||
`GET ... KEY ... AS ...` now releases its temporary map and key values after lookup. `python3 tools/validate_get_key_expression_cleanup.py` reproduced 1,004,000 leaked bytes before the change and passes after it with ASan/UBSan/LSan enabled. `EXISTS FILE ... AS ...` now releases its evaluated path value after checking it; `python3 tools/validate_exists_expression_cleanup.py` first reproduced 62,000 leaked bytes across 2,000 calls, then passed with ASan/UBSan/LSan enabled. These are narrow expression-temporary cleanup slices, not a claim that all runtime allocations are leak-free.
|
||||
|
||||
`LIST FOLDER ... AS ...` now releases its evaluated path and the temporary folder-list value after runtime storage copies the list. `python3 tools/validate_list_folder_expression_cleanup.py` reproduced 220,000 leaked bytes across 2,000 operations under ASan/UBSan/LSan before the change and passes after it with no leak report. The working-tree checks `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This is a narrow ownership improvement, not a claim that all runtime allocations are leak-free.
|
||||
The `DELETE FILE ...` and `DELETE FOLDER ...` commands now release their evaluated path values after converting the paths to owned strings. `python3 tools/validate_delete_expression_cleanup.py` first reproduced 178,000 leaked bytes across 2,000 `DELETE FILE` expression evaluations under ASan/UBSan/LSan, then passed after cleanup. This check covers expression ownership; malformed-input diagnostic paths still have the previously documented `rt_error` message leak. The `COPY FILE` and `MOVE FILE` commands now also release their evaluated source/destination path values after conversion to strings; `python3 tools/validate_copy_move_expression_cleanup.py` reproduced 74,000 bytes leaked across 1,000 copy/move pairs before the fix and is the focused ASan/UBSan/LSan regression gate. `CREATE FOLDER ...` now releases its evaluated path value after converting it to an owned path string; `python3 tools/validate_create_folder_expression_cleanup.py` reproduced 200,000 leaked bytes across 2,000 calls before the fix and passes with ASan/UBSan/LSan enabled. This narrow cleanup slice does not establish that all runtime allocations are leak-free.
|
||||
|
||||
## Feature matrix
|
||||
|
||||
@@ -279,3 +300,9 @@ Use feature docs with these expectations:
|
||||
- **Plans or experiments:** `PACKAGE_REGISTRY.md`, `WEB_SERVER_PLAN.md`, `EDITOR_EXTENSION_PLAN.md`, `GRAPHICS.md`, `SDL12.md`, `COMPLETE_PLATFORM_ROADMAP.md`, `FUTURE_FEATURES_ROADMAP.md`.
|
||||
|
||||
If a doc sounds more ambitious than this status map, treat this file as the current source of truth and update the older doc before teaching from it.
|
||||
|
||||
## 2026-09-28 random.int full-range arithmetic follow-up
|
||||
|
||||
`random.int` now calculates its inclusive range width and result in `long long`, avoiding signed-int overflow for the full accepted C `int` range. The regression fixture is `tests/43_random_int_extreme_bounds.claro`.
|
||||
|
||||
Verified: built with `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /home/ubuntu/.hermes/profiles/maxwell/cache/scratch/claro-rng-probe -lm`; ran `ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=1 /home/ubuntu/.hermes/profiles/maxwell/cache/scratch/claro-rng-probe tests/43_random_int_extreme_bounds.claro` (exit 0, no sanitizer report). Before the change, the same full-range call failed with UBSan signed integer overflow at `src/claro.c:347`. This validates the extreme-range runtime behavior under ASan/UBSan; `make -s all`, `./claro test` (0 failures, including this fixture), `./claro doctor`, `./claro validate`, and `git diff --check` pass.
|
||||
|
||||
@@ -8,3 +8,91 @@ Safety caps (to prevent memory abuse):
|
||||
- Maximum program lines: 500,000
|
||||
|
||||
If a file exceeds these limits, the loader fails cleanly.
|
||||
|
||||
## Expression-token cleanup
|
||||
|
||||
Each expression now releases its token strings and token-array storage before returning. This is a narrow cleanup boundary; runtime-owned variables, loaded programs, and other allocations remain separate follow-up work.
|
||||
|
||||
Focused verification:
|
||||
|
||||
```text
|
||||
python3 tools/validate_memory_cleanup.py
|
||||
```
|
||||
|
||||
The validator builds an AddressSanitizer/UndefinedBehaviorSanitizer binary, runs a repeated variable-overwrite probe, and confirms LeakSanitizer no longer reports allocations from `tokenize`/`toks_add`. The interpreter remains a trusted-script runtime, not a sandbox.
|
||||
|
||||
## Overwritten-value cleanup
|
||||
|
||||
Runtime variables and map entries own deep copies of their values. Replacing an existing variable or map entry now releases the previous string, list, or map value before storing its replacement. This is intentionally limited to overwrite boundaries; final runtime teardown remains a follow-up cleanup slice.
|
||||
|
||||
## Split-argument cleanup
|
||||
|
||||
Command argument lists created by `DO`, `CALL`, `TEXT ... CONTAINS`, and `RANDOM` are temporary parser storage. They now share one cleanup helper, so repeated calls do not retain the duplicated argument strings or pointer array. The helper does not change argument evaluation or syntax compatibility.
|
||||
|
||||
Focused verification builds with AddressSanitizer/UndefinedBehaviorSanitizer, repeatedly exercises a four-argument `DO`, and checks the cleanup helper before confirming the existing string, list, and map overwrite behavior.
|
||||
|
||||
The `REMOVE` command now releases its evaluated needle and copied list/map value after updating runtime storage. Focused verification: `python3 tools/validate_remove_expression_cleanup.py` runs 2,000 discarded string needles under ASan/UBSan/LSan; it passed with no reported leaks. This slice does not yet address the separate ownership of an item removed from a populated copied list.
|
||||
|
||||
The `PUT ... KEY ... VALUE ...` command now releases its evaluated map, key, and value copies after `map_put`/`rt_set` have copied the data they own. Focused verification: `python3 tools/validate_put_expression_cleanup.py` exercises 2,000 string-valued writes under ASan/UBSan/LSan and passes with no reported leaks. This covers expression-temporary ownership only; broader malformed-input sanitizer runs still report the previously documented `rt_error` diagnostic-string leaks.
|
||||
|
||||
## HTTP response handling
|
||||
|
||||
HTTP responses are capped at 1,048,576 bytes. Exceeding the cap produces a beginner-facing runtime error instead of retaining an unbounded response. The curl status suffix is taken from the final status marker, so a response body containing marker-like text is preserved. Existing `HTTP CHECK` URL safety rules remain unchanged.
|
||||
|
||||
Focused verification:
|
||||
|
||||
```text
|
||||
python3 tools/validate_http_hardening.py
|
||||
```
|
||||
|
||||
This validator uses a local HTTP server to check marker-like response text, status `200`, and the oversized-response diagnostic.
|
||||
|
||||
## External command trust boundary
|
||||
|
||||
`RUN COMMAND` intentionally executes a shell command with the user's permissions. It is a trusted-code capability, not a sandbox or an untrusted-script safety feature. Claro does not attempt a fragile blacklist sanitizer; users must review scripts before running them.
|
||||
|
||||
Focused documentation verification:
|
||||
|
||||
```text
|
||||
python3 tools/validate_trusted_command_docs.py
|
||||
```
|
||||
|
||||
## Control-flow expression cleanup
|
||||
|
||||
Conditions evaluated by `IF` are temporary runtime values. The `IF` command now
|
||||
releases its condition after choosing a branch, including when the condition is
|
||||
a text expression. This is a narrow cleanup boundary; other expression
|
||||
temporaries remain separate follow-up work.
|
||||
|
||||
Focused verification:
|
||||
|
||||
```text
|
||||
python3 tools/validate_control_expression_cleanup.py
|
||||
```
|
||||
|
||||
The validator builds with AddressSanitizer/UndefinedBehaviorSanitizer,
|
||||
executes 2,000 temporary `IF` conditions under LeakSanitizer, and checks the
|
||||
expected output.
|
||||
|
||||
## ASK temporary-value cleanup
|
||||
|
||||
`ASK` releases the evaluated prompt value after converting it to display text,
|
||||
and releases the temporary input value after `rt_set_checked` copies it into
|
||||
runtime storage. This keeps prompt and input strings from accumulating during
|
||||
repeated input loops without changing prompt or input behavior.
|
||||
|
||||
Focused verification:
|
||||
|
||||
```text
|
||||
python3 tools/validate_ask_prompt_cleanup.py
|
||||
```
|
||||
|
||||
The validator runs 2,000 prompt/input operations with AddressSanitizer, UndefinedBehaviorSanitizer, and LeakSanitizer enabled. The dedicated validator passes. A separate malformed-input sanitizer smoke run (`gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o ... -lm` followed by the generated malformed script) exits 1 due to two existing leaked error-message strings (140 bytes total) allocated in `rt_error` at `src/claro.c:142`; this is outside the ASK cleanup slice and remains a blocker to sanitizer-clean malformed-input validation.
|
||||
|
||||
## GET KEY temporary-value cleanup
|
||||
|
||||
`GET ... KEY ... AS ...` releases its evaluated map copy and key value after storing the selected value. `python3 tools/validate_get_key_expression_cleanup.py` runs 2,000 lookups under ASan/UBSan/LSan; before the cleanup it reproduced 1,004,000 bytes leaked, and after it passes with the expected output and no reported leaks.
|
||||
|
||||
## COPY/MOVE expression temporary cleanup
|
||||
|
||||
`COPY FILE ... TO ...` and `MOVE FILE ... TO ...` release both evaluated path values after converting them to owned path strings. `python3 tools/validate_copy_move_expression_cleanup.py` runs 1,000 copy/move pairs under ASan/UBSan/LSan; before the cleanup it reproduced 74,000 bytes leaked across 4,000 path-expression values, and after it passes with no reported leaks.
|
||||
|
||||
@@ -64,3 +64,5 @@ SAY LASTEXIT
|
||||
```
|
||||
|
||||
Use this carefully. It runs commands on the user's computer.
|
||||
|
||||
`RUN COMMAND` is for trusted code only. It is not a sandbox: it can start programs, read or change files, and use the same permissions as the user running Claro. Do not run scripts from an untrusted source, and do not treat this feature as a security boundary.
|
||||
|
||||
+71
-49
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
TEACH recurse
|
||||
DO recurse
|
||||
LEARNED
|
||||
|
||||
TRY
|
||||
DO recurse
|
||||
CATCH
|
||||
SAY LASTERROR
|
||||
ENDTRY
|
||||
@@ -0,0 +1 @@
|
||||
Claro function call depth exceeded the safe limit of 256. Simplify the recursion or add a stopping condition.
|
||||
@@ -0,0 +1,2 @@
|
||||
RUN COMMAND "exit 3" AS output
|
||||
SAY LASTEXIT
|
||||
@@ -0,0 +1 @@
|
||||
3
|
||||
@@ -0,0 +1,6 @@
|
||||
IMPORT "lib/random.claro" AS random
|
||||
CALL random.seed WITH 17
|
||||
CALL random.int WITH -2147483648, 2147483647
|
||||
SET value RESULT
|
||||
SAY value >= -2147483648
|
||||
SAY value <= 2147483647
|
||||
@@ -0,0 +1,2 @@
|
||||
YES
|
||||
YES
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that ADD releases its evaluated item under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-add-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "add_expressions.claro"
|
||||
script.write_text(
|
||||
'SET items TO LIST\n'
|
||||
+ 'ADD "transient" + " item" TO items\n' * 2000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: ADD expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: ADD expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check ASK prompt expression values are released under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-ask-prompt-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "ask_prompts.claro"
|
||||
script.write_text(
|
||||
''.join('ASK "question" + " value" AS answer\n' for _ in range(2000)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="", file=sys.stderr)
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, input="\n" * 2000,
|
||||
text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode:
|
||||
print("FAIL: ASK prompt cleanup probe failed")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: ASK prompt expression values still leak")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if run.stdout.count("question value\n") != 2000:
|
||||
print("FAIL: ASK prompt cleanup probe produced the wrong output")
|
||||
return 1
|
||||
print("PASS: ASK prompt expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for discarded control-flow expression Values."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-control-expression-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "control_expressions.claro"
|
||||
script.write_text(
|
||||
''.join(
|
||||
'IF "condition" + " value"\n'
|
||||
' SET seen TO YES\n'
|
||||
'ENDIF\n'
|
||||
for _ in range(2000)
|
||||
)
|
||||
+ ''.join(
|
||||
'DO "1" TIMES\n'
|
||||
' SET seen TO YES\n'
|
||||
'DONE\n'
|
||||
for _ in range(2000)
|
||||
)
|
||||
+ 'SAY seen\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
[
|
||||
"gcc", "-std=c99", "-O0", "-g",
|
||||
"-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm",
|
||||
], cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="", file=sys.stderr)
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode:
|
||||
print("FAIL: control-expression cleanup probe failed")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: discarded control-flow expression values still leak")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if run.stdout != "YES\n":
|
||||
print("FAIL: control-expression cleanup probe produced the wrong output")
|
||||
print(run.stdout, end="", file=sys.stderr)
|
||||
return 1
|
||||
print("PASS: discarded control-flow expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for control-flow expression Value cleanup under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-control-flow-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "control_flow.claro"
|
||||
script.write_text(
|
||||
'SET items TO LIST\n'
|
||||
'FOR EACH item IN items\n'
|
||||
' SAY item\n'
|
||||
'DONE\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
[
|
||||
"gcc", "-std=c99", "-O0", "-g",
|
||||
"-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm",
|
||||
], cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="", file=sys.stderr)
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode:
|
||||
print("FAIL: control-flow cleanup probe failed")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: control-flow expression values still leak")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if run.stdout != "":
|
||||
print("FAIL: control-flow cleanup probe produced the wrong output")
|
||||
print(run.stdout, end="", file=sys.stderr)
|
||||
return 1
|
||||
print("PASS: control-flow expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for COPY/MOVE FILE expression temporary ownership."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-copy-move-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "copy_move_expressions.claro"
|
||||
(tmp_path / "copy-source.txt").write_text("sample", encoding="utf-8")
|
||||
script.write_text(
|
||||
('COPY FILE "copy-source.txt" TO "copy-destination.txt"\n'
|
||||
'MOVE FILE "copy-destination.txt" TO "copy-source.txt"\n') * 1000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=tmp_path, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: COPY/MOVE FILE expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: COPY/MOVE FILE expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for discarded COUNT expression Values."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-count-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "count_expressions.claro"
|
||||
script.write_text(
|
||||
'SET items TO LIST\n'
|
||||
+ ''.join('COUNT items AS item_count\n' for _ in range(2000))
|
||||
+ 'SAY item_count\n', encoding="utf-8"
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="", file=sys.stderr)
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: discarded COUNT expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if run.stdout != "0\n":
|
||||
print("FAIL: COUNT cleanup probe produced the wrong output")
|
||||
print(run.stdout, end="", file=sys.stderr)
|
||||
return 1
|
||||
print("PASS: discarded COUNT expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for CREATE FOLDER expression temporary ownership."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-create-folder-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "create_folder_expressions.claro"
|
||||
script.write_text(
|
||||
'SET path TO "' + str(tmp_path / "new-folder") + '"\n'
|
||||
+ "CREATE FOLDER path\n" * 2000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: CREATE FOLDER expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: CREATE FOLDER expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for DELETE FILE expression temporary ownership."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-delete-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "delete_expressions.claro"
|
||||
script.write_text(
|
||||
'SET path TO "' + str(tmp_path / "unused") + '"\n'
|
||||
+ 'DELETE FILE path + ""\n' * 2000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: DELETE FILE expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: DELETE FILE expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for EXISTS FILE expression temporary ownership."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-exists-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "exists_expressions.claro"
|
||||
script.write_text(
|
||||
'SET path TO "missing-file-for-cleanup-probe"\n'
|
||||
+ 'EXISTS FILE path AS present\n' * 2000
|
||||
+ "SAY present\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: EXISTS FILE expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
if run.stdout != "NO\n":
|
||||
print(f"FAIL: unexpected EXISTS FILE probe output: {run.stdout!r}")
|
||||
return 1
|
||||
print("PASS: EXISTS FILE expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for discarded expression Value cleanup under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-expression-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "expressions.claro"
|
||||
script.write_text(
|
||||
'SET value TO "seed"\n'
|
||||
+ ''.join(
|
||||
'SET value TO "a" + "b"\n'
|
||||
'SET answer TO 1 + 2 * 3\n'
|
||||
for _ in range(2000)
|
||||
)
|
||||
+ 'SAY value\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
[
|
||||
"gcc", "-std=c99", "-O0", "-g",
|
||||
"-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm",
|
||||
], cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="", file=sys.stderr)
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode:
|
||||
print("FAIL: expression temporary cleanup probe failed")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: discarded expression values still leak")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if run.stdout != "ab\n":
|
||||
print("FAIL: expression cleanup probe produced the wrong output")
|
||||
print(run.stdout, end="", file=sys.stderr)
|
||||
return 1
|
||||
print("PASS: discarded expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for FIND expression temporaries."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-find-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "find_expressions.claro"
|
||||
script.write_text(
|
||||
"SET items TO LIST\n"
|
||||
+ 'FIND "Missing" IN items AS position\n' * 2000
|
||||
+ "SAY position\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: FIND expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
if run.stdout != "0\n":
|
||||
print(f"FAIL: unexpected FIND probe output: {run.stdout!r}")
|
||||
return 1
|
||||
print("PASS: FIND expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for GET AT expression temporaries."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-get-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "get_expressions.claro"
|
||||
script.write_text(
|
||||
"SET items TO LIST\n"
|
||||
+ "GET items AT 1 AS selected\n" * 2000
|
||||
+ "SAY selected\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: GET AT expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
if run.stdout != "\n":
|
||||
print(f"FAIL: unexpected GET AT probe output: {run.stdout!r}")
|
||||
return 1
|
||||
print("PASS: GET AT expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for GET KEY expression temporaries."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-get-key-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "get_key_expressions.claro"
|
||||
script.write_text(
|
||||
'SET items TO MAP\nPUT items KEY "selected" VALUE "value"\n'
|
||||
+ 'GET items KEY "selected" AS selected\n' * 2000
|
||||
+ "SAY selected\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: GET KEY expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
if run.stdout != "value\n":
|
||||
print(f"FAIL: unexpected GET KEY probe output: {run.stdout!r}")
|
||||
return 1
|
||||
print("PASS: GET KEY expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Focused HTTP regression checks for marker-safe bodies and bounded responses."""
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
MAX_HTTP_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/marker":
|
||||
body = b"before\n__CLARO_HTTP_STATUS__999\nafter"
|
||||
status = 200
|
||||
elif self.path == "/large":
|
||||
body = b"x" * (MAX_HTTP_BYTES + 1)
|
||||
status = 200
|
||||
else:
|
||||
body = b"not found"
|
||||
status = 404
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with tempfile.TemporaryDirectory(prefix="claro-http-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
script = tmp_path / "http.claro"
|
||||
script.write_text(
|
||||
'HTTP GET URL_MARKER AS body STATUS status\n'
|
||||
'SAY body\n'
|
||||
'SAY status\n'
|
||||
'HTTP GET URL_LARGE AS ignored\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
binary = tmp_path / "claro-http"
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
marker = f'"http://127.0.0.1:{server.server_port}/marker"'
|
||||
large = f'"http://127.0.0.1:{server.server_port}/large"'
|
||||
text = script.read_text(encoding="utf-8").replace("URL_MARKER", marker).replace("URL_LARGE", large)
|
||||
script.write_text(text, encoding="utf-8")
|
||||
run = subprocess.run([str(binary), str(script)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
expected_body = "before\n__CLARO_HTTP_STATUS__999\nafter\n200\n"
|
||||
if run.returncode == 0:
|
||||
print("FAIL: oversized HTTP response was accepted")
|
||||
print(run.stdout, end="")
|
||||
return 1
|
||||
if expected_body not in run.stdout:
|
||||
print("FAIL: marker-like response body was altered")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
if "HTTP response exceeds the safe size limit" not in run.stderr and "HTTP response exceeds the safe size limit" not in run.stdout:
|
||||
print("FAIL: oversized response did not get a beginner-facing diagnostic")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: HTTP status is separated from marker-like bodies and responses are bounded")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for LIST FOLDER expression temporary ownership."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-list-folder-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "list_folder_expressions.claro"
|
||||
folder = tmp_path / "folder"
|
||||
folder.mkdir()
|
||||
script.write_text(
|
||||
'SET path TO "' + str(folder) + '"\n'
|
||||
+ "LIST FOLDER path AS entries\n" * 2000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: LIST FOLDER expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: LIST FOLDER expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Focused regression check for expression-token cleanup under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-memory-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "overwrite.claro"
|
||||
script.write_text(
|
||||
'TEACH consume first, second, third, fourth\n'
|
||||
' RETURN first\n'
|
||||
'END\n'
|
||||
'SET value TO "first"\n'
|
||||
'SET value TO "second"\n'
|
||||
'SET items TO LIST\n'
|
||||
'ADD "one" TO items\n'
|
||||
'SET items TO LIST\n'
|
||||
'SET profile TO MAP\n'
|
||||
'PUT profile KEY "name" VALUE "Ada"\n'
|
||||
'PUT profile KEY "name" VALUE "Grace"\n'
|
||||
+ ''.join('DO consume "a", "b", "c", "d"\n' for _ in range(2000))
|
||||
+ 'SAY profile\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
[
|
||||
"gcc", "-std=c99", "-O0", "-g",
|
||||
"-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm",
|
||||
], cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="", file=sys.stderr)
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if "toks_add" in run.stderr or "tokenize" in run.stderr:
|
||||
print("FAIL: expression token allocations still leak")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if "load_program" in run.stderr:
|
||||
print("FAIL: loaded program storage still leaks")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
source = (ROOT / "src" / "claro.c").read_text(encoding="utf-8")
|
||||
if "static void split_args_free(char **parts, int count)" not in source:
|
||||
print("FAIL: split argument storage has no cleanup boundary")
|
||||
return 1
|
||||
if "static void value_free(Value v)" not in source or "value_free(v->val);" not in source:
|
||||
print("FAIL: overwritten runtime values are not released")
|
||||
return 1
|
||||
if "static void runtime_free(Runtime *rt)" not in source:
|
||||
print("FAIL: runtime-owned values have no final cleanup boundary")
|
||||
return 1
|
||||
functional = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=0"},
|
||||
)
|
||||
if functional.returncode or "[map]\n" not in functional.stdout:
|
||||
print("FAIL: overwrite cleanup probe produced the wrong output")
|
||||
print(functional.stdout, end="", file=sys.stderr)
|
||||
return 1
|
||||
print("PASS: expression token allocations are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that PUT releases evaluated map/key/value copies under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-put-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "put_expressions.claro"
|
||||
script.write_text(
|
||||
'SET items TO MAP\n'
|
||||
+ 'PUT items KEY "key" VALUE "transient" + " value"\n' * 2000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: PUT expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
print("PASS: PUT expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that REMOVE releases its evaluated needle under LeakSanitizer."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.name == "nt":
|
||||
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory(prefix="claro-remove-cleanup-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
binary = tmp_path / "claro-lsan"
|
||||
script = tmp_path / "remove_expressions.claro"
|
||||
script.write_text(
|
||||
'REMOVE "transient" + " needle" FROM missing\n' * 2000,
|
||||
encoding="utf-8",
|
||||
)
|
||||
build = subprocess.run(
|
||||
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||
"src/claro.c", "-o", str(binary), "-lm"],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
)
|
||||
if build.returncode:
|
||||
print(build.stdout, end="")
|
||||
print(build.stderr, end="")
|
||||
return build.returncode
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: REMOVE expression values still leak")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="")
|
||||
return 1
|
||||
if run.stdout != "":
|
||||
print(f"FAIL: unexpected REMOVE probe output: {run.stdout!r}")
|
||||
return 1
|
||||
print("PASS: REMOVE expression values are released")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ensure the learner-facing RUN COMMAND docs describe its trust boundary."""
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOC = ROOT / "docs" / "PRACTICAL_SCRIPTING.md"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = DOC.read_text(encoding="utf-8").lower()
|
||||
required = (
|
||||
"trusted code",
|
||||
"not a sandbox",
|
||||
"runs commands on the user's computer",
|
||||
)
|
||||
missing = [phrase for phrase in required if phrase not in text]
|
||||
if missing:
|
||||
raise SystemExit("RUN COMMAND documentation is missing: " + ", ".join(missing))
|
||||
print("Trusted command documentation validation complete")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user