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:
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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.")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user