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.
This commit is contained in:
Hermes Agent
2026-10-25 02:42:53 +00:00
parent bf230f354e
commit ed23524817
42 changed files with 1128 additions and 176 deletions
+1 -1
View File
@@ -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']
+35
View File
@@ -0,0 +1,35 @@
/* Native-OS evidence helper, not part of the Claro interpreter. */
#include <windows.h>
#include <stdio.h>
#include <string.h>
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;
}
+4
View File
@@ -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",
]
+3
View File
@@ -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',
)
+25
View File
@@ -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()
+48
View File
@@ -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()
+19
View File
@@ -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()
+1 -1
View File
@@ -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)
+9 -9
View File
@@ -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)
+71
View File
@@ -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()
+1 -1
View File
@@ -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.")
+6 -2
View File
@@ -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()
+5 -5
View File
@@ -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")