From ed2352481774f60d76864ff11818f827410e029a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 25 Oct 2026 02:42:53 +0000 Subject: [PATCH] fix: harden Claro diagnostics and runtime guards Preserve source locations, validate malformed JSON, and add opt-in resource limits. Include executable error-guide coverage, learner pilot materials, native Windows acceptance helpers, and updated CI/documentation. --- .forgejo/workflows/ci.yml | 6 + .gitattributes | 2 + CONTRIBUTING.md | 12 + README.md | 3 +- docs/CLASSROOM_READINESS.md | 3 + docs/CLI.md | 4 +- docs/COMPLETE_PLATFORM_ROADMAP.md | 2 +- docs/CURRENT_STATUS.md | 16 +- docs/ERRORS.md | 139 +++++++++- docs/GRAPHICS.md | 2 +- docs/LEARNER_FEEDBACK_TEMPLATE.md | 32 +++ docs/LEARNER_PILOT.md | 92 +++++++ docs/NATIVE_WINDOWS_TESTING.md | 53 ++++ docs/PERFORMANCE.md | 2 +- docs/README.md | 4 + docs/RELEASE_STATUS.md | 9 + docs/RELIABILITY.md | 17 ++ docs/RUNTIME_LIMITS.md | 74 ++++-- docs/SDL12.md | 2 +- docs/SIX_TRACK_STATUS.md | 61 +++++ docs/SPEC.md | 2 +- docs/TESTING.md | 16 ++ docs/V1_16_PACKAGES_PROJECTS.md | 10 +- docs/WEB_SERVER_PLAN.md | 2 +- src/claro.c | 350 ++++++++++++++++++------- test98.bat | 21 ++ tests/error_examples.json | 56 ++++ tests/learner_pilot.json | 44 ++++ testwin.bat | 21 ++ tools/build_distribution.py | 2 +- tools/native_platform_probe.c | 35 +++ tools/validate_ci_workflow.py | 4 + tools/validate_documentation_status.py | 3 + tools/validate_error_guide.py | 25 ++ tools/validate_error_locations.py | 48 ++++ tools/validate_learner_pilot.py | 19 ++ tools/validate_learning_ownership.py | 2 +- tools/validate_package_security.py | 18 +- tools/validate_resource_guards.py | 71 +++++ tools/validate_v1_16.py | 2 +- tools/validate_v1_17.py | 8 +- tools/validate_version_convention.py | 10 +- 42 files changed, 1128 insertions(+), 176 deletions(-) create mode 100644 docs/LEARNER_FEEDBACK_TEMPLATE.md create mode 100644 docs/LEARNER_PILOT.md create mode 100644 docs/NATIVE_WINDOWS_TESTING.md create mode 100644 docs/SIX_TRACK_STATUS.md create mode 100644 test98.bat create mode 100644 tests/error_examples.json create mode 100644 tests/learner_pilot.json create mode 100644 testwin.bat create mode 100644 tools/native_platform_probe.c create mode 100644 tools/validate_error_guide.py create mode 100644 tools/validate_error_locations.py create mode 100644 tools/validate_learner_pilot.py create mode 100644 tools/validate_resource_guards.py diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 03498f6..065770d 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -35,6 +35,12 @@ jobs: run: python3 tools/validate_c89.py - name: Validate executable learning path run: python3 tools/validate_learning_syntax.py + - name: Validate diagnostics, error guide and learner pilot code + run: | + python3 tools/validate_error_locations.py + python3 tools/validate_error_guide.py + python3 tools/validate_learner_pilot.py + python3 tools/validate_resource_guards.py - name: Validate memory and malformed-input reliability run: python3 tools/validate_reliability.py - name: Validate CI workflow coverage diff --git a/.gitattributes b/.gitattributes index 583b49b..16092f8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ # Preserve DOS line endings for the Windows 98 COMMAND.COM build inputs. build98.bat -text whitespace=cr-at-eol build98.rsp -text whitespace=cr-at-eol +test98.bat -text whitespace=cr-at-eol +testwin.bat -text whitespace=cr-at-eol diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d7750b..f9894f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,18 @@ and test formatting, numeric boundaries, and simple functions. Do not introduce C99 syntax or depend on generated project headers. See [C89 notes](docs/C89.md). +## Learning-first charter + +Claro is a learning tool first and a general-purpose language second. Keep the +core small, predictable, and beginner-focused; prioritize executable lessons, +useful diagnostics, and reliability over feature breadth. Teach one recommended +spelling per concept while preserving supported compatibility forms. Do not +claim accessibility validation without feedback from intended learners. The +[learning-first charter](docs/LEARNING_FIRST.md) is the current product guidance. +Older ambitious plans are retained as historical context, not active priorities; +they do not imply that existing features are removed. See the current +[learning-first roadmap](docs/ROADMAP.md). + ## Beginner syntax consistency Follow [the syntax guide](docs/SYNTAX.md) in introductory teaching material. diff --git a/README.md b/README.md index ede0c1b..6706aa3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Claro helps beginners learn programming through readable commands, simple rules, and small programs. Accessibility for people with learning disabilities is a core design goal; it still needs testing with learners. -Development version: **v2026.10.20.223203** +Development version: **v2026.10.25.014405** This is a development snapshot, not a newly published release. See [current status](docs/CURRENT_STATUS.md) and the dated @@ -51,6 +51,7 @@ concept. Older forms remain supported in a separate compatibility reference. - [Documentation](docs/README.md) — learning guides and reference. - [Current roadmap](docs/ROADMAP.md) — the small set of active priorities. - [Learning-first rules](docs/LEARNING_FIRST.md) — how Claro is designed. +- [Active learning-first roadmap](docs/ROADMAP.md) — current priorities; older platform plans are historical context. - [Contributing](CONTRIBUTING.md) — source layout, builds, and validation. Existing features and compatibility syntax remain supported; they are not diff --git a/docs/CLASSROOM_READINESS.md b/docs/CLASSROOM_READINESS.md index 73cb383..583fa7b 100644 --- a/docs/CLASSROOM_READINESS.md +++ b/docs/CLASSROOM_READINESS.md @@ -5,6 +5,9 @@ Accessibility for learners with learning disabilities is a design goal and is **not yet validated** by intended learners. Passing tests does not establish it. Use [current status](CURRENT_STATUS.md) for scope, not older feature roadmaps. +The [ready-to-run pilot kit](LEARNER_PILOT.md) includes short tasks and a +[blank feedback form](LEARNER_FEEDBACK_TEMPLATE.md). No real session is implied. + ## Before a session - Use a build whose platform was actually tested; check `claro --version`. diff --git a/docs/CLI.md b/docs/CLI.md index bcc9ec0..2433bcf 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,6 +1,6 @@ # Claro Command Line -Claro v2026.10.20.223203 supports these main commands: +Claro v2026.10.25.014405 supports these main commands: ```bash claro file.claro @@ -58,7 +58,7 @@ claro package doctor claro package lock ``` -The v2026.10.20.223203 package command is a safer local project-file helper. It creates `claro.project`, `claro.lock`, and local folders under `packages/`. Package names may appear only once in `claro.project`; `claro package doctor` reports duplicate entries so the lockfile stays unambiguous. It also requires the project manifest to have exactly one non-empty `name:` field. It is not yet an online package registry. +The v2026.10.25.014405 package command is a safer local project-file helper. It creates `claro.project`, `claro.lock`, and local folders under `packages/`. Package names may appear only once in `claro.project`; `claro package doctor` reports duplicate entries so the lockfile stays unambiguous. It also requires the project manifest to have exactly one non-empty `name:` field. It is not yet an online package registry. ## IDE metadata diff --git a/docs/COMPLETE_PLATFORM_ROADMAP.md b/docs/COMPLETE_PLATFORM_ROADMAP.md index b1fecec..30bbd26 100644 --- a/docs/COMPLETE_PLATFORM_ROADMAP.md +++ b/docs/COMPLETE_PLATFORM_ROADMAP.md @@ -1,6 +1,6 @@ # Claro Complete Platform Roadmap -Current release: **Claro v2026.10.20.223203** +Current release: **Claro v2026.10.25.014405** This roadmap tracks the larger goal: Claro as a complete beginner-friendly programming platform, not only a toy scripting interpreter. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index b628004..6db10ab 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -1,6 +1,6 @@ # Claro current status -Development version: **v2026.10.20.223203** +Development version: **v2026.10.25.014405** Claro is a **learning tool first**. The development timestamp identifies source; it does not mean that a release was published or every platform was tested. @@ -28,8 +28,9 @@ call depth and the learning path under leak-enabled sanitizers. See Earlier failure logs are history, not current unresolved findings. Claro runs trusted scripts with the user's permissions. It is **not a sandbox**. -Intentional loops can run indefinitely, and global file/JSON/CPU budgets are not -provided. See [actual runtime guards](RUNTIME_LIMITS.md). +Loops can run indefinitely by default. Opt-in step and READ FILE budgets are +available; parsed JSON nesting is capped. Global file/output/memory and CPU-time +budgets are not provided. See [actual runtime guards](RUNTIME_LIMITS.md). ## Platforms — do not confuse compilation with OS acceptance @@ -55,9 +56,12 @@ They are **not active expansion priorities** without a separate request. ## What is next -1. Observe actual beginners and improve instructions/error recovery. -2. Verify the Windows builds on their real target operating systems. -3. Review the accumulated local changes and publish only when authorized. +1. Use the [learner pilot kit](LEARNER_PILOT.md) with a willing person; code checks + are not human feedback. Source-located expression diagnostics now have regressions. +2. Use the [native Windows kit](NATIVE_WINDOWS_TESTING.md) on an authorized target. + Wine is explicitly rejected by its OS evidence helper. +3. Hosted CI/release delivery needs authenticated Gitea access and enabled Actions. + See the [six-track status](SIX_TRACK_STATUS.md) for current blockers. Follow the [small active roadmap](ROADMAP.md). The [previous detailed status](history/CURRENT_STATUS_BEFORE_STATUS_CLEANUP.md) and diff --git a/docs/ERRORS.md b/docs/ERRORS.md index 4ca1672..034dd30 100644 --- a/docs/ERRORS.md +++ b/docs/ERRORS.md @@ -1,29 +1,144 @@ -# Claro Errors +# Errors and small repairs -Claro tries to report mistakes in beginner-friendly language. +An error starts with `filename:line:`. Open that file and find that line. +The line belongs to the function or imported file where the error happened, +not automatically to the caller. Caught errors expose the same origin through +`LASTERRORFILE` and `LASTERRORLINE`. -Examples: +Use `claro check my_program.claro` for command/block mistakes; running the +program also detects runtime expression/value errors. A checker is not a sandbox. +The examples below are executed by `tools/validate_error_guide.py`. + +## Quote + +Save as `quote.claro`. Run `claro quote.claro`. + +```claro +SAY "Start" +SAY "unfinished +``` + +Message: ```text -I do not know the command "PRNT". Did you mean "SAY"? +quote.claro:2: Text is missing a closing quote. Add a double quote at the end. ``` +One repair: + +```claro +SAY "Start" +SAY "finished" +``` + +## Operand + +Save as `operand.claro`. Run `claro operand.claro`. + +```claro +SET score 1 +SAY score + +``` + +Message: + ```text -SET needs TO. -Try: -SET name TO "Alex" +operand.claro:2: This expression needs a value after the operator. ``` +One repair: + +```claro +SET score 1 +SAY score + 1 +``` + +## Command + +Save as `command.claro`. Run `claro check command.claro`. + +```claro +PRNT "Hello" +``` + +Message: + ```text -IF block opened here is missing ENDIF +command.claro:1: I do not know the command "PRNT". Did you mean "SAY"? ``` +One repair: + +```claro +SAY "Hello" +``` + +## Assignment + +Save as `assignment.claro`. Run `claro check assignment.claro`. + +```claro +SET name +``` + +Message: + ```text -I do not know the variable "socre" yet. Did you mean "score"? +assignment.claro:1: SET needs a name and a value. Try: SET name "Alex" ``` -For safer learning, run this before running a new program: +One repair: -```bash -./claro check my_program.claro +```claro +SET name "Alex" +SAY name ``` + +## Choice + +Save as `choice.claro`. Run `claro check choice.claro`. + +```claro +IF YES + SAY "Yes" +``` + +Message: + +```text +choice.claro:1: IF block opened here needs END +``` + +One repair: + +```claro +IF YES + SAY "Yes" +END +``` + +## Variable + +Save as `variable.claro`. Run `claro variable.claro`. + +```claro +SET score 10 +SAY socre +``` + +Message: + +```text +variable.claro:2: I do not know the variable "socre" yet. +``` + +One repair: + +```claro +SET score 10 +SAY score +``` + +These teach short `SET` and `END`. Older forms remain supported in +[compatibility syntax](COMPATIBILITY.md), not required as repairs. +For a caught error, read its metadata before another error changes it. diff --git a/docs/GRAPHICS.md b/docs/GRAPHICS.md index 3169c3d..cef0ab9 100644 --- a/docs/GRAPHICS.md +++ b/docs/GRAPHICS.md @@ -1,6 +1,6 @@ # Claro Graphics Status -Current release: **Claro v2026.10.20.223203** +Current release: **Claro v2026.10.25.014405** Graphics support is intentionally separated from the stable beginner console workflow. diff --git a/docs/LEARNER_FEEDBACK_TEMPLATE.md b/docs/LEARNER_FEEDBACK_TEMPLATE.md new file mode 100644 index 0000000..b56e1d4 --- /dev/null +++ b/docs/LEARNER_FEEDBACK_TEMPLATE.md @@ -0,0 +1,32 @@ +# Learner feedback — blank observation form + +Status: NOT RUN. Leave observations blank until a consenting person participates. +Do not fill this form with predictions, agent simulations or invented participants. + +- Anonymous session ID: +- Consent to retain anonymous notes: yes / no +- Build version and artifact checksum: +- OS/device and assistive tools (only if voluntarily relevant): +- Tasks attempted / skipped: +- Breaks, pacing and requested adjustments: + +For each attempted task record: + +- What the learner tried or said (anonymous, no health information): +- What worked independently: +- What help or clarification was requested: +- Exact confusing instruction/error and its filename/line: +- Whether the suggested repair helped: +- Learner's suggested change: + +Use plain observations, not ability rankings. Do not infer a medical diagnosis. + +Debrief: + +- Clearest part: +- Most confusing part: +- One small change to try: +- Follow-up session agreed, if any: +- Notes retention/deletion agreement: + +Automated example execution verifies code/output only, not these observations. diff --git a/docs/LEARNER_PILOT.md b/docs/LEARNER_PILOT.md new file mode 100644 index 0000000..2154af7 --- /dev/null +++ b/docs/LEARNER_PILOT.md @@ -0,0 +1,92 @@ +# Beginner pilot — observer kit + +**Status: prepared, not learner-validated.** No participant feedback has been +collected by these automated checks. Do not infer accessibility from test passes. + +Ask for willing participation and permission to keep anonymous notes. Explain +that we are testing the instructions, not the person. Collect no diagnosis, +medical history, real names or sensitive information. Let the learner decline, +ask for help, take breaks or stop. Use any assistive technology they prefer. + +Use a platform-tested build, a fresh writable practice folder and reviewed scripts. +Show how to stop/close the interpreter before starting. Do not run downloaded +untrusted scripts. Explain that optional limits are not a sandbox. + +Try the first three tasks at the learner's pace. Offer the rest only if wanted. +Let the learner predict, run and change the program. Observe before giving a +hint; never leave someone distressed just to measure independence. Record help +without judging it. Answers/output for observers are in `tests/learner_pilot.json`. + +## Say hello + +Run this once. Change the message to something you choose and run it again. + +```claro +SAY "Hello!" +``` + +## Ask a name + +Run it and type a pretend name. Explain where that name is remembered. + +```claro +ASK "Name? " name +SAY "Hello " + name +``` + +## Change a number + +Predict the result, run it, then change 2 to another small number. + +```claro +SET score 2 +SAY score + 1 +``` + +## Make a choice + +Optional: change score to 0. Predict which message appears. + +```claro +SET score 2 +IF score > 1 + SAY "More than one" +ELSE + SAY "One or less" +END +``` + +## Repeat + +Optional: change how many times the message appears. Ask how to stop a stuck run. + +```claro +DO 3 TIMES + SAY "Again" +END +``` + +## Save a file + +Optional: use a new writable practice folder. Find the file after running it. + +```claro +WRITE FILE "practice.txt" WITH "My first file" +READ FILE "practice.txt" AS text +SAY text +``` + +## Gentle mistake and recovery + +In the hello task, replace SAY with PRNT, run `claro check practice.claro`, and +ask what the message suggests. Restore SAY and rerun. This mistake is optional; +skip it if it adds stress. Do not teach alternative compatibility spellings here. + +## Debrief and next change + +Ask: What was clear? What was confusing? What would you change? Observe whether +the person can start/run/save, predict a result and use a repair hint. Do not +calculate a disability score or call one session proof of universal accessibility. +Use [the blank feedback form](LEARNER_FEEDBACK_TEMPLATE.md); keep only consented, +anonymous notes, record the exact build/platform, and choose one small improvement. +The [classroom checklist](CLASSROOM_READINESS.md) is a pilot guide, not certification. diff --git a/docs/NATIVE_WINDOWS_TESTING.md b/docs/NATIVE_WINDOWS_TESTING.md new file mode 100644 index 0000000..76f591c --- /dev/null +++ b/docs/NATIVE_WINDOWS_TESTING.md @@ -0,0 +1,53 @@ +# Native Windows acceptance kit + +**Prepared, not executed on a native target.** Wine results do not satisfy this +checklist. A Windows login/authorized VM was not supplied for this run. + +Use a **fresh extracted test folder**, not personal documents. Scripts write +`oscheck.txt`, `nver.txt`, `ntests.txt`, `ncheck.txt` and replace their own `native.ok` +marker. Preserve failed logs. A marker alone is not proof: review the OS evidence, +version, all discovered fixtures and every test result together. + +## OS evidence helper + +`tools/native_platform_probe.c` builds a separate `osprobe.exe`; it is not a +second interpreter source file. With VC6 configured: + +```bat +cl /nologo /TC /ML /O1 tools\native_platform_probe.c /Feosprobe.exe /link /SUBSYSTEM:CONSOLE,4.10 +``` + +The probe records numeric OS API fields, detects Wine's exported implementation +entry point, and refuses Wine with exit 2. `--require-win98se` additionally needs +Win32-Windows platform ID and version 4.10 build 2222. A real Windows VM is an +acceptable target; a compatibility layer is not. Old API version reporting on +modern Windows can be affected by application compatibility, so also record the +actual OS/version from its own system information screen. The probe is evidence, +not cryptographic attestation or a substitute for inspecting a real target. + +## Automated checks + +Put the compiled probe beside the matching interpreter and the complete package. +For Windows 98 SE run [test98.bat](../test98.bat). For modern x64 Windows run +[testwin.bat](../testwin.bat) with the MinGW `claro.exe` build. +These batch scripts avoid modern CMD-only extensions and reject a wrong target +before writing a PASS marker. Review `ntests.txt`: every packaged golden fixture +must appear, with zero failures. `ncheck.txt` must show built-in validation passed. + +## Manual target observations — leave blank until actually tested + +- Artifact version/SHA-256 and OS/VM identity: +- `oscheck.txt` reviewed, Wine false, target version independently confirmed: +- All packaged fixtures discovered/passed: +- Keyboard input, editing and non-ASCII display observations: +- Save/read a text file in a writable folder: +- File paths containing spaces: +- Start/stop a loop and recover to the console: +- First-hour examples and quiz interaction: +- Missing input or failed command recovery: +- Tester/date, consent to retain anonymous notes, unresolved failures: + +Transfer only consented technical logs, not passwords, personal files or medical +information. Do not change an unknown remote machine or install services just to +make a test pass. The native target remains **unverified** until these observations +and actual target logs exist. See [Windows 98 compilation](WINDOWS98.md). diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 6132388..990c2a8 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -1,6 +1,6 @@ # Claro Performance and Optimization Notes -Current release: **Claro v2026.10.20.223203** +Current release: **Claro v2026.10.25.014405** Performance work should come after language behavior is stable and measured. Do not optimize blindly. diff --git a/docs/README.md b/docs/README.md index 8f038e6..bb3e95d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,10 @@ - [Reliability](RELIABILITY.md) and [runtime guards](RUNTIME_LIMITS.md) — tested boundaries, not sandbox guarantees. - [Portability](PORTABILITY.md) and [Windows 98 / VC6](WINDOWS98.md) — compilation versus target execution. +- [Learner pilot kit](LEARNER_PILOT.md) and [blank feedback](LEARNER_FEEDBACK_TEMPLATE.md) +- [Native Windows acceptance kit](NATIVE_WINDOWS_TESTING.md) +- [Six-track progress and blockers](SIX_TRACK_STATUS.md) + ## Look something up - [Beginner syntax](SYNTAX.md) and [older compatibility forms](COMPATIBILITY.md) diff --git a/docs/RELEASE_STATUS.md b/docs/RELEASE_STATUS.md index 79d2906..25f5d1c 100644 --- a/docs/RELEASE_STATUS.md +++ b/docs/RELEASE_STATUS.md @@ -19,6 +19,15 @@ A development timestamp is not a stable-release designation. Use [current status](CURRENT_STATUS.md) for the learning scope and [the release checklist](FINAL_RELEASE_CHECKLIST.md) before publishing. +## Candidate continuation — 2026-10-21 + +The last pushed source checkpoint is `d55507140660186e594303306647e0f0e9facf76`. +The continuation candidate is a separate development run; see +[six-track status](SIX_TRACK_STATUS.md) for scope and blocked acceptance. +Gitea currently reports Actions disabled, and authenticated runs/release access +was not available for this continuation. No hosted CI success or hosted release +is claimed. Earlier artifact records below retain their original hashes/versions. + ## Immutable artifact checkpoints These are verified build artifacts previously delivered for testing, **not a diff --git a/docs/RELIABILITY.md b/docs/RELIABILITY.md index 6820191..0bfb847 100644 --- a/docs/RELIABILITY.md +++ b/docs/RELIABILITY.md @@ -41,6 +41,23 @@ fictional printed list representation. Five isolated ownership tests separate metadata, field storage and object construction so one leak cannot hide another. Malformed-expression and nesting tests check failure diagnostics and valid inputs. +## Diagnostics, JSON and opt-in budgets + +Expression errors keep source origins across calls, imports and TRY/CATCH. +The error guide is executable-verified. Malformed JSON now fails instead of +hanging, nesting is bounded, control-character round trips are checked, and +Unicode escapes/surrogate pairs are covered. JSON temporaries, serialization +copies and popped import-stack entries have focused leak-enabled regressions. +The optional step budget counts empty loop entries; READ FILE can enforce a +per-read byte cap. These are not process isolation or wall-clock bounds. + +`validate_error_locations.py`, `validate_error_guide.py`, `validate_learner_pilot.py` +and `validate_resource_guards.py` run in the full gate and inside the existing +learning-ownership sanitizer check. The reliability runner still counts 35 +maintained top-level checks; subcase coverage has expanded, not been relabeled +as 35 new independent platform tests. Human feedback and native Windows tests +remain separate, blocked acceptance tracks. + ## Scope A passing run establishes the exercised boundaries, not universal leak freedom, diff --git a/docs/RUNTIME_LIMITS.md b/docs/RUNTIME_LIMITS.md index 1b22004..c6bc963 100644 --- a/docs/RUNTIME_LIMITS.md +++ b/docs/RUNTIME_LIMITS.md @@ -1,32 +1,58 @@ # Current runtime guards -These are implemented guardrails, not a sandbox or a promise that arbitrary -scripts have bounded resource use. +Claro runs trusted scripts with the user's permissions. These guards are **not +a sandbox**, a wall-clock timeout, or proof that arbitrary scripts use bounded resources. -- **Function/method calls:** at most 256 active calls. Excessive recursion raises - a Claro error. Other limits can stop a program earlier. -- **Expressions:** at most 128 nested parser levels, covering parentheses and - unary operations. Excessive nesting produces a diagnostic instead of - continuing recursive parsing. -- **Control blocks:** at most 128 additional nested control-block frames. The - runtime and syntax checker reject deeper blocks. Function-call frames have - their own guard. -- **HTTP response bodies:** limited to 1,048,576 bytes by the existing HTTP path. +## Always-on nesting/response guards -Malformed expressions such as a lone `!`, an unclosed quote/parenthesis, -a missing operand, or trailing expression tokens are rejected by the runtime. -Use `NOT` for logical negation; `!=` remains a supported comparison. +- Function/method calls: at most 256 active calls. +- Expressions and additional nested control blocks: at most 128 levels. +- Parsed JSON: at most 128 nested arrays/objects. A scalar inside 128 containers + is allowed; a 129th container is rejected. Malformed syntax fails with a Claro + diagnostic rather than looping without consuming input. JSON Unicode escapes + and surrogate pairs are decoded; embedded zero characters are rejected because + Claro text uses zero-terminated strings. This is not a general encoding validator. +- HTTP response bodies: the existing 1,048,576-byte cap remains. + +Errors raised during expressions keep the executing source file and line, +including imported functions and caught `LASTERRORFILE`/`LASTERRORLINE` metadata. +REPEAT without UNTIL is rejected before accessing a missing terminator. + +## Optional guards — disabled by default + +`CLARO_STEP_LIMIT` sets a non-negative interpreter-work budget. Zero/unset means +unlimited. It counts source-line dispatches and block/range entries, including +empty loop iterations; it is **not** an exact statement count or elapsed time. +Once exhausted it reports a source-located error suggesting the loop be checked. +The consumed budget is not reset by TRY/CATCH or by calling a function. + +`CLARO_READ_FILE_LIMIT` sets the maximum bytes read by each `READ FILE` command. +Zero/unset means unlimited. The exact boundary is accepted; larger input errors +without storing a partial result. It does not cap source loading, package metadata, +all file operations, aggregate memory or output. Invalid/overflowing limit values +fail configuration; supported numeric range depends on the compiler's unsigned long. + +For example, in a Windows command prompt before running a reviewed script: + +```bat +set CLARO_STEP_LIMIT=10000 +set CLARO_READ_FILE_LIMIT=1048576 +claro98.exe practice.claro +``` + +On Linux/macOS shells the equivalent is an environment assignment before `./claro`. +These optional budgets do not interrupt a blocked ASK, a shell command, curl, +or an individual long-running builtin. They are not untrusted-code protection. ## Not provided -The former beta document described `CLARO_STEP_LIMIT`, `CLARO_MAX_FILE_BYTES`, -and `CLARO_MAX_JSON_BYTES`. Those environment controls are not implemented in -the current `src/claro.c`; do not depend on them. The original document is -preserved under [history](history/RUNTIME_LIMITS_BETA25.md). +There is no general CPU/time budget, process isolation, total-allocation limit, +global file/output limit or JSON byte-size control. The old beta names +`CLARO_MAX_FILE_BYTES` and `CLARO_MAX_JSON_BYTES` are still not implemented. +The newer READ FILE guard is deliberately narrower. The former unimplemented +step-limit claim is preserved as history; the current step guard is the explicit +implementation described above, not validation of those older promises. -Ordinary intentional loops can still run indefinitely. This checkpoint does -not establish a general JSON recursion limit, global file/output-size limit, -CPU budget, or untrusted-script protection. Scripts and `RUN COMMAND` execute -with the user's permissions. - -See [reliability verification](RELIABILITY.md) for the tested scope. +Without the optional step budget ordinary intentional loops can run indefinitely. +Run only trusted scripts. See [reliability](RELIABILITY.md), +[errors and repairs](ERRORS.md), and the [historical limits](history/RUNTIME_LIMITS_BETA25.md). diff --git a/docs/SDL12.md b/docs/SDL12.md index 2597112..1898aa8 100644 --- a/docs/SDL12.md +++ b/docs/SDL12.md @@ -2,7 +2,7 @@ SDL 1.2 support is **not included in the current stable executable**. -The `examples/experimental_sdl/` files are design experiments / future examples only. In Claro v2026.10.20.223203, running them with the included `claro.exe` will fail with: +The `examples/experimental_sdl/` files are design experiments / future examples only. In Claro v2026.10.25.014405, running them with the included `claro.exe` will fail with: ```text Unknown command: SDL diff --git a/docs/SIX_TRACK_STATUS.md b/docs/SIX_TRACK_STATUS.md new file mode 100644 index 0000000..06ce813 --- /dev/null +++ b/docs/SIX_TRACK_STATUS.md @@ -0,0 +1,61 @@ +# Six-track progress — development candidate v2026.10.21.010934 + +This is a work/acceptance map, not a stable release or a claim all six external +milestones are complete. Real learner sessions and native OS access were not +supplied in the clarification request. No simulated observations are recorded. + +## 1. Error reporting — implemented, regression-tested + +Expressions retain file/line origins through functions, imports, UNTIL and +TRY/CATCH metadata. The first failing location is not overwritten by its caller. +Focused tests cover direct, nested and caught paths, including leak-enabled runs. + +## 2. Error guide — aligned and executable-verified + +The [guide](ERRORS.md) now shows current short SET/END forms and six actual +failure/repair pairs. `tests/error_examples.json` is checked against every guide +code block and against the interpreter's diagnostic and repaired output. + +## 3. Beginner testing — kit ready; actual learner acceptance BLOCKED + +The [pilot kit](LEARNER_PILOT.md) contains six small starter tasks (three primary, +three optional), observer instructions and a [blank feedback form](LEARNER_FEEDBACK_TEMPLATE.md). +Automated code/output checks are not a person testing the language. A willing, +consenting learner and anonymous real observations are still required. + +## 4. Native Windows — test kit prepared; target acceptance BLOCKED + +[Native checks](NATIVE_WINDOWS_TESTING.md) require an authorized Windows login or +real Windows 98 SE VM. No such access was supplied. The OS helper explicitly +rejects Wine and can require Win98 SE 4.10.2222 before a PASS marker is written. +Compiler/Wine checks are separate evidence and never counted as target acceptance. + +## 5. Robustness — focused fixes and guards, not a sandbox + +Malformed JSON such as `[?]` previously hung; it now produces a located diagnostic. +Containers are bounded at 128, separators/numbers/escapes are checked, Unicode +escapes/surrogate pairs and control-character serialization are covered. JSON +parse/serialize temporaries and popped import entries have cleanup regressions. +Missing UNTIL is rejected. Opt-in step and per-READ FILE byte budgets are described +in [runtime guards](RUNTIME_LIMITS.md). Empty loops consume work budget; TRY/CATCH +does not reset it. These do not bound all memory, time, output or external commands. + +## 6. Release delivery — candidate validation; hosted gate BLOCKED + +Local source/Linux/Windows candidate archives must pass extracted-payload checks +before delivery. Actual checksums/reports belong to those frozen artifacts, not +just the source checkout. The VC6 ZIP uses a separate manifest/report path. + +On this run the Gitea repository metadata reported `has_actions: false`; the +Actions runs API returned HTTP 401, the Actions web page was unavailable, and +there was no authenticated browser/API credential. Secure vault entry was not +available in this session. Hosted CI cannot be verified or enabled from Git SSH +access alone. No hosted release or CI success is claimed and no release assets +have been uploaded. Authenticated administration and enabled/configured Actions +are needed before completing the requested hosted CI/release gate. + +The repository API also reported `private: false`. No visibility change was made; +do not put credentials or sensitive learner/native-host information into this repo. + +See [release status](RELEASE_STATUS.md) for dated earlier checkpoints. Candidate +build evidence does not retroactively validate native Windows or accessibility. diff --git a/docs/SPEC.md b/docs/SPEC.md index 80fa386..c1be89f 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1,6 +1,6 @@ # Claro Language Specification -Current package: **Claro v2026.10.20.223203** +Current package: **Claro v2026.10.25.014405** Status: **Living v1 specification** This document defines the intended behavior of the Claro language. It is written to reduce breaking changes and keep the v1 line teachable and predictable. diff --git a/docs/TESTING.md b/docs/TESTING.md index a4481d1..ab513f1 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -59,6 +59,22 @@ Wine results must be labeled Wine results. Claim native Windows/Win98 support only after running the target tests there. [Release status](RELEASE_STATUS.md) records the dated scope; [the checklist](FINAL_RELEASE_CHECKLIST.md) governs publication. +## New focused checks + +```bash +python3 tools/validate_error_locations.py +python3 tools/validate_error_guide.py +python3 tools/validate_learner_pilot.py +python3 tools/validate_resource_guards.py +``` + +They are also part of the full gate. `validate_learning_ownership.py` runs them +with leak detection enabled, including negative paths. Pilot code checks are +not learner observations. [Native acceptance](NATIVE_WINDOWS_TESTING.md) requires +real target logs; the Wine-rejecting probe prevents silently substituting a +compatibility-layer result. Current [six-track status](SIX_TRACK_STATUS.md) +separates implemented work from blocked human/hosted acceptance. + ## Historical tools Older `validate_rc*.py` and versioned validators remain for history and focused diff --git a/docs/V1_16_PACKAGES_PROJECTS.md b/docs/V1_16_PACKAGES_PROJECTS.md index dcca14b..734d384 100644 --- a/docs/V1_16_PACKAGES_PROJECTS.md +++ b/docs/V1_16_PACKAGES_PROJECTS.md @@ -1,6 +1,6 @@ -# Claro v2026.10.20.223203 - Packages and Projects +# Claro v2026.10.25.014405 - Packages and Projects -Claro v2026.10.20.223203 makes projects and packages safer and more useful while keeping the beginner syntax simple. +Claro v2026.10.25.014405 makes projects and packages safer and more useful while keeping the beginner syntax simple. ## Create a project @@ -28,7 +28,7 @@ README.md manifest-version: 1 name: MyProject main: main.claro -version: v2026.10.20.223203 +version: v2026.10.25.014405 packages: package: text package: net_tools @@ -77,10 +77,10 @@ If `package doctor` reports `BAD lock checksum: math-tools`, check the `math-too BAD lock version: expected exactly one lock-version: 1 line ``` -The lockfile must also contain exactly one current release line, `version: v2026.10.20.223203`. A missing, stale, blank, or duplicate release line fails with: +The lockfile must also contain exactly one current release line, `version: v2026.10.25.014405`. A missing, stale, blank, or duplicate release line fails with: ```text -BAD lock release version: expected exactly one version: v2026.10.20.223203 line +BAD lock release version: expected exactly one version: v2026.10.25.014405 line ``` The doctor leaves your files unchanged. After reviewing your project and package manifests, run `claro package lock` to regenerate the lockfile, then run `claro package doctor` again. Spaces around values and capitalization of the lock-format field name are accepted, as with other lockfile fields. Valid generated lockfiles continue to work unchanged; this check does not add registry downloads or verify package content hashes. diff --git a/docs/WEB_SERVER_PLAN.md b/docs/WEB_SERVER_PLAN.md index e5925eb..0332d5a 100644 --- a/docs/WEB_SERVER_PLAN.md +++ b/docs/WEB_SERVER_PLAN.md @@ -1,6 +1,6 @@ # Claro Web Server Plan -Current release: **Claro v2026.10.20.223203** +Current release: **Claro v2026.10.25.014405** Claro currently has beginner HTTP client commands and offline `claro://` test URLs. A web server API is not complete yet. diff --git a/src/claro.c b/src/claro.c index 533da52..2fafda7 100644 --- a/src/claro.c +++ b/src/claro.c @@ -1,5 +1,5 @@ /* - Claro v2026.10.20.223203 - Networking Reliability Release + Claro v2026.10.25.014405 - Networking Reliability Release A compact, buildable Claro interpreter focused on the beginner-friendly teaching core: readable commands, simple expressions, functions, collections, @@ -45,7 +45,7 @@ int pclose(FILE *stream); #define F_OK 0 #endif -#define CLARO_VERSION "Claro v2026.10.20.223203" +#define CLARO_VERSION "Claro v2026.10.25.014405" #define CLARO_MAX_CALL_DEPTH 256 #define CLARO_MAX_EXPR_DEPTH 128 #define CLARO_MAX_BLOCK_DEPTH 128 @@ -574,6 +574,9 @@ typedef struct Runtime { char *err_msg; char *err_file; int err_line; + const char *active_file; /* borrowed while a source line executes */ + int active_line; + unsigned long steps, step_limit, read_file_limit; int returning; Value ret; int call_depth; @@ -585,10 +588,42 @@ typedef struct Runtime { int script_argc; char **script_argv; } Runtime; +static void rt_error(Runtime *rt, const char *file, int line, const char *fmt, ...); +static unsigned long rt_env_limit(Runtime *rt, const char *name) { + const char *text = getenv(name); + unsigned long value = 0; + if (!text || !*text) return 0; + while (*text) { + unsigned long digit; + if (*text < '0' || *text > '9') { + rt_error(rt, "", 1, "%s needs a non-negative whole number.", name); + return 0; + } + digit = (unsigned long)(*text++ - '0'); + if (value > (ULONG_MAX - digit) / 10) { + rt_error(rt, "", 1, "%s is too large for this build.", name); + return 0; + } + value = value * 10 + digit; + } + return value; +} +static int rt_tick(Runtime *rt) { + if (rt->error) return 0; + if (!rt->step_limit) return 1; + if (rt->steps >= rt->step_limit) { + rt_error(rt, "", 0, "Execution reached its step limit. Check the loop or raise CLARO_STEP_LIMIT."); + return 0; + } + rt->steps++; + return 1; +} static void rt_init(Runtime *rt) { memset(rt, 0, sizeof(*rt)); str_init(&rt->captured); rt->ret = v_none(); + rt->step_limit = rt_env_limit(rt, "CLARO_STEP_LIMIT"); + rt->read_file_limit = rt_env_limit(rt, "CLARO_READ_FILE_LIMIT"); srand(1); } static Var *env_find(Var *v, const char *name) { @@ -825,6 +860,10 @@ static void rt_error(Runtime *rt, const char *file, int line, const char *fmt, . va_start(ap, fmt); claro_vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); + if (!file || !*file) + file = rt->active_file; + if (line <= 0) + line = rt->active_line; nfile = normalize_path_copy(file ? file : ""); rt->error = 1; rt->err_msg = xstrdup(buf); @@ -1877,134 +1916,195 @@ static int find_done(Program *p, int start) { /* ---------- JSON ---------- */ typedef struct { const char *s; + int depth; + const char *error; } JsonP; +#define CLARO_MAX_JSON_DEPTH 128 static void jp_ws(JsonP *j) { while (*j->s && isspace((unsigned char)*j->s)) j->s++; } +static unsigned long jp_hex4(JsonP *j) { + unsigned long value = 0; + int i, digit; + for (i = 0; i < 4; i++) { + char c = *j->s; + if (c >= '0' && c <= '9') digit = c - '0'; + else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10; + else { j->error = "JSON Unicode escapes need four hexadecimal digits."; return 0; } + value = value * 16 + (unsigned long)digit; + j->s++; + } + return value; +} +static void jp_utf8(JsonP *j, Str *b, unsigned long code) { + unsigned long low; + if (code >= 0xd800UL && code <= 0xdbffUL) { + if (j->s[0] != '\\' || j->s[1] != 'u') { + j->error = "JSON Unicode needs a matching low surrogate."; + return; + } + j->s += 2; + low = jp_hex4(j); + if (j->error) return; + if (low < 0xdc00UL || low > 0xdfffUL) { + j->error = "JSON Unicode needs a matching low surrogate."; + return; + } + code = 0x10000UL + (code - 0xd800UL) * 1024 + low - 0xdc00UL; + } else if (code >= 0xdc00UL && code <= 0xdfffUL) { + j->error = "JSON Unicode has an unmatched low surrogate."; + return; + } + if (!code) { j->error = "JSON text cannot contain a zero character."; return; } + if (code < 0x80UL) str_ch(b, (char)code); + else if (code < 0x800UL) { + str_ch(b, (char)(0xc0UL | (code >> 6))); + str_ch(b, (char)(0x80UL | (code & 63))); + } else if (code < 0x10000UL) { + str_ch(b, (char)(0xe0UL | (code >> 12))); + str_ch(b, (char)(0x80UL | ((code >> 6) & 63))); + str_ch(b, (char)(0x80UL | (code & 63))); + } else { + str_ch(b, (char)(0xf0UL | (code >> 18))); + str_ch(b, (char)(0x80UL | ((code >> 12) & 63))); + str_ch(b, (char)(0x80UL | ((code >> 6) & 63))); + str_ch(b, (char)(0x80UL | (code & 63))); + } +} static char *jp_string(JsonP *j) { Str b; + char escaped; str_init(&b); jp_ws(j); - if (*j->s != '"') - return xstrdup(""); + if (*j->s != '"') { j->error = "JSON object keys need double quotes."; return xstrdup(""); } j->s++; - while (*j->s && *j->s != '"') { + while (*j->s && *j->s != '"' && !j->error) { if (*j->s == '\\') { j->s++; - if (*j->s == 'n') - str_ch(&b, '\n'); - else if (*j->s == 't') - str_ch(&b, '\t'); - else if (*j->s == 'r') - str_ch(&b, '\r'); - else if (*j->s) - str_ch(&b, *j->s); - if (*j->s) - j->s++; - } else - str_ch(&b, *j->s++); + if (!*j->s) { j->error = "JSON text is missing a closing quote."; break; } + escaped = *j->s++; + if (escaped == 'n') str_ch(&b, '\n'); + else if (escaped == 't') str_ch(&b, '\t'); + else if (escaped == 'r') str_ch(&b, '\r'); + else if (escaped == 'b') str_ch(&b, '\b'); + else if (escaped == 'f') str_ch(&b, '\f'); + else if (escaped == 'u') { + unsigned long code = jp_hex4(j); + if (!j->error) jp_utf8(j, &b, code); + } else if (escaped == '"' || escaped == '\\' || escaped == '/') str_ch(&b, escaped); + else j->error = "JSON has an invalid text escape."; + } else if ((unsigned char)*j->s < 32) j->error = "JSON text contains an unescaped control character."; + else str_ch(&b, *j->s++); } - if (*j->s == '"') - j->s++; + if (*j->s == '"') j->s++; + else if (!j->error) j->error = "JSON text is missing a closing quote."; return str_take(&b); } static Value jp_value(JsonP *j); static Value jp_array(JsonP *j) { Value arr = v_list(); + j->s++; jp_ws(j); - if (*j->s == '[') - j->s++; - jp_ws(j); - if (*j->s == ']') { - j->s++; - return arr; - } - while (*j->s) { + if (*j->s == ']') { j->s++; return arr; } + while (!j->error) { Value v = jp_value(j); - list_add(arr.list, v); + if (!j->error) list_add(arr.list, v); + value_free(v); + if (j->error) break; jp_ws(j); - if (*j->s == ',') { - j->s++; - continue; - } - if (*j->s == ']') { - j->s++; - break; - } + if (*j->s == ']') { j->s++; break; } + if (*j->s != ',') { j->error = "JSON list needs a comma or closing bracket."; break; } + j->s++; } return arr; } static Value jp_object(JsonP *j) { Value obj = v_map(); + j->s++; jp_ws(j); - if (*j->s == '{') - j->s++; - jp_ws(j); - if (*j->s == '}') { - j->s++; - return obj; - } - while (*j->s) { - char *k = jp_string(j); - Value v; + if (*j->s == '}') { j->s++; return obj; } + while (!j->error) { + char *key = jp_string(j); + Value v = v_none(); jp_ws(j); - if (*j->s == ':') - j->s++; - v = jp_value(j); - map_put(obj.map, k, v); - free(k); + if (!j->error && *j->s != ':') j->error = "JSON object needs a colon after its key."; + if (!j->error) { j->s++; v = jp_value(j); } + if (!j->error) map_put(obj.map, key, v); + value_free(v); + free(key); + if (j->error) break; jp_ws(j); - if (*j->s == ',') { - j->s++; - continue; - } - if (*j->s == '}') { - j->s++; - break; - } + if (*j->s == '}') { j->s++; break; } + if (*j->s != ',') { j->error = "JSON object needs a comma or closing brace."; break; } + j->s++; } return obj; } static Value jp_value(JsonP *j) { + const char *scan; char *end; + double number; + Value result; jp_ws(j); if (*j->s == '"') { - char *s = jp_string(j); - Value v = v_str(s); - free(s); - return v; + char *text = jp_string(j); + result = v_str(text); + free(text); + return result; } - if (*j->s == '{') - return jp_object(j); - if (*j->s == '[') - return jp_array(j); - if (!strncmp(j->s, "true", 4)) { - j->s += 4; - return v_bool(1); + if (*j->s == '[' || *j->s == '{') { + if (j->depth >= CLARO_MAX_JSON_DEPTH) { + j->error = "JSON nesting exceeds the safe limit of 128. Simplify the data."; + return v_none(); + } + j->depth++; + result = *j->s == '[' ? jp_array(j) : jp_object(j); + j->depth--; + return result; } - if (!strncmp(j->s, "false", 5)) { - j->s += 5; - return v_bool(0); + if (!strncmp(j->s, "true", 4)) { j->s += 4; return v_bool(1); } + if (!strncmp(j->s, "false", 5)) { j->s += 5; return v_bool(0); } + if (!strncmp(j->s, "null", 4)) { j->s += 4; return v_none(); } + scan = j->s; + if (*scan == '-') scan++; + if (*scan == '0') scan++; + else if (*scan >= '1' && *scan <= '9') while (isdigit((unsigned char)*scan)) scan++; + else { j->error = "JSON needs a value here."; return v_none(); } + if (*scan == '.') { + scan++; + if (!isdigit((unsigned char)*scan)) { j->error = "JSON number needs digits after its decimal point."; return v_none(); } + while (isdigit((unsigned char)*scan)) scan++; } - if (!strncmp(j->s, "null", 4)) { - j->s += 4; + if (*scan == 'e' || *scan == 'E') { + scan++; + if (*scan == '+' || *scan == '-') scan++; + if (!isdigit((unsigned char)*scan)) { j->error = "JSON number needs exponent digits."; return v_none(); } + while (isdigit((unsigned char)*scan)) scan++; + } + number = strtod(j->s, &end); + if (end != scan || number > DBL_MAX || number < -DBL_MAX) { + j->error = "JSON number is outside the supported range."; return v_none(); } - end = NULL; - { - double d = strtod(j->s, &end); - if (end && end != j->s) { - j->s = end; - return v_num(d); - } - } - return v_none(); + j->s = scan; + return v_num(number); } -static Value parse_json_text(const char *s) { +static Value parse_json_text(Runtime *rt, const char *text) { JsonP j; - j.s = s; - return jp_value(&j); + Value result; + memset(&j, 0, sizeof(j)); + j.s = text; + result = jp_value(&j); + jp_ws(&j); + if (!j.error && *j.s) j.error = "JSON has unexpected text after its value."; + if (j.error) { + value_free(result); + rt_error(rt, "", 0, "%s", j.error); + return v_none(); + } + return result; } static void json_escape(Str *b, const char *s) { str_ch(b, '"'); @@ -2015,6 +2115,12 @@ static void json_escape(Str *b, const char *s) { str_add(b, "\\\\"); } else if (*s == '\n') { str_add(b, "\\n"); + } else if ((unsigned char)*s < 32) { + static const char hex[] = "0123456789abcdef"; + unsigned char c = (unsigned char)*s; + str_add(b, "\\u00"); + str_ch(b, hex[c >> 4]); + str_ch(b, hex[c & 15]); } else str_ch(b, *s); s++; @@ -2861,6 +2967,9 @@ static void do_import(Runtime *rt, const char *path, const char *ns, const char exec_range(rt, p, 0, p->count); } rt->import_depth--; + free(rt->import_stack[rt->import_depth]); + rt->import_stack[rt->import_depth] = NULL; + split_args_free(exports, ec); free(prefix); } static void create_object_value(Runtime *rt, const char *file, int line, const char *cls, @@ -2889,7 +2998,7 @@ static void create_object_value(Runtime *rt, const char *file, int line, const c } } } -static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { +static void exec_line_body(Runtime *rt, Program *p, int *pcp, const char *raw) { char buf[4096], up[64], w[128]; char *t; int pc = *pcp; @@ -3461,12 +3570,25 @@ static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { } if (strcmp(up, "REPEAT") == 0) { int end = match_block(p, pc, "REPEAT", "UNTIL", NULL, NULL); - char *cond = p->lines[end] + 5; + char *cond; + int again = 0; + if (end < 0) { + rt_error(rt, p->path, pc + 1, "REPEAT needs UNTIL and a stopping condition."); + return; + } + cond = p->lines[end] + 5; do { exec_range(rt, p, pc + 1, end); if (rt->error || rt->returning) break; - } while (!v_truth(eval_expr(rt, cond))); + rt->active_file = p->path; + rt->active_line = end + 1; + { + Value condition = eval_expr(rt, cond); + again = !v_truth(condition); + value_free(condition); + } + } while (again && !rt->error); *pcp = end < 0 ? pc : end; return; } @@ -3717,12 +3839,21 @@ static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { Str b; int c; Value contents; + unsigned long bytes = 0; str_init(&b); - while ((c = fgetc(f)) != EOF) + while ((c = fgetc(f)) != EOF) { + if (rt->read_file_limit && bytes >= rt->read_file_limit) { + rt_error(rt, p->path, pc + 1, + "READ FILE exceeded the configured file limit. Raise CLARO_READ_FILE_LIMIT if needed."); + break; + } str_ch(&b, (char)c); + if (rt->read_file_limit) bytes++; + } + if (ferror(f)) rt_error(rt, p->path, pc + 1, "Could not finish reading the file."); fclose(f); contents = v_str(b.s ? b.s : ""); - rt_set(rt, var, contents); + if (!rt->error) rt_set(rt, var, contents); value_free(contents); free(b.s); } @@ -3844,7 +3975,10 @@ static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { char *var = xstrdup(trim_inplace((char *)as + 2)); Value s = eval_expr(rt, ex); char *txt = v_to_string(s); - rt_set(rt, var, parse_json_text(txt)); + Value parsed = parse_json_text(rt, txt); + if (!rt->error) rt_set(rt, var, parsed); + value_free(parsed); + value_free(s); free(txt); free(ex); free(var); @@ -3860,7 +3994,10 @@ static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { char *var = xstrdup(trim_inplace((char *)as + 2)); Value v = eval_expr(rt, ex); char *js = make_json(v, pretty); - rt_set(rt, var, v_str(js)); + Value encoded = v_str(js); + rt_set(rt, var, encoded); + value_free(encoded); + value_free(v); free(js); free(ex); free(var); @@ -4136,6 +4273,16 @@ static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { } rt_error(rt, p->path, pc + 1, "Unknown command: %s", w); } +static void exec_line(Runtime *rt, Program *p, int *pcp, const char *raw) { + const char *saved_file = rt->active_file; + int saved_line = rt->active_line; + rt->active_file = p->path; + rt->active_line = *pcp + 1; + if (rt_tick(rt)) + exec_line_body(rt, p, pcp, raw); + rt->active_file = saved_file; + rt->active_line = saved_line; +} static void exec_range(Runtime *rt, Program *p, int start, int end) { int pc; /* Function calls have their own guard; count additional control-block frames. */ @@ -4144,6 +4291,7 @@ static void exec_range(Runtime *rt, Program *p, int start, int end) { "Block nesting exceeds the safe limit of 128. Simplify the nested blocks."); return; } + if (!rt_tick(rt)) return; rt->range_depth++; if (end < 0 || end > p->count) end = p->count; @@ -6630,12 +6778,12 @@ static int create_new_project(const char *name) { claro_snprintf(path, sizeof(path), "%s/claro.project", name); claro_snprintf( text, sizeof(text), - "manifest-version: 1\nname: %s\nmain: main.claro\nversion: v2026.10.20.223203\npackages:\n", + "manifest-version: 1\nname: %s\nmain: main.claro\nversion: v2026.10.25.014405\npackages:\n", name); write_text_file_simple(path, text); claro_snprintf(path, sizeof(path), "%s/claro.lock", name); write_text_file_simple(path, - "# Claro package lock\nlock-version: 1\nversion: v2026.10.20.223203\n"); + "# Claro package lock\nlock-version: 1\nversion: v2026.10.25.014405\n"); claro_snprintf(path, sizeof(path), "%s/README.md", name); write_text_file_simple( path, "# My Claro Project\n\nBuild Claro, then run this project with:\n\n```bash\nclaro " @@ -6732,7 +6880,7 @@ static void ensure_project_file(void) { if (!file_exists_simple("claro.project")) write_text_file_simple("claro.project", "manifest-version: 1\nname: ClaroProject\nmain: " - "main.claro\nversion: v2026.10.20.223203\npackages:\n"); + "main.claro\nversion: v2026.10.25.014405\npackages:\n"); } static char *project_value(const char *key) { char *txt = read_file_text("claro.project"); @@ -6858,7 +7006,7 @@ static int write_package_lock(void) { free(txt); return 0; } - fputs("# Claro package lock\nlock-version: 1\nversion: v2026.10.20.223203\n", f); + fputs("# Claro package lock\nlock-version: 1\nversion: v2026.10.25.014405\n", f); p = txt; while (*p) { char *line = p; @@ -7341,7 +7489,7 @@ static int package_doctor(void) { } if (txt && !package_lock_release_version_matches(txt)) { printf( - " BAD lock release version: expected exactly one version: v2026.10.20.223203 line\n"); + " BAD lock release version: expected exactly one version: v2026.10.25.014405 line\n"); ok = 0; } free(txt); @@ -7505,7 +7653,7 @@ static int run_package_cmd(int argc, char **argv, int arg) { static int print_ide_info(void) { printf("{\n"); printf(" \"language\": \"Claro\",\n"); - printf(" \"version\": \"v2026.10.20.223203\",\n"); + printf(" \"version\": \"v2026.10.25.014405\",\n"); printf(" \"file_extensions\": [\".claro\"],\n"); printf(" \"keywords\": " "[\"SAY\",\"ASK\",\"SET\",\"IF\",\"ELSE\",\"ENDIF\",\"END\",\"REPEAT\",\"UNTIL\"," @@ -7855,7 +8003,7 @@ static int run_validate(void) { printf( "%s\n", fails ? "Validation found problems." - : "Validation passed. Claro v2026.10.20.223203 foundation checks are ready for use."); + : "Validation passed. Claro v2026.10.25.014405 foundation checks are ready for use."); return fails ? 1 : 0; } diff --git a/test98.bat b/test98.bat new file mode 100644 index 0000000..710cc87 --- /dev/null +++ b/test98.bat @@ -0,0 +1,21 @@ +@echo off +rem Run only in a fresh extracted Claro test folder. +if exist native.ok del native.ok +if not exist osprobe.exe goto wrongos +osprobe.exe --require-win98se >oscheck.txt +if errorlevel 1 goto wrongos +claro98.exe --version >nver.txt +if errorlevel 1 goto failed +claro98.exe test >ntests.txt +if errorlevel 1 goto failed +claro98.exe validate >ncheck.txt +if errorlevel 1 goto failed +echo Automated native checks passed. Manual console and file checks are still required. +echo PASS>native.ok +goto end +:wrongos +echo Native target not verified. Check oscheck.txt and the probe message. +goto end +:failed +echo Claro checks failed. Keep the logs; do not report acceptance. +:end diff --git a/tests/error_examples.json b/tests/error_examples.json new file mode 100644 index 0000000..34ef047 --- /dev/null +++ b/tests/error_examples.json @@ -0,0 +1,56 @@ +[ + { + "name": "quote", + "filename": "quote.claro", + "mode": "run", + "code": "SAY \"Start\"\nSAY \"unfinished\n", + "diagnostic": "quote.claro:2: Text is missing a closing quote. Add a double quote at the end.", + "repair": "SAY \"Start\"\nSAY \"finished\"\n", + "output": "Start\nfinished\n" + }, + { + "name": "operand", + "filename": "operand.claro", + "mode": "run", + "code": "SET score 1\nSAY score +\n", + "diagnostic": "operand.claro:2: This expression needs a value after the operator.", + "repair": "SET score 1\nSAY score + 1\n", + "output": "2\n" + }, + { + "name": "command", + "filename": "command.claro", + "mode": "check", + "code": "PRNT \"Hello\"\n", + "diagnostic": "command.claro:1: I do not know the command \"PRNT\". Did you mean \"SAY\"?", + "repair": "SAY \"Hello\"\n", + "output": "Hello\n" + }, + { + "name": "assignment", + "filename": "assignment.claro", + "mode": "check", + "code": "SET name\n", + "diagnostic": "assignment.claro:1: SET needs a name and a value. Try: SET name \"Alex\"", + "repair": "SET name \"Alex\"\nSAY name\n", + "output": "Alex\n" + }, + { + "name": "choice", + "filename": "choice.claro", + "mode": "check", + "code": "IF YES\n SAY \"Yes\"\n", + "diagnostic": "choice.claro:1: IF block opened here needs END", + "repair": "IF YES\n SAY \"Yes\"\nEND\n", + "output": "Yes\n" + }, + { + "name": "variable", + "filename": "variable.claro", + "mode": "run", + "code": "SET score 10\nSAY socre\n", + "diagnostic": "variable.claro:2: I do not know the variable \"socre\" yet.", + "repair": "SET score 10\nSAY score\n", + "output": "10\n" + } +] diff --git a/tests/learner_pilot.json b/tests/learner_pilot.json new file mode 100644 index 0000000..246a779 --- /dev/null +++ b/tests/learner_pilot.json @@ -0,0 +1,44 @@ +[ + { + "name": "Say hello", + "code": "SAY \"Hello!\"\n", + "input": "", + "output": "Hello!\n", + "task": "Run this once. Change the message to something you choose and run it again." + }, + { + "name": "Ask a name", + "code": "ASK \"Name? \" name\nSAY \"Hello \" + name\n", + "input": "Sam\n", + "output": "Name? \nHello Sam\n", + "task": "Run it and type a pretend name. Explain where that name is remembered." + }, + { + "name": "Change a number", + "code": "SET score 2\nSAY score + 1\n", + "input": "", + "output": "3\n", + "task": "Predict the result, run it, then change 2 to another small number." + }, + { + "name": "Make a choice", + "code": "SET score 2\nIF score > 1\n SAY \"More than one\"\nELSE\n SAY \"One or less\"\nEND\n", + "input": "", + "output": "More than one\n", + "task": "Optional: change score to 0. Predict which message appears." + }, + { + "name": "Repeat", + "code": "DO 3 TIMES\n SAY \"Again\"\nEND\n", + "input": "", + "output": "Again\nAgain\nAgain\n", + "task": "Optional: change how many times the message appears. Ask how to stop a stuck run." + }, + { + "name": "Save a file", + "code": "WRITE FILE \"practice.txt\" WITH \"My first file\"\nREAD FILE \"practice.txt\" AS text\nSAY text\n", + "input": "", + "output": "My first file\n", + "task": "Optional: use a new writable practice folder. Find the file after running it." + } +] diff --git a/testwin.bat b/testwin.bat new file mode 100644 index 0000000..ae6c374 --- /dev/null +++ b/testwin.bat @@ -0,0 +1,21 @@ +@echo off +rem Run only in a fresh extracted Claro test folder. +if exist native.ok del native.ok +if not exist osprobe.exe goto wrongos +osprobe.exe --require-native >oscheck.txt +if errorlevel 1 goto wrongos +claro.exe --version >nver.txt +if errorlevel 1 goto failed +claro.exe test >ntests.txt +if errorlevel 1 goto failed +claro.exe validate >ncheck.txt +if errorlevel 1 goto failed +echo Automated native checks passed. Manual console and file checks are still required. +echo PASS>native.ok +goto end +:wrongos +echo Native target not verified. Check oscheck.txt and the probe message. +goto end +:failed +echo Claro checks failed. Keep the logs; do not report acceptance. +:end diff --git a/tools/build_distribution.py b/tools/build_distribution.py index bbeeb35..bea1ec7 100644 --- a/tools/build_distribution.py +++ b/tools/build_distribution.py @@ -14,7 +14,7 @@ import zipfile ROOT = Path(__file__).resolve().parents[1] DIRECTORIES = ['.forgejo', 'src', 'docs', 'lessons', 'examples', 'lib', 'tests', 'tools', 'assets', 'support'] -FILES = ['README.md', 'LICENSE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'Makefile', 'build.bat', 'build.ps1', 'build98.bat', 'build98.rsp', 'run_tests.bat'] +FILES = ['README.md', 'LICENSE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'Makefile', 'build.bat', 'build.ps1', 'build98.bat', 'build98.rsp', 'test98.bat', 'testwin.bat', 'run_tests.bat'] FLAGS = ['-std=c89', '-pedantic-errors', '-Werror=implicit-function-declaration', '-O2', '-static'] diff --git a/tools/native_platform_probe.c b/tools/native_platform_probe.c new file mode 100644 index 0000000..87310d5 --- /dev/null +++ b/tools/native_platform_probe.c @@ -0,0 +1,35 @@ +/* Native-OS evidence helper, not part of the Claro interpreter. */ +#include +#include +#include +int main(int argc, char **argv) { + OSVERSIONINFOA info; + HMODULE module; + int wine, want98, match98; + memset(&info, 0, sizeof(info)); + info.dwOSVersionInfoSize = sizeof(info); + if (!GetVersionExA(&info)) { + fprintf(stderr, "Could not identify the reported Windows API version.\n"); + return 3; + } + module = GetModuleHandleA("ntdll.dll"); + wine = module && GetProcAddress(module, "wine_get_version") != NULL; + want98 = argc > 1 && strcmp(argv[1], "--require-win98se") == 0; + match98 = info.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS && + info.dwMajorVersion == 4 && info.dwMinorVersion == 10 && + (info.dwBuildNumber & 0xffffUL) == 2222; + printf("{\"wine\":%s,\"reported_api_major\":%lu,\"reported_api_minor\":%lu,\"reported_build\":%lu,\"platform_id\":%lu,\"matches_win98se\":%s}\n", + wine ? "true" : "false", (unsigned long)info.dwMajorVersion, + (unsigned long)info.dwMinorVersion, + (unsigned long)(info.dwBuildNumber & 0xffffUL), + (unsigned long)info.dwPlatformId, match98 ? "true" : "false"); + if (wine) { + fprintf(stderr, "Wine detected: this is NOT native Windows acceptance.\n"); + return 2; + } + if (want98 && !match98) { + fprintf(stderr, "This is not the required Windows 98 SE 4.10.2222 target.\n"); + return 2; + } + return 0; +} diff --git a/tools/validate_ci_workflow.py b/tools/validate_ci_workflow.py index e0d33a0..b881e6f 100644 --- a/tools/validate_ci_workflow.py +++ b/tools/validate_ci_workflow.py @@ -17,6 +17,10 @@ REQUIRED_COMMANDS = [ "python3 tools/validate_c89.py", "python3 tools/validate_msvc6_source.py", "python3 tools/validate_documentation_status.py", + "python3 tools/validate_error_locations.py", + "python3 tools/validate_error_guide.py", + "python3 tools/validate_learner_pilot.py", + "python3 tools/validate_resource_guards.py", "python3 tools/validate_ci_workflow.py", ] diff --git a/tools/validate_documentation_status.py b/tools/validate_documentation_status.py index 98c0285..381c7cc 100644 --- a/tools/validate_documentation_status.py +++ b/tools/validate_documentation_status.py @@ -11,6 +11,9 @@ ACTIVE = ( 'docs/RELEASE_STATUS.md', 'docs/FINAL_RELEASE_CHECKLIST.md', 'docs/CLASSROOM_READINESS.md', 'docs/TESTING.md', 'docs/HARDENING.md', 'docs/PORTABILITY.md', 'docs/RELIABILITY.md', + 'docs/ERRORS.md', 'docs/RUNTIME_LIMITS.md', 'docs/LEARNER_PILOT.md', + 'docs/LEARNER_FEEDBACK_TEMPLATE.md', 'docs/NATIVE_WINDOWS_TESTING.md', + 'docs/SIX_TRACK_STATUS.md', 'docs/history/CURRENT_STATUS_BEFORE_STATUS_CLEANUP.md', 'docs/history/HARDENING_BEFORE_STATUS_CLEANUP.md', ) diff --git a/tools/validate_error_guide.py b/tools/validate_error_guide.py new file mode 100644 index 0000000..8be2a8a --- /dev/null +++ b/tools/validate_error_guide.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Execute every error-guide failure and repair against the actual interpreter.""" +from pathlib import Path +import json, os, re, shlex, subprocess, tempfile +ROOT = Path(__file__).resolve().parents[1] +def main(): + cases = json.loads((ROOT/'tests/error_examples.json').read_text()) + guide = (ROOT/'docs/ERRORS.md').read_text() + blocks = re.findall(r'```claro\n(.*?)```',guide,re.S) + expected = [c[k] for c in cases for k in ['code','repair']] + assert blocks == expected, 'Every error and repair block must match executable coverage' + with tempfile.TemporaryDirectory(prefix='claro-error-guide-',dir=os.environ.get('TMPDIR')) as tmp: + work=Path(tmp); override=os.environ.get('CLARO_EXE'); exe=Path(override).resolve() if override else work/'claro' + if not override: + subprocess.run(shlex.split(os.environ.get('CLARO_CC','gcc'))+['-std=c89','-pedantic-errors',str(ROOT/'src/claro.c'),'-o',str(exe),'-lm'],check=True) + for case in cases: + path=work/case['filename'];path.write_text(case['code']) + cmd=[str(exe)]+(['check'] if case['mode']=='check' else [])+[path.name] + r=subprocess.run(cmd,cwd=work,capture_output=True,text=True,timeout=10) + assert r.returncode==1 and case['diagnostic'] in r.stdout+r.stderr,(case['name'],r.stdout,r.stderr) + assert case['diagnostic'] in guide + path.write_text(case['repair']);r=subprocess.run([str(exe),path.name],cwd=work,capture_output=True,text=True,timeout=10) + assert r.returncode==0 and r.stdout==case['output'],(case['name'],r.stdout,r.stderr) + print('PASS error guide:',case['name']) +if __name__=='__main__':main() diff --git a/tools/validate_error_locations.py b/tools/validate_error_locations.py new file mode 100644 index 0000000..8131a67 --- /dev/null +++ b/tools/validate_error_locations.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Verify real runtime diagnostic origins, including functions/imports/TRY.""" +from pathlib import Path +import os +import shlex +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + with tempfile.TemporaryDirectory(prefix='claro-error-locations-', dir=os.environ.get('TMPDIR')) as temp: + work = Path(temp) + override = os.environ.get('CLARO_EXE') + exe = Path(override).resolve() if override else work / 'claro' + if not override: + compiler = shlex.split(os.environ.get('CLARO_CC', 'gcc')) + subprocess.run(compiler + ['-std=c89', '-pedantic-errors', str(ROOT / 'src/claro.c'), '-o', str(exe), '-lm'], check=True) + cases = [ + ('quote.claro', 'SAY "Start"\nSAY "unfinished\n', 2, 'Text is missing a closing quote.'), + ('operand.claro', 'SET score 1\nSAY score +\n', 2, 'needs a value after the operator'), + ('parenthesis.claro', 'SAY "Start"\nSAY (1 + 2\n', 2, 'needs a closing parenthesis'), + ('function.claro', 'TEACH broken\n SAY 1 +\nEND\nDO broken\n', 2, 'needs a value after the operator'), + ('repeat.claro', 'REPEAT\n SAY "Once"\nUNTIL (\n', 3, 'needs a value after the operator'), + ] + for name, text, line, message in cases: + (work / name).write_text(text) + result = subprocess.run([str(exe), name], cwd=work, capture_output=True, text=True, timeout=10) + output = result.stdout + result.stderr + assert not any(t in output for t in ["AddressSanitizer", "LeakSanitizer", "runtime error:"]), output + assert result.returncode == 1, (name, result.returncode, output) + assert f'{name}:{line}:' in output and ':0:' not in output and message in output, (name, output) + print('PASS origin:', name, line) + (work / 'broken-module.claro').write_text('TEACH broken\n SAY 1 +\nEND\n') + (work / 'caller.claro').write_text('IMPORT "broken-module.claro" AS problem\nDO problem.broken\n') + result = subprocess.run([str(exe), 'caller.claro'], cwd=work, capture_output=True, text=True, timeout=10) + assert result.returncode == 1 and 'broken-module.claro:2:' in result.stderr, result.stderr + print('PASS origin: imported function') + (work / 'caught.claro').write_text('TRY\n DO problem.broken\nCATCH\n SAY LASTERRORFILE\n SAY LASTERRORLINE\nENDTRY\n') + (work / 'caller.claro').write_text('IMPORT "broken-module.claro" AS problem\nTRY\n DO problem.broken\nCATCH\n SAY LASTERRORFILE\n SAY LASTERRORLINE\nENDTRY\n') + result = subprocess.run([str(exe), 'caller.claro'], cwd=work, capture_output=True, text=True, timeout=10) + assert result.returncode == 0 and result.stdout.replace('\\', '/').endswith('broken-module.claro\n2\n'), (result.stdout, result.stderr) + print('PASS origin: caught error metadata') + + +if __name__ == '__main__': + main() diff --git a/tools/validate_learner_pilot.py b/tools/validate_learner_pilot.py new file mode 100644 index 0000000..29da08a --- /dev/null +++ b/tools/validate_learner_pilot.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Execute the pilot's six starter tasks, not simulated human observations.""" +from pathlib import Path +import json,os,re,shlex,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[1] +def main(): + tasks=json.loads((ROOT/'tests/learner_pilot.json').read_text()) + blocks=re.findall(r'```claro\n(.*?)```',(ROOT/'docs/LEARNER_PILOT.md').read_text(),re.S) + assert blocks==[t['code'] for t in tasks] + with tempfile.TemporaryDirectory(prefix='claro-pilot-',dir=os.environ.get('TMPDIR')) as tmp: + work=Path(tmp);override=os.environ.get('CLARO_EXE');exe=Path(override).resolve() if override else work/'claro' + if not override:subprocess.run(shlex.split(os.environ.get('CLARO_CC','gcc'))+['-std=c89','-pedantic-errors',str(ROOT/'src/claro.c'),'-o',str(exe),'-lm'],check=True) + for i,t in enumerate(tasks): + p=work/f'task{i}.claro';p.write_text(t['code']);r=subprocess.run([str(exe),p.name],cwd=work,input=t['input'],capture_output=True,text=True,timeout=10) + assert r.returncode==0 and r.stdout==t['output'],(t['name'],r.stdout,r.stderr) + print('PASS pilot code:',t['name']) + assert (work/'practice.txt').read_text()=='My first file' + print('Human usability/accessibility: NOT TESTED') +if __name__=='__main__':main() diff --git a/tools/validate_learning_ownership.py b/tools/validate_learning_ownership.py index e44f479..8d22889 100644 --- a/tools/validate_learning_ownership.py +++ b/tools/validate_learning_ownership.py @@ -17,7 +17,7 @@ def main(): binary = Path(tmp) / 'claro-asan' subprocess.run(['gcc', '-std=c89', '-pedantic-errors', '-Werror=implicit-function-declaration', '-O0', '-g', '-fsanitize=address,undefined', str(ROOT / 'src/claro.c'), '-o', str(binary), '-lm'], check=True) env = dict(os.environ, ASAN_OPTIONS='detect_leaks=1:halt_on_error=1', UBSAN_OPTIONS='halt_on_error=1', CLARO_EXE=str(binary)) - for checker in ['validate_learning_syntax.py', 'validate_first_hour.py']: + for checker in ['validate_learning_syntax.py', 'validate_first_hour.py', 'validate_error_locations.py', 'validate_error_guide.py', 'validate_learner_pilot.py', 'validate_resource_guards.py']: result = subprocess.run([sys.executable, str(ROOT / 'tools' / checker)], cwd=ROOT, env=env, capture_output=True, text=True, timeout=90) if result.returncode: print(result.stdout + result.stderr) diff --git a/tools/validate_package_security.py b/tools/validate_package_security.py index 48c886d..3526fce 100644 --- a/tools/validate_package_security.py +++ b/tools/validate_package_security.py @@ -6,7 +6,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] EXE = ROOT / ("claro.exe" if os.name == "nt" else "claro") -EXPECTED_VERSION = "Claro v2026.10.20.223203" +EXPECTED_VERSION = "Claro v2026.10.25.014405" def fail(message): @@ -57,11 +57,11 @@ def main(): fail(out) starter = work / "StarterApp" starter_project = read(starter / "claro.project") - for phrase in ["manifest-version: 1", "name: StarterApp", "main: main.claro", "version: v2026.10.20.223203", "packages:"]: + for phrase in ["manifest-version: 1", "name: StarterApp", "main: main.claro", "version: v2026.10.25.014405", "packages:"]: if phrase not in starter_project: fail(f"claro new project file missing {phrase!r}:\n{starter_project}") starter_lock = read(starter / "claro.lock") - for phrase in ["lock-version: 1", "version: v2026.10.20.223203"]: + for phrase in ["lock-version: 1", "version: v2026.10.25.014405"]: if phrase not in starter_lock: fail(f"claro new lockfile missing {phrase!r}:\n{starter_lock}") @@ -71,7 +71,7 @@ def main(): if rc != 0: fail(out) project = read(work / "claro.project") - for phrase in ["manifest-version: 1", "name: ClaroProject", "main: main.claro", "version: v2026.10.20.223203", "packages:"]: + for phrase in ["manifest-version: 1", "name: ClaroProject", "main: main.claro", "version: v2026.10.25.014405", "packages:"]: if phrase not in project: fail(f"claro.project missing {phrase!r}:\n{project}") @@ -100,7 +100,7 @@ def main(): fail(f"Package checksum should be a stable non-placeholder value, got {checksum!r}") lock = read(work / "claro.lock") - for phrase in ["lock-version: 1", "version: v2026.10.20.223203", "package: math-tools", "checksum:"]: + for phrase in ["lock-version: 1", "version: v2026.10.25.014405", "package: math-tools", "checksum:"]: if phrase not in lock: fail(f"claro.lock missing {phrase!r}:\n{lock}") @@ -127,12 +127,12 @@ def main(): fail(f"Lock format-version diagnostic was unclear:\n{out}") if read(work / "claro.lock") != invalid_lock: fail("package doctor must not rewrite an invalid lock format version") - invalid_release_lock = lock.replace("version: v2026.10.20.223203", "version: v1.18.25", 1) + invalid_release_lock = lock.replace("version: v2026.10.25.014405", "version: v1.18.25", 1) (work / "claro.lock").write_text(invalid_release_lock, encoding="utf-8") rc, out = run([str(EXE), "package", "doctor"], work) if rc == 0: fail("package doctor must reject a lockfile for a different Claro release") - if "BAD lock release version: expected exactly one version: v2026.10.20.223203 line" not in out: + if "BAD lock release version: expected exactly one version: v2026.10.25.014405 line" not in out: fail(f"Lock release-version diagnostic was unclear:\n{out}") if read(work / "claro.lock") != invalid_release_lock: fail("package doctor must not rewrite an invalid lock release version") @@ -343,7 +343,7 @@ def main(): fail("Long package name should not create a package folder") (work / "claro.project").write_text( - "manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v2026.10.20.223203\npackages:\npackage: ../bad\n", + "manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v2026.10.25.014405\npackages:\npackage: ../bad\n", encoding="utf-8", ) rc, out = run([str(EXE), "package", "doctor"], work) @@ -382,7 +382,7 @@ def main(): fail("package add should not create a package folder while unsafe names remain in claro.project") (work / "claro.project").write_text( - "manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v2026.10.20.223203\npackages:\npackage: math-tools\npackage: ../bad\n", + "manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v2026.10.25.014405\npackages:\npackage: math-tools\npackage: ../bad\n", encoding="utf-8", ) rc, out = run([str(EXE), "package", "remove", "math-tools"], work) diff --git a/tools/validate_resource_guards.py b/tools/validate_resource_guards.py new file mode 100644 index 0000000..321f8b0 --- /dev/null +++ b/tools/validate_resource_guards.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Exercise bounded malformed JSON, opt-in resource guards and empty loops.""" +from pathlib import Path +import json +import os +import shlex +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + with tempfile.TemporaryDirectory(prefix='claro-resource-guards-', dir=os.environ.get('TMPDIR')) as tmp: + work = Path(tmp) + override = os.environ.get('CLARO_EXE') + exe = Path(override).resolve() if override else work / 'claro' + if not override: + subprocess.run(shlex.split(os.environ.get('CLARO_CC', 'gcc')) + ['-std=c89', '-pedantic-errors', str(ROOT/'src/claro.c'), '-o', str(exe), '-lm'], check=True) + def run(name, text, error=None, limits=None): + (work/name).write_text(text) + env = dict(os.environ) + for key in ['CLARO_STEP_LIMIT', 'CLARO_READ_FILE_LIMIT']: + env.pop(key, None) + env.update(limits or {}) + try: + r = subprocess.run([str(exe), name], cwd=work, env=env, capture_output=True, text=True, timeout=8) + except subprocess.TimeoutExpired: + raise AssertionError(f'{name}: interpreter failed to terminate') from None + output = r.stdout+r.stderr + assert not any(t in output for t in ["AddressSanitizer", "LeakSanitizer", "runtime error:"]), output + assert r.returncode == (1 if error else 0), (name, r.returncode, output) + if error: + assert error in output and ':0:' not in output, (name, output) + print('PASS:', name) + return r + (work/'invalid.json').write_text('[?]') + run('bad-json.claro', 'READ FILE "invalid.json" AS text\nPARSE JSON text AS data\n', 'JSON') + for number, text in enumerate(['[1,]', '{"x" 1}', '[1 2]', '"unterminated', '1 extra', '01', '[', '{', '{"x":}', '1e+', '"\\uD800"', '"\\uDC00"', '"\\u12zz"', '"\\u0000"', '"\\q"']): + (work/'invalid.json').write_text(text) + run(f'malformed-{number}.claro', 'READ FILE "invalid.json" AS text\nPARSE JSON text AS data\n', 'JSON') + (work/'nested.json').write_text('['*129+'0'+']'*129) + run('deep-json.claro','READ FILE "nested.json" AS text\nPARSE JSON text AS data\n','nesting') + (work/'valid.json').write_text('['*128+'0'+']'*128) + run('valid-depth.claro','READ FILE "valid.json" AS text\nPARSE JSON text AS data\n') + (work/'valid.json').write_text('{"text":"\\u0041\\uD83D\\uDE00", "n": -1.25e2, "ok": true}') + r=run('valid-json.claro','READ FILE "valid.json" AS text\nPARSE JSON text AS data\nGET data KEY "text" AS result\nSAY result\n') + assert r.stdout=='A😀\n',r.stdout + original = {"text": "tab\tnewline\ncontrol\x01quote\"", "values": [1, True, None, "A😀"]} + (work/'valid.json').write_text(json.dumps(original)) + run('roundtrip.claro','READ FILE "valid.json" AS text\nPARSE JSON text AS data\nMAKE JSON data AS encoded\nWRITE FILE "roundtrip.json" WITH encoded\n') + assert json.loads((work/'roundtrip.json').read_text()) == original + for name, text in [('empty-repeat.claro','REPEAT\nUNTIL NO\n'),('empty-count.claro','DO 1000000000 TIMES\nEND\n'),('empty-for.claro','FOR i FROM 1 TO 1000000000\nEND\n')]: + run(name,text,'step limit',{'CLARO_STEP_LIMIT':'20'}) + run('finite.claro','DO 3 TIMES\nSAY "Yes"\nEND\n',limits={'CLARO_STEP_LIMIT':'20'}) + run('missing-until.claro','REPEAT\nSAY "Oops"\n','UNTIL') + (work/'large.txt').write_bytes(b'x'*1048577) + run('read-limit.claro','READ FILE "large.txt" AS data\n','file limit',{'CLARO_READ_FILE_LIMIT':'1048576'}) + (work/'boundary.txt').write_bytes(b'x'*32) + r=run('read-boundary.claro','READ FILE "boundary.txt" AS data\nTEXT LENGTH data AS length\nSAY length\n',limits={'CLARO_READ_FILE_LIMIT':'32'}) + assert r.stdout=='32\n',r.stdout + run('unlimited-read.claro','READ FILE "large.txt" AS data\n') + run('caught-budget.claro','TRY\nREPEAT\nUNTIL NO\nCATCH\nSAY "must not escape"\nENDTRY\n','step limit',{'CLARO_STEP_LIMIT':'20'}) + r=run('long-finite.claro','SET i 0\nREPEAT\nSET i i + 1\nUNTIL i = 20000\nSAY i\n',limits={'CLARO_STEP_LIMIT':'200000'}) + assert r.stdout=='20000\n',r.stdout + run('overflow-limit.claro','SAY "must not execute"\n','CLARO_STEP_LIMIT',{'CLARO_STEP_LIMIT':'999999999999999999999999999999'}) + run('invalid-limit.claro','SAY "must not execute"\n','CLARO_STEP_LIMIT',{'CLARO_STEP_LIMIT':'-1'}) + + +if __name__=='__main__': + main() diff --git a/tools/validate_v1_16.py b/tools/validate_v1_16.py index fb4281f..4d4da63 100644 --- a/tools/validate_v1_16.py +++ b/tools/validate_v1_16.py @@ -43,4 +43,4 @@ with tempfile.TemporaryDirectory() as td: run([str(local), "package", "remove", "text"], cwd=proj) run([str(local), "run"], cwd=proj) -print("Validation passed. Claro v2026.10.20.223203 networking reliability is ready for use.") +print("Validation passed. Claro v2026.10.25.014405 networking reliability is ready for use.") diff --git a/tools/validate_v1_17.py b/tools/validate_v1_17.py index 5ff31a3..5e2d733 100644 --- a/tools/validate_v1_17.py +++ b/tools/validate_v1_17.py @@ -40,12 +40,16 @@ def main(): run([sys.executable, "tools/validate_c89.py"]) run([sys.executable, "tools/validate_msvc6_source.py"]) out = run([str(EXE), "--version"]) - if "Claro v2026.10.20.223203" not in out: + if "Claro v2026.10.25.014405" not in out: raise SystemExit("Wrong version string") run([str(EXE), "test"]) run([str(EXE), "validate"]) run([sys.executable, "tools/validate_first_hour.py"]) run([sys.executable, "tools/validate_learning_syntax.py"]) + run([sys.executable, "tools/validate_error_locations.py"]) + run([sys.executable, "tools/validate_error_guide.py"]) + run([sys.executable, "tools/validate_learner_pilot.py"]) + run([sys.executable, "tools/validate_resource_guards.py"]) run([sys.executable, "tools/validate_ide_metadata.py"]) run([sys.executable, "tools/validate_lsp_helper.py"]) run([sys.executable, "tools/validate_concurrency.py"]) @@ -55,7 +59,7 @@ def main(): run([sys.executable, "tools/validate_version_convention.py"]) run([sys.executable, "tools/validate_documentation_status.py"]) run([str(EXE), "examples/networking.claro"]) - print("v2026.10.20.223203 validation complete") + print("v2026.10.25.014405 validation complete") if __name__ == "__main__": main() diff --git a/tools/validate_version_convention.py b/tools/validate_version_convention.py index ed076ae..b5ca0b0 100644 --- a/tools/validate_version_convention.py +++ b/tools/validate_version_convention.py @@ -7,7 +7,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] EXE = ROOT / "claro.exe" -EXPECTED = "Claro v2026.10.20.223203" +EXPECTED = "Claro v2026.10.25.014405" def fail(message): @@ -31,12 +31,12 @@ def main(): if ide.returncode != 0: fail(ide.stdout) metadata = json.loads(ide.stdout) - if metadata.get("version") != "v2026.10.20.223203": - fail(f"Expected IDE metadata version 'v2026.10.20.223203', got {metadata.get('version')!r}") + if metadata.get("version") != "v2026.10.25.014405": + fail(f"Expected IDE metadata version 'v2026.10.25.014405', got {metadata.get('version')!r}") validator = ROOT / "tools" / "validate_v1_17.py" text = validator.read_text(encoding="utf-8") - if "Claro v2026.10.20.223203" not in text: - fail("tools/validate_v1_17.py must validate Claro v2026.10.20.223203") + if "Claro v2026.10.25.014405" not in text: + fail("tools/validate_v1_17.py must validate Claro v2026.10.25.014405") print("Version convention validation OK")