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.
121 lines
7.8 KiB
Python
121 lines
7.8 KiB
Python
#!/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()
|