fix: stabilize learning path, reliability and legacy Windows builds

Unify beginner syntax while preserving compatibility, repair ownership and parser guards, add verified distribution tooling and VC6 Windows 98 build inputs, and clarify documentation and release status. Preserve DOS build-input line endings and exclude generated binaries/caches.
This commit is contained in:
Hermes Agent
2026-10-20 22:40:19 +00:00
parent 8ea2f772cd
commit d555071406
73 changed files with 2403 additions and 730 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Build clean source/Linux-musl/Windows-x64 archives from the current tree."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import tempfile
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']
FLAGS = ['-std=c89', '-pedantic-errors', '-Werror=implicit-function-declaration', '-O2', '-static']
def sha256(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--target', required=True, choices=['source', 'linux-x86_64', 'windows-x86_64'])
parser.add_argument('--output', required=True, type=Path)
args = parser.parse_args()
match = re.search(r'#define CLARO_VERSION "Claro (v[^"]+)"', (ROOT / 'src/claro.c').read_text())
if match is None:
raise SystemExit('Cannot read the runtime version from src/claro.c')
version = match[1]
args.output.mkdir(parents=True, exist_ok=True)
name = f'Claro-{version}-{args.target}'
destination = args.output / (name + '.zip')
if destination.exists():
raise SystemExit(f'Refusing to replace existing archive: {destination}')
with tempfile.TemporaryDirectory(prefix='claro-package-', dir=os.environ.get('TMPDIR')) as tmp:
stage = Path(tmp) / name
stage.mkdir()
for directory in DIRECTORIES:
for source in sorted((ROOT / directory).rglob('*')):
if not source.is_file() or source.is_symlink() or '__pycache__' in source.parts or source.suffix in ['.pyc', '.o']:
continue
target = stage / source.relative_to(ROOT)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
for filename in FILES:
shutil.copy2(ROOT / filename, stage / filename)
command = []
compiler = None
if args.target != 'source':
if args.target == 'linux-x86_64':
if platform.machine().lower() not in ['x86_64', 'amd64']:
raise SystemExit('Linux x64 package requires an x86-64 musl toolchain')
compiler, executable = 'musl-gcc', 'claro'
notices = {'musl.txt': '/usr/share/doc/musl/copyright', 'GCC-runtime.txt': '/usr/share/doc/gcc-15-base/copyright'}
else:
compiler, executable = 'x86_64-w64-mingw32-gcc', 'claro.exe'
notices = {'MinGW-runtime.txt': '/usr/share/doc/mingw-w64-common/copyright', 'GCC-runtime.txt': '/usr/share/doc/gcc-mingw-w64-x86-64-win32/copyright'}
if not shutil.which(compiler):
raise SystemExit(f'Missing distribution compiler: {compiler}')
command = [compiler] + FLAGS + ['src/claro.c', '-o', executable, '-lm']
if args.target == 'windows-x86_64':
command += ['-Wl,--stack,16777216']
subprocess.run(command, cwd=stage, check=True)
third_party = stage / 'THIRD_PARTY_NOTICES'
third_party.mkdir()
for filename, source in notices.items():
if not Path(source).is_file():
raise SystemExit(f'Missing required toolchain notice: {source}')
shutil.copy2(source, third_party / filename)
metadata = {'version': version, 'target': args.target, 'source_sha256': sha256(stage / 'src/claro.c'), 'build_command': command}
if compiler:
metadata['compiler'] = subprocess.check_output([compiler, '--version'], text=True).splitlines()[0]
(stage / 'DISTRIBUTION.json').write_text(json.dumps(metadata, indent=2) + '\n')
start = 'Extract this entire archive before using Claro. Open a terminal in the extracted folder.\n\n'
if args.target == 'windows-x86_64':
start += 'Run: .\\claro.exe lessons\\01_hello.claro\nThis is a genuine Windows x64 executable, not the Linux build with an .exe suffix.\n'
elif args.target == 'linux-x86_64':
start += 'Run: ./claro lessons/01_hello.claro\nIf extraction removed execute permission: chmod +x claro\nThis is a static musl build; it does not require the builder host glibc.\n'
else:
start += 'Build: make\nOr compile src/claro.c directly with the strict C89 command in README.md.\nOn Windows, use build.bat/build.ps1; the executable needs the documented stack reserve.\n'
start += '\nRead docs/QUICK_START.md and docs/PORTABILITY.md. Run only trusted scripts.\nReal HTTP requires curl; the basic lessons and offline networking need no network.\n'
(stage / 'START_HERE.txt').write_text(start)
entries = {str(path.relative_to(stage)).replace(os.sep, '/'): sha256(path) for path in sorted(stage.rglob('*')) if path.is_file()}
(stage / 'MANIFEST.json').write_text(json.dumps({'sha256': entries}, indent=2) + '\n')
with zipfile.ZipFile(destination, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
for path in sorted(stage.rglob('*')):
if path.is_file():
archive.write(path, path.relative_to(stage.parent))
print(json.dumps({'archive': str(destination.resolve()), 'sha256': sha256(destination), 'target': args.target}, indent=2))
if __name__ == '__main__':
main()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Verify recursive Claro calls stop with the documented runtime diagnostic."""
from pathlib import Path
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parent.parent
EXPECTED = (
"Claro function call depth exceeded the safe limit of 256. "
"Simplify the recursion or add a stopping condition."
)
SCRIPT = """TEACH loop
CALL loop
ENDTEACH
TRY
CALL loop
CATCH
SAY LASTERROR
ENDTRY
"""
def main() -> int:
binary = ROOT / ("claro.exe" if sys.platform == "win32" else "claro")
if not binary.exists():
print(f"FAIL: build Claro first; missing {binary}")
return 1
with tempfile.TemporaryDirectory(prefix="claro-call-depth-") as tmp:
program = Path(tmp) / "recursive.claro"
program.write_text(SCRIPT, encoding="utf-8")
run = subprocess.run([str(binary), str(program)], cwd=ROOT, text=True,
capture_output=True, timeout=30)
output = run.stdout + run.stderr
if EXPECTED not in output:
print("FAIL: recursion did not produce the documented call-depth error")
print(output, end="")
return 1
if run.returncode == 0:
print("FAIL: runtime error unexpectedly exited successfully")
return 1
print("PASS: unbounded recursion stops with the documented call-depth error")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+5
View File
@@ -12,6 +12,11 @@ REQUIRED_COMMANDS = [
"python3 tools/validate_version_convention.py",
"python3 tools/validate_package_security.py",
"python3 tools/validate_compiler_warnings.py",
"python3 tools/validate_reliability.py",
"python3 tools/validate_learning_syntax.py",
"python3 tools/validate_c89.py",
"python3 tools/validate_msvc6_source.py",
"python3 tools/validate_documentation_status.py",
"python3 tools/validate_ci_workflow.py",
]
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Extract a distribution, verify hashes, and execute its actual payload."""
import argparse
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shlex
import shutil
import stat
import struct
import subprocess
import sys
import tempfile
import zipfile
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('archive', type=Path)
parser.add_argument('--wine-prefix', type=Path)
parser.add_argument('--report', type=Path, required=True)
args = parser.parse_args()
if args.report.exists():
raise SystemExit('Refusing to replace existing verification report')
results = {'archive': str(args.archive.resolve()), 'sha256': digest(args.archive), 'checks': []}
logs = args.report.parent / (args.report.stem + '-logs')
logs.mkdir(parents=True, exist_ok=False)
env = dict(os.environ)
def run(label, command, cwd, timeout=180):
result = subprocess.run(command, cwd=cwd, env=env, capture_output=True, text=True, timeout=timeout)
log = logs / (label + '.log')
log.write_text(result.stdout + result.stderr, encoding='utf-8')
results['checks'].append({'name': label, 'exit': result.returncode, 'log': str(log.resolve())})
assert result.returncode == 0, (label, result.returncode, (result.stdout + result.stderr)[-3000:])
print('PASS:', label)
return result.stdout
try:
with tempfile.TemporaryDirectory(prefix='claro-unpack-', dir=os.environ.get('TMPDIR')) as tmp:
work = Path(tmp) / 'unpacked package with spaces'
work.mkdir()
with zipfile.ZipFile(args.archive) as archive:
names = archive.namelist()
assert len(names) == len(set(names)), 'Duplicate archive entries'
assert len({PurePosixPath(name).parts[0] for name in names}) == 1, 'Expected one archive root'
for info in archive.infolist():
path = PurePosixPath(info.filename)
assert not path.is_absolute() and '..' not in path.parts and '\\' not in info.filename, 'Unsafe archive path'
mode = info.external_attr >> 16
assert not stat.S_ISLNK(mode), 'Archive must not contain symlinks'
target = work.joinpath(*path.parts)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(archive.read(info))
if os.name != 'nt' and mode:
target.chmod(mode & 0o777)
root = next(work.iterdir())
manifest = json.loads((root / 'MANIFEST.json').read_text())['sha256']
actual = {str(p.relative_to(root)).replace(os.sep, '/'): digest(p) for p in root.rglob('*') if p.is_file() and p.name != 'MANIFEST.json'}
assert actual == manifest, 'Package manifest mismatch'
assert not any('__pycache__' in name or name.endswith('.pyc') or 'claro-asan' in name for name in actual), 'Development leftovers in package'
metadata = json.loads((root / 'DISTRIBUTION.json').read_text())
assert metadata['source_sha256'] == digest(root / 'src/claro.c')
results.update(target=metadata['target'], version=metadata['version'], manifest_files=len(manifest), source_sha256=metadata['source_sha256'])
print('PASS: archive inventory, source hash and manifest')
target = metadata['target']
if target == 'source':
assert not (root / 'claro').exists() and not (root / 'claro.exe').exists()
isolated = work / 'standalone'
isolated.mkdir()
shutil.copy2(root / 'src/claro.c', isolated / 'claro.c')
run('isolated-c89-build', ['gcc', '-std=c89', '-pedantic-errors', '-Werror=implicit-function-declaration', 'claro.c', '-o', 'claro', '-lm'], isolated)
run('isolated-version', [str(isolated / 'claro'), '--version'], isolated)
run('source-make', ['make', '-s', 'all'], root)
executable = root / ('claro.exe' if os.name == 'nt' else 'claro')
runner = [str(executable)]
elif target == 'linux-x86_64':
executable = root / 'claro'
assert not (root / 'claro.exe').exists(), 'Linux archive contains misleading .exe'
data = executable.read_bytes()
assert data[:5] == b'\x7fELF\x02' and struct.unpack_from('<H', data, 18)[0] == 62
headers = run('elf-inspection', ['readelf', '-l', str(executable)], root)
assert 'INTERP' not in headers, 'Linux release is not static'
runner = [str(executable)]
elif target == 'windows-x86_64':
executable = root / 'claro.exe'
assert not (root / 'claro').exists()
data = executable.read_bytes()
assert data[:2] == b'MZ'
pe = struct.unpack_from('<I', data, 60)[0]
assert data[pe:pe+4] == b'PE\0\0' and struct.unpack_from('<H', data, pe+4)[0] == 0x8664
reserve = struct.unpack_from('<Q', data, pe+24+72)[0]
assert reserve >= 16777216, 'Windows recursion guard requires a sufficient stack reserve'
results['windows_stack_reserve'] = reserve
pe_text = run('pe-inspection', ['x86_64-w64-mingw32-objdump', '-p', str(executable)], root)
results['windows_dll_imports'] = re.findall(r'DLL Name:\s*(\S+)', pe_text)
if os.name == 'nt':
runner = [str(executable)]
else:
assert args.wine_prefix and args.wine_prefix.is_dir(), 'Supply an initialized isolated --wine-prefix'
env.update(WINEPREFIX=str(args.wine_prefix.resolve()), WINEDEBUG='-all')
runner = ['wine', str(executable)]
else:
raise AssertionError('Unknown distribution target')
version = run('version', runner + ['--version'], root)
assert metadata['version'] in version
standalone = work / 'single executable with spaces'
standalone.mkdir()
isolated_binary = standalone / executable.name
shutil.copy2(executable, isolated_binary)
(standalone / 'hello.claro').write_text('SAY "Standalone Claro works"\n')
isolated_runner = (['wine', str(isolated_binary)] if runner[0] == 'wine' else [str(isolated_binary)])
output = run('standalone-runtime', isolated_runner + ['hello.claro'], standalone)
assert output == 'Standalone Claro works\n'
run('doctor', runner + ['doctor'], root)
golden = run('golden-tests', runner + ['test'], root)
results['golden_pass_count'] = len(re.findall(r'^PASS tests/', golden, re.M))
assert results['golden_pass_count'] == len(list((root / 'tests').glob('*.out'))), 'Golden runner did not cover all packaged fixtures'
run('builtin-validation', runner + ['validate'], root)
if runner[0] == 'wine':
launcher = work / 'wine-launcher'
launcher.write_text('#!/bin/sh\nexec wine ' + shlex.quote(str(executable)) + ' "$@"\n')
launcher.chmod(0o755)
env['CLARO_EXE'] = str(launcher)
else:
env['CLARO_EXE'] = str(executable)
run('learning-path', [sys.executable, str(root / 'tools/validate_learning_syntax.py')], root, timeout=600 if runner[0] == 'wine' else 180)
run('first-hour', [sys.executable, str(root / 'tools/validate_first_hour.py')], root)
if target == 'source':
run('full-source-validation', [sys.executable, str(root / 'tools/validate_v1_17.py')], root, timeout=360)
results['status'] = 'passed'
except Exception as error:
results.update(status='failed', error=str(error))
raise
finally:
args.report.write_text(json.dumps(results, indent=2) + '\n', encoding='utf-8')
print('Verified:', args.report)
if __name__ == '__main__':
main()
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Check active status/navigation without treating historical claims as current."""
import ast
from pathlib import Path
import re
from urllib.parse import unquote, urlsplit
ROOT = Path(__file__).resolve().parents[1]
ACTIVE = (
'README.md', 'docs/README.md', 'docs/CURRENT_STATUS.md',
'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/history/CURRENT_STATUS_BEFORE_STATUS_CLEANUP.md',
'docs/history/HARDENING_BEFORE_STATUS_CLEANUP.md',
)
def validate():
match = re.search(r'#define CLARO_VERSION "Claro ([^"]+)"',
(ROOT / 'src/claro.c').read_text())
assert match, 'Missing runtime version'
version = match.group(1)
readme = (ROOT / 'README.md').read_text()
assert 'Current release:' not in readme, 'A development stamp is not a published release'
assert f'Development version: **{version}**' in readme, 'README must match the runtime development stamp'
status = (ROOT / 'docs/CURRENT_STATUS.md').read_text()
assert version in status and 'RELEASE_STATUS.md' in status
assert len(status.splitlines()) <= 100, 'Keep the current status concise; archive detailed history'
assert 'not a sandbox' in status.lower()
release = (ROOT / 'docs/RELEASE_STATUS.md').read_text()
assert 'not published' in release.lower() and 'wine' in release.lower()
assert 'unverified' in release.lower() and 'MSVC600' in release
checklist = (ROOT / 'docs/FINAL_RELEASE_CHECKLIST.md').read_text()
assert 'v1.xx.yy' not in checklist and 'vYYYY.MM.DD.HHMMSS' in checklist
assert '[ ]' in checklist, 'Release actions must be explicit, unchecked gates'
classroom = (ROOT / 'docs/CLASSROOM_READINESS.md').read_text().lower()
assert 'learner testing' in classroom and 'not yet validated' in classroom
hardening = (ROOT / 'docs/HARDENING.md').read_text()
assert 'historical' in hardening.lower() and 'RELIABILITY.md' in hardening
for name in ['CURRENT_STATUS_BEFORE_STATUS_CLEANUP.md', 'HARDENING_BEFORE_STATUS_CLEANUP.md']:
assert (ROOT / 'docs/history' / name).is_file(), ('Missing historical record', name)
tree = ast.parse((ROOT / 'tools/stamp_version.py').read_text())
current_docs = next(ast.literal_eval(n.value) for n in tree.body
if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'CURRENT_DOCS' for t in n.targets))
assert 'RELEASE_STATUS.md' not in current_docs, 'Do not re-stamp immutable verification checkpoints'
checked = 0
for name in ACTIVE:
path = ROOT / name
text = re.sub(r'```.*?```', '', path.read_text(), flags=re.S)
for target in re.findall(r'!?\[[^\]]*\]\(([^\s)]+)\)', text):
url = urlsplit(target)
if url.scheme or url.netloc or not url.path:
continue
destination = (path.parent / unquote(url.path)).resolve()
assert destination.is_relative_to(ROOT), (name, 'Link leaves the repository', target)
assert destination.exists(), (name, 'Broken local link', target)
checked += 1
print(f'PASS: development/release separation, archived history, claim scope, and {checked} local documentation links')
if __name__ == '__main__':
validate()
+2 -2
View File
@@ -8,7 +8,7 @@ import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
EXE = ROOT / ("claro.exe" if os.name == "nt" else "claro")
EXE = Path(os.environ.get("CLARO_EXE", str(ROOT / ("claro.exe" if os.name == "nt" else "claro")))).resolve()
INPUTS = {2: "Ada\n", 5: "4\n"}
EXPECTED = {
1: "Hello!\n",
@@ -35,7 +35,7 @@ def main() -> int:
script.write_text(block + "\n", encoding="utf-8")
run = subprocess.run([str(EXE), str(script)], cwd=tmp,
input=INPUTS.get(number, ""), text=True,
capture_output=True)
capture_output=True, timeout=10)
if run.returncode or run.stdout != EXPECTED[number]:
print(f"FAIL: First Hour block {number}: exit={run.returncode}")
print(f"expected stdout: {EXPECTED[number]!r}")
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Run the full introductory learning path with invalid-access and leak checks."""
from pathlib import Path
import os
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
def main():
if os.name == 'nt':
print('SKIP: learning LeakSanitizer validation requires POSIX')
return
with tempfile.TemporaryDirectory(prefix='claro-learning-ownership-', dir=os.environ.get('TMPDIR')) as tmp:
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']:
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)
raise SystemExit(result.returncode)
print('PASS: leak-checked', checker)
print('PASS: introductory lessons, examples, checker paths and REPL under ASan/UBSan/LSan')
if __name__ == '__main__':
main()
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Keep the introductory learning path consistent and executable."""
import os
import json
from pathlib import Path
import re
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
EXE = Path(os.environ.get('CLARO_EXE', str(ROOT / ('claro.exe' if os.name == 'nt' else 'claro')))).resolve()
DOCS = ['README.md', 'docs/QUICK_START.md', 'docs/FIRST_HOUR.md', 'docs/SIMPLE_FUNCTIONS.md']
LEGACY = re.compile(r'^\s*(?:CALL\b|LEARNED\b|ENDIF\b|DONE\b|TEACH\b[^\n]*\bTAKES\b|SET\s+\w+\s+TO\b|ASK\s+"(?:[^"\\]|\\.)*"\s+AS\b)', re.MULTILINE)
LESSONS = {
'01_hello.claro': ('', 'Lesson 1: Hello\nClaro can show messages with SAY.\nHello from Claro!\n'),
'02_ask_name.claro': ('Ada\n', 'Lesson 2: Ask a question\nWhat is your name?\nHello Ada\n'),
'03_variables.claro': ('', 'Lesson 3: Variables\nAlex\n5\n'),
'04_math.claro': ('', 'Lesson 4: Math\n5\n'),
'05_if_else.claro': ('', 'Lesson 5: Choices\nYou passed!\n'),
'06_loops.claro': ('', 'Lesson 6: Loops\nPractice\nPractice\nPractice\n1\n2\n3\n'),
'07_lists.claro': ('', 'Lesson 7: Lists\nmilk\nbread\n'),
'08_functions.claro': ('', 'Lesson 8: Functions with TEACH\nHello Alex\n'),
'09_files.claro': ('', 'Lesson 9: Files\nClaro saved this note.\n'),
'10_final_quiz.claro': ('4\n', 'Lesson 10: Tiny Quiz\nWhat is 2 + 2?\nCorrect!\n'),
}
QUICK = [('', 'Hello!\n'), ('Ada\n', 'What is your name?\nHello Ada\n'),
('', '10\n'), ('', 'Good job!\n'), ('', 'Practice\n' * 3),
('', 'milk\nbread\n'), ('', 'Hello Alex\n'), ('', 'Hello from Claro\n'),
('4\n', 'Tiny Quiz\nWhat is 2 + 2?\nCorrect!\n')]
def blocks(path):
return re.findall(r'```claro\s*\n(.*?)\n```', path.read_text(encoding='utf-8'), re.S)
def check_run(work, label, code, input_text, expected):
script = work / 'example.claro'
script.write_text(code + '\n', encoding='utf-8')
for command in ['check', 'typecheck']:
checked = subprocess.run([str(EXE), command, str(script)], cwd=work, text=True, capture_output=True, timeout=10)
assert checked.returncode == 0, (label, command, checked.stdout, checked.stderr)
result = subprocess.run([str(EXE), str(script)], cwd=work, input=input_text,
text=True, capture_output=True, timeout=10)
assert result.returncode == 0 and result.stdout == expected and not result.stderr, (label, result.returncode, result.stdout, result.stderr)
print('PASS:', label)
def main():
actual = {p.name for p in (ROOT / 'lessons').glob('*.claro')}
assert actual == set(LESSONS), ('Lesson coverage changed; update checker', actual)
sources = [(name, '\n'.join(blocks(ROOT / name))) for name in DOCS]
sources += [('lessons/' + name, (ROOT / 'lessons' / name).read_text()) for name in LESSONS]
examples = json.loads((ROOT / 'tests/learning_examples.json').read_text())
assert len(examples) == 10, 'Introductory example coverage changed'
sources += [('examples/' + name, (ROOT / 'examples' / name).read_text()) for name in examples]
for name, code in sources:
found = LEGACY.search(code)
assert not found, f'{name}: introductory syntax must use short ASK/SET, TEACH/DO, and END; found {found.group(0)!r}'
print('PASS: introductory syntax policy')
with tempfile.TemporaryDirectory(prefix='claro-learning-', dir=os.environ.get('TMPDIR')) as tmp:
work = Path(tmp)
modern_function = blocks(ROOT / 'docs/FIRST_HOUR.md')[7]
function_script = work / 'modern-function.claro'
function_script.write_text(modern_function + '\n')
for command in ['check', 'typecheck']:
result = subprocess.run([str(EXE), command, str(function_script)], cwd=work, text=True, capture_output=True, timeout=10)
assert result.returncode == 0, ('modern multi-argument function', command, result.stdout, result.stderr)
print('PASS: modern function accepted by', command)
mistakes = [
('SET', 'SET\n', 'Try: SET name "Alex"'),
('IF', 'IF YES\n SAY "yes"\n', 'IF block opened here needs END'),
('TEACH', 'TEACH greet name\n SAY name\n', 'TEACH block opened here needs END'),
('DO', 'DO 2 TIMES\n SAY "practice"\n', 'DO block opened here needs END'),
('FOR', 'FOR i FROM 1 TO 3\n SAY i\n', 'FOR block opened here needs END'),
('REPEAT', 'REPEAT\n SAY "practice"\n', 'REPEAT block opened here needs UNTIL'),
]
for label, code, hint in mistakes:
script = work / 'mistake.claro'
script.write_text(code)
result = subprocess.run([str(EXE), 'check', str(script)], cwd=work, text=True, capture_output=True, timeout=10)
assert result.returncode != 0 and hint in result.stdout, (label, result.returncode, result.stdout)
assert not re.search(r'needs (?:ENDIF|DONE|LEARNED)|SET name TO', result.stdout), result.stdout
print('PASS: beginner repair hint for', label)
check_run(work, 'quoted multi-argument function', 'TEACH greet first last\n RETURN first + last\nEND\nDO greet "Hello " "Ada"\nSAY RESULT', '', 'Hello Ada\n')
check_run(work, 'bare container constructors', 'SET items LIST\nSET pairs MAP\nSAY items\nSAY pairs', '', '[list]\n[map]\n')
for name, (input_text, expected) in LESSONS.items():
check_run(work, name, (ROOT / 'lessons' / name).read_text(), input_text, expected)
check_run(work, 'quiz wrong answer', (ROOT / 'lessons/10_final_quiz.claro').read_text(), '3\n', 'Lesson 10: Tiny Quiz\nWhat is 2 + 2?\nThe answer is 4.\n')
quick = blocks(ROOT / 'docs/QUICK_START.md')
assert len(quick) == len(QUICK), 'Quick Start block coverage changed'
for number, (code, (input_text, expected)) in enumerate(zip(quick, QUICK), 1):
check_run(work, f'Quick Start block {number}', code, input_text, expected)
functions = blocks(ROOT / 'docs/SIMPLE_FUNCTIONS.md')
assert len(functions) == 1, 'Function guide should teach one initial form'
check_run(work, 'simple function guide', functions[0], '', 'Hello Jon\n')
older = blocks(ROOT / 'docs/COMPATIBILITY.md')
assert len(older) == 1, 'Compatibility guide coverage changed'
compatibility = older[0]
check_run(work, 'legacy syntax remains supported', compatibility, '4\n', '5\nNumber?\n8\n8\n')
for name, (input_text, expected) in examples.items():
check_run(work, 'examples/' + name, (ROOT / 'examples' / name).read_text(), input_text, expected)
result = subprocess.run([str(EXE), 'new', 'Starter'], cwd=work, text=True, capture_output=True, timeout=10)
assert result.returncode == 0, result.stderr
starter = (work / 'Starter/main.claro').read_text()
assert not LEGACY.search(starter), 'Generated starter still teaches older syntax'
check_run(work, 'generated starter', starter, '', 'Welcome to Claro!\nHello Learner\nNice to meet you, Friend\n')
repl = subprocess.run([str(EXE), 'repl'], cwd=work, input='HELP\nEXIT\n', text=True, capture_output=True, timeout=10)
assert repl.returncode == 0 and 'SET name "Jon"' in repl.stdout and 'SET name TO' not in repl.stdout, repl.stdout
metadata = json.loads(subprocess.check_output([str(EXE), 'ide'], cwd=work, text=True))
snippets = {item['name']: item['body'] for item in metadata['snippets']}
for name in ['hello', 'if_else', 'function']:
assert not LEGACY.search(snippets[name]), ('IDE snippet still teaches older syntax', name)
check_run(work, 'IDE choice snippet', 'SET condition YES\n' + snippets['if_else'], '', 'Yes\n')
check_run(work, 'IDE function snippet', snippets['function'] + '\nDO name "Ada"', '', 'Ada\n')
print('PASS: REPL help and beginner IDE snippets')
print('Learning syntax validation complete')
if __name__ == '__main__':
main()
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Prevent VC6 handle/Win98 build-setting regressions without needing the SDK."""
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
def main():
source = (ROOT / 'src/claro.c').read_text()
start = source.index('static int run_tests(')
body = source[start:source.index('static int ', start + 20)]
branches = re.search(r'#if defined\(_MSC_VER\) && _MSC_VER < 1300\s+(.*?)#else\s+(.*?)#endif\s+h = _findfirst', body, re.S)
assert branches, 'Old MSVC needs its own long _findfirst handle before calling the CRT'
legacy = re.sub(r'/\*.*?\*/', '', branches[1], flags=re.S)
assert re.search(r'\blong h;', legacy) and 'intptr_t' not in legacy
assert re.search(r'\bintptr_t h;', branches[2]), 'Preserve the modern pointer-sized handle'
build = (ROOT / 'build98.bat').read_text()
response = ROOT / 'build98.rsp'
options = build + (response.read_text() if response.exists() else '')
for option in ['/TC', '/ML', '/MACHINE:I386', '/SUBSYSTEM:CONSOLE,4.10', '/STACK:16777216']:
assert option in options, ('Missing Windows 98 build setting', option)
assert max(map(len, build.splitlines())) < 127, 'Keep COMMAND.COM command lines below its legacy length limit'
assert '/Dintptr_t' not in options, 'Fix source rather than injecting a global type macro'
for name in ['build98.bat', 'build98.rsp']:
data = (ROOT / name).read_bytes()
assert b'\r\n' in data and b'\n' not in data.replace(b'\r\n', b''), ('Use DOS line endings', name)
assert len(Path(name).stem) <= 8
assert 'cl @build98.rsp' in build
assert not re.search(r'(?i)setlocal|exit /b|%~|\bwhere\b|&&|\|\|', build), 'Use COMMAND.COM-compatible batch syntax'
assert 'build98.bat' in (ROOT / 'tools/build_distribution.py').read_text(), 'Include the legacy build script in source distributions'
print('PASS: guarded VC6 CRT handle, modern handle, Win98 build settings and source packaging')
if __name__ == '__main__':
main()
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Deep control blocks must produce diagnostics, not checker/CPU stack overflows."""
from pathlib import Path
import os
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
def main():
if os.name == 'nt':
print('SKIP: current nesting sanitizer build requires POSIX')
return
with tempfile.TemporaryDirectory(prefix='claro-nesting-', dir=os.environ.get('TMPDIR')) as tmp:
work = Path(tmp)
exe = work / '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(exe), '-lm'], check=True)
env = dict(os.environ, ASAN_OPTIONS='detect_leaks=1:halt_on_error=1', UBSAN_OPTIONS='halt_on_error=1')
for depth, expected in [(64, 0), (600, 1)]:
script = work / 'nested.claro'
script.write_text('IF YES\n' * depth + 'SAY "OK"\n' + 'END\n' * depth)
for command in ['check', 'run']:
run = subprocess.run([str(exe), command, str(script)], cwd=work, env=env, capture_output=True, text=True, timeout=15)
combined = run.stdout + run.stderr
assert run.returncode == expected, (depth, command, run.returncode, combined[-2000:])
assert not any(s in combined for s in ['AddressSanitizer', 'LeakSanitizer', 'runtime error:']), combined
if expected:
assert 'block nesting' in combined.lower(), (command, combined)
else:
assert run.stdout == ('OK\n' if command == 'run' else 'OK\n'), (command, combined)
print('PASS:', command, 'block depth', depth)
if __name__ == '__main__':
main()
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Check OBJECT CLASS/FIELDS release evaluated values under sanitizers."""
from pathlib import Path
import os
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parent.parent
def main() -> int:
if os.name == "nt":
print("SKIP: LeakSanitizer check is POSIX-only")
return 0
with tempfile.TemporaryDirectory(prefix="object-expression-cleanup-") as tmp:
binary = Path(tmp) / "claro-asan"
build = subprocess.run(
["gcc", "-std=c89", "-pedantic-errors", "-Werror=implicit-function-declaration", "-O0", "-g", "-fsanitize=address,undefined",
"src/claro.c", "-o", str(binary), "-lm"],
cwd=ROOT, text=True, capture_output=True,
)
if build.returncode:
print(build.stdout, end="")
print(build.stderr, end="", file=sys.stderr)
return build.returncode
script = Path(tmp) / "object-expressions.claro"
script.write_text(
"CLASS Person\nHAS name TEXT\nEND\nNEW Person person\n"
"DO 2000 TIMES\nOBJECT CLASS person AS kind\nOBJECT FIELDS person AS fields\nEND\n"
"SAY kind\nCOUNT fields AS count\nGET fields AT 1 AS first\nSAY count\nSAY first\n", encoding="utf-8"
)
run = subprocess.run(
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
)
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
print("FAIL: repeated OBJECT CLASS/FIELDS retains expression allocations")
print(run.stdout, end="")
print(run.stderr, end="", file=sys.stderr)
return 1
if run.stdout != "Person\n1\nname\n":
print(f"FAIL: unexpected output: {run.stdout!r}")
return 1
print("PASS: repeated OBJECT CLASS/FIELDS is leak-free and preserves results")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+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.160950"
EXPECTED_VERSION = "Claro v2026.10.20.223203"
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.160950", "packages:"]:
for phrase in ["manifest-version: 1", "name: StarterApp", "main: main.claro", "version: v2026.10.20.223203", "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.160950"]:
for phrase in ["lock-version: 1", "version: v2026.10.20.223203"]:
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.160950", "packages:"]:
for phrase in ["manifest-version: 1", "name: ClaroProject", "main: main.claro", "version: v2026.10.20.223203", "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.160950", "package: math-tools", "checksum:"]:
for phrase in ["lock-version: 1", "version: v2026.10.20.223203", "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.160950", "version: v1.18.25", 1)
invalid_release_lock = lock.replace("version: v2026.10.20.223203", "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.160950 line" not in out:
if "BAD lock release version: expected exactly one version: v2026.10.20.223203 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.160950\npackages:\npackage: ../bad\n",
"manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v2026.10.20.223203\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.160950\npackages:\npackage: math-tools\npackage: ../bad\n",
"manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v2026.10.20.223203\npackages:\npackage: math-tools\npackage: ../bad\n",
encoding="utf-8",
)
rc, out = run([str(EXE), "package", "remove", "math-tools"], work)
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Bounded malformed-expression regressions; sanitizer runs include leak checks."""
from pathlib import Path
import os
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
DRIVER = r'''#define main claro_cli_main
#include "claro.c"
#undef main
int main(int argc,char **argv)
{
Runtime rt;
Value result;
int status;
if(argc==3 && strcmp(argv[1],"check")==0) return check_file(argv[2]);
if(argc!=2) return 2;
rt_init(&rt);
result=eval_expr(&rt,argv[1]);
value_free(result);
status=rt.error?1:0;
if(rt.error) fprintf(stderr,"%s\n",rt.err_msg);
runtime_free(&rt);
return status;
}
'''
CASES = [
('!', 'NOT'),
('(1 + 2', 'closing'),
('1 +', 'value'),
('"unfinished', 'quote'),
('1 2', 'Unexpected'),
('(' * 600 + '1' + ')' * 600, 'nesting'),
('NOT ' * 600 + 'YES', 'nesting'),
]
def limited():
import resource
resource.setrlimit(resource.RLIMIT_AS, (128 * 1024 * 1024, 128 * 1024 * 1024))
def main():
if os.name == 'nt':
print('SKIP: current parser sanitizer driver requires POSIX')
return
with tempfile.TemporaryDirectory(prefix='claro-parse-', dir=os.environ.get('TMPDIR')) as tmp:
work = Path(tmp)
(work / 'claro.c').write_bytes((ROOT / 'src/claro.c').read_bytes())
driver = work / 'parse.c'
driver.write_text(DRIVER)
flags = ['gcc', '-std=c89', '-pedantic-errors', '-Werror=implicit-function-declaration', '-O0', '-g']
plain, sanitized = work / 'parse', work / 'parse-asan'
subprocess.run(flags + [str(driver), '-o', str(plain), '-lm'], check=True)
for expression, expected in CASES:
run = subprocess.run([str(plain), expression], cwd=work, capture_output=True, text=True, timeout=5, preexec_fn=limited)
assert run.returncode == 1 and expected in run.stderr, (expression[:50], run.returncode, run.stderr[-1000:])
subprocess.run(flags + ['-fsanitize=address,undefined', str(driver), '-o', str(sanitized), '-lm'], check=True)
env = dict(os.environ, ASAN_OPTIONS='detect_leaks=1:halt_on_error=1', UBSAN_OPTIONS='halt_on_error=1')
for expression, expected in CASES:
run = subprocess.run([str(sanitized), expression], cwd=work, env=env, capture_output=True, text=True, timeout=10)
assert run.returncode == 1 and expected in run.stderr, (expression[:50], run.returncode, run.stderr[-1000:])
assert not any(marker in run.stderr for marker in ['AddressSanitizer', 'LeakSanitizer', 'runtime error:']), run.stderr
for expression, expected in CASES[:1] + CASES[3:4]:
script = work / 'bad.claro'
script.write_text('SAY ' + expression + '\n')
run = subprocess.run([str(sanitized), 'check', str(script)], cwd=work, env=env, capture_output=True, text=True, timeout=10)
assert run.returncode == 1 and expected in run.stdout, ('check', expression, run.returncode, run.stdout, run.stderr[-1000:])
assert not run.stderr, run.stderr
for expression in ['(1 + 2) * 3', '(' * 64 + '1' + ')' * 64, 'NOT ' * 64 + 'YES', '"a" + "b"', '1 != 2']:
run = subprocess.run([str(sanitized), expression], cwd=work, env=env, capture_output=True, text=True, timeout=10)
assert run.returncode == 0 and not run.stderr, (expression[:50], run.returncode, run.stderr[-1000:])
print('PASS: seven malformed expressions and five valid expressions terminate cleanly under ASan/UBSan/LSan')
if __name__ == '__main__':
main()
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Run the maintained ownership, malformed-input, nesting and safety regressions."""
from concurrent.futures import ThreadPoolExecutor
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
EXTRA = ['validate_learning_ownership.py', 'validate_value_ownership.py', 'validate_call_depth_guard.py',
'validate_nesting_guards.py', 'validate_http_hardening.py',
'validate_trusted_command_docs.py']
def main():
scripts = sorted((ROOT / 'tools').glob('validate_*cleanup.py'))
scripts += [ROOT / 'tools' / name for name in EXTRA]
assert len(scripts) == len(set(scripts)), 'Duplicate reliability checks'
assert all(path.is_file() and path.stat().st_size for path in scripts), 'Missing or empty reliability check'
logs = Path(tempfile.mkdtemp(prefix='claro-reliability-', dir=os.environ.get('TMPDIR')))
def run(path):
try:
result = subprocess.run([sys.executable, str(path)], cwd=ROOT, text=True, capture_output=True, timeout=180)
output = result.stdout + result.stderr
status = result.returncode
except subprocess.TimeoutExpired as error:
output = f'TIMEOUT: {error}'
status = 124
log = logs / (path.stem + '.log')
log.write_text(output, encoding='utf-8')
return {'test': path.name, 'exit': status, 'log': str(log), 'skipped': output.startswith('SKIP:')}
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(run, scripts))
(logs / 'results.json').write_text(json.dumps(results, indent=2), encoding='utf-8')
for result in results:
status = 'FAIL' if result['exit'] else ('SKIP' if result['skipped'] else 'PASS')
print(status + ': ' + result['test'])
if result['exit']:
print(Path(result['log']).read_text()[-6000:])
failed = sum(result['exit'] != 0 for result in results)
skipped = sum(result['skipped'] for result in results)
print(f'Reliability checks: {len(results)} total, {failed} failed, {skipped} skipped')
print('Results:', logs / 'results.json')
return 1 if failed else 0
if __name__ == '__main__':
raise SystemExit(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.160950 networking reliability is ready for use.")
print("Validation passed. Claro v2026.10.20.223203 networking reliability is ready for use.")
+6 -8
View File
@@ -38,26 +38,24 @@ def build():
def main():
build()
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.160950" not in out:
if "Claro v2026.10.20.223203" 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_ide_metadata.py"])
run([sys.executable, "tools/validate_lsp_helper.py"])
run([sys.executable, "tools/validate_concurrency.py"])
run([sys.executable, "tools/validate_package_security.py"])
run([sys.executable, "tools/validate_typecheck_diagnostics.py"])
run([sys.executable, "tools/validate_runtime_error_cleanup.py"])
run([sys.executable, "tools/validate_get_argument_expression_cleanup.py"])
run([sys.executable, "tools/validate_get_env_expression_cleanup.py"])
run([sys.executable, "tools/validate_get_at_expression_cleanup.py"])
run([sys.executable, "tools/validate_read_file_expression_cleanup.py"])
run([sys.executable, "tools/validate_write_file_expression_cleanup.py"])
run([sys.executable, "tools/validate_reliability.py"])
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.160950 validation complete")
print("v2026.10.20.223203 validation complete")
if __name__ == "__main__":
main()
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Isolate borrowed metadata and owned object temporaries with ASan/UBSan/LSan."""
from pathlib import Path
import os
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
DRIVER = r'''#define main claro_cli_main
#include "claro.c"
#undef main
#include <assert.h>
int main(int argc,char **argv)
{
Runtime rt;
Value obj,text;
ClassDef *cls;
int i,which;
const char *name;
char typebuf[320];
assert(argc==2);
which=atoi(argv[1]);
rt_init(&rt);
if(which==1){
for(i=0;i<2000;i++) assert(rt_set_checked(&rt,"probe",1,"score","NUMBER",v_num(i)));
} else if(which==2){
text=v_str("NUMBER"); rt_set(&rt,"__type_score",text); value_free(text);
for(i=0;i<2000;i++){
name=runtime_type_for(&rt,"score",typebuf,sizeof(typebuf));
assert(name && strcmp(name,"NUMBER")==0);
}
} else if(which==3){
obj=v_map(); text=v_str("Person"); map_put(obj.map,"class",text); value_free(text);
for(i=0;i<2000;i++){
name=object_class_name(obj); assert(name && strcmp(name,"Person")==0);
}
value_free(obj);
} else if(which==4){
obj=v_map(); rt_set(&rt,"person",obj); value_free(obj);
for(i=0;i<2000;i++) assert(rt_set_checked(&rt,"probe",1,"person.score","NUMBER",v_num(i)));
} else if(which==5){
cls=add_class(&rt,"Person"); class_add_field(cls,"name","TEXT");
for(i=0;i<2000;i++) create_object_value(&rt,"probe",1,"Person","person");
} else return 2;
assert(!rt.error);
runtime_free(&rt);
return 0;
}
'''
def main():
if os.name == 'nt':
print('SKIP: LeakSanitizer ownership checks require POSIX')
return
failures = []
with tempfile.TemporaryDirectory(prefix='claro-ownership-', dir=os.environ.get('TMPDIR')) as tmp:
work = Path(tmp)
(work / 'claro.c').write_bytes((ROOT / 'src/claro.c').read_bytes())
driver = work / 'ownership.c'
driver.write_text(DRIVER)
binary = work / 'ownership'
subprocess.run(['gcc', '-std=c89', '-pedantic-errors', '-Werror=implicit-function-declaration', '-g', '-O0', '-fsanitize=address,undefined', str(driver), '-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')
for number, name in enumerate(['checked type storage', 'runtime type metadata', 'object class metadata', 'object field copies', 'object construction'], 1):
run = subprocess.run([str(binary), str(number)], cwd=work, env=env, capture_output=True, text=True, timeout=20)
if run.returncode or run.stderr:
failures.append(name)
print('FAIL:', name, run.stderr[-3500:])
else:
print('PASS:', name)
if failures:
raise SystemExit('Ownership failures: ' + ', '.join(failures))
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.160950"
EXPECTED = "Claro v2026.10.20.223203"
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.160950":
fail(f"Expected IDE metadata version 'v2026.10.20.160950', got {metadata.get('version')!r}")
if metadata.get("version") != "v2026.10.20.223203":
fail(f"Expected IDE metadata version 'v2026.10.20.223203', got {metadata.get('version')!r}")
validator = ROOT / "tools" / "validate_v1_17.py"
text = validator.read_text(encoding="utf-8")
if "Claro v2026.10.20.160950" not in text:
fail("tools/validate_v1_17.py must validate Claro v2026.10.20.160950")
if "Claro v2026.10.20.223203" not in text:
fail("tools/validate_v1_17.py must validate Claro v2026.10.20.223203")
print("Version convention validation OK")