diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index dc8cdc8..e060291 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2245,4 +2245,13 @@ target_link_libraries(step373_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step374_test tests/step374_test.cpp) +target_include_directories(step374_test PRIVATE src) +target_link_libraries(step374_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/Pipeline.h b/editor/src/Pipeline.h index 727a96f..8c05b44 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -131,6 +131,10 @@ public: auto pr = CommonLispParser::parseCommonLispWithDiagnostics(source); diags = std::move(pr.diagnostics); return std::move(pr.module); + } else if (language == "scheme" || language == "scm") { + auto pr = SchemeParser::parseSchemeWithDiagnostics(source); + diags = std::move(pr.diagnostics); + return std::move(pr.module); } return nullptr; } diff --git a/editor/src/ast/Parser.h b/editor/src/ast/Parser.h index 171bd40..2ff44ac 100644 --- a/editor/src/ast/Parser.h +++ b/editor/src/ast/Parser.h @@ -167,3 +167,4 @@ private: #include "ast/CParser.h" #include "ast/WatParser.h" #include "ast/CommonLispParser.h" +#include "ast/SchemeParser.h" diff --git a/editor/src/ast/SchemeParser.h b/editor/src/ast/SchemeParser.h new file mode 100644 index 0000000..bfd3598 --- /dev/null +++ b/editor/src/ast/SchemeParser.h @@ -0,0 +1,461 @@ +#pragma once + +#include "ASTNode.h" +#include "Module.h" +#include "Function.h" +#include "Variable.h" +#include "Parameter.h" +#include "Type.h" +#include "Statement.h" +#include "Expression.h" +#include "AsyncNodes.h" +#include "PreprocessorNodes.h" +#include "Annotation.h" +#include "../ast/Parser.h" +#include +#include +#include + +class SchemeParser { +public: + static std::unique_ptr parseScheme(const std::string& source) { + ParseResult result = parseSchemeWithDiagnostics(source); + return std::move(result.module); + } + + static ParseResult parseSchemeWithDiagnostics(const std::string& source) { + ParseResult result; + result.module = std::make_unique(); + result.module->id = IdGenerator::next("mod"); + result.module->name = "parsed_scheme_module"; + result.module->targetLanguage = "scheme"; + + std::vector forms = parseForms(source, result.diagnostics); + for (const auto& form : forms) { + convertTopLevelForm(form, result.module.get()); + } + return result; + } + +private: + struct SExpr { + bool isList = false; + std::string atom; + std::vector list; + }; + + static bool isWhitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + static bool isDelimiter(char c) { + return isWhitespace(c) || c == '(' || c == ')' || c == '\'' || c == '`' || c == ';'; + } + + static bool isInteger(const std::string& s) { + if (s.empty()) return false; + size_t i = (s[0] == '-' || s[0] == '+') ? 1 : 0; + if (i >= s.size()) return false; + for (; i < s.size(); ++i) { + if (!std::isdigit(static_cast(s[i]))) return false; + } + return true; + } + + static bool isFloat(const std::string& s) { + if (s.empty()) return false; + bool seenDot = false; + bool seenDigit = false; + size_t i = (s[0] == '-' || s[0] == '+') ? 1 : 0; + if (i >= s.size()) return false; + for (; i < s.size(); ++i) { + char c = s[i]; + if (std::isdigit(static_cast(c))) { + seenDigit = true; + } else if (c == '.' && !seenDot) { + seenDot = true; + } else { + return false; + } + } + return seenDigit && seenDot; + } + + static bool isStringToken(const std::string& s) { + return s.size() >= 2 && s.front() == '"' && s.back() == '"'; + } + + static std::string canonicalHead(const std::string& symbol) { + size_t pos = symbol.rfind(':'); + if (pos == std::string::npos) return symbol; + return symbol.substr(pos + 1); + } + + static std::string atomText(const SExpr& expr) { + return expr.isList ? "" : expr.atom; + } + + static std::string headSymbol(const SExpr& expr) { + if (!expr.isList || expr.list.empty() || expr.list[0].isList) return ""; + return canonicalHead(expr.list[0].atom); + } + + static void skipWhitespaceAndComments(const std::string& source, size_t& pos) { + while (pos < source.size()) { + if (isWhitespace(source[pos])) { + ++pos; + continue; + } + if (source[pos] == ';') { + while (pos < source.size() && source[pos] != '\n') ++pos; + continue; + } + break; + } + } + + static std::string parseStringToken(const std::string& source, size_t& pos, + std::vector& diags) { + std::string out; + out.push_back('"'); + ++pos; + bool escaped = false; + while (pos < source.size()) { + char c = source[pos++]; + out.push_back(c); + if (escaped) { + escaped = false; + continue; + } + if (c == '\\') { + escaped = true; + } else if (c == '"') { + return out; + } + } + diags.push_back({1, 1, "Unterminated string in Scheme source", "error"}); + return out; + } + + static std::string parseAtomToken(const std::string& source, size_t& pos) { + size_t start = pos; + while (pos < source.size() && !isDelimiter(source[pos])) ++pos; + return source.substr(start, pos - start); + } + + static SExpr makeQuotedForm(const SExpr& value) { + SExpr q; + q.isList = true; + SExpr quoteSym; + quoteSym.atom = "quote"; + q.list.push_back(quoteSym); + q.list.push_back(value); + return q; + } + + static SExpr parseSingleForm(const std::string& source, size_t& pos, + std::vector& diags) { + skipWhitespaceAndComments(source, pos); + if (pos >= source.size()) return SExpr{}; + + if (source[pos] == '(') { + ++pos; + SExpr list; + list.isList = true; + for (;;) { + skipWhitespaceAndComments(source, pos); + if (pos >= source.size()) { + diags.push_back({1, 1, "Unbalanced parentheses in Scheme source", "error"}); + return list; + } + if (source[pos] == ')') { + ++pos; + return list; + } + list.list.push_back(parseSingleForm(source, pos, diags)); + } + } + + if (source[pos] == ')') { + diags.push_back({1, 1, "Unmatched closing parenthesis in Scheme source", "error"}); + ++pos; + return SExpr{}; + } + + if (source[pos] == '\'' || source[pos] == '`') { + ++pos; + SExpr inner = parseSingleForm(source, pos, diags); + return makeQuotedForm(inner); + } + + if (source[pos] == '"') { + SExpr atom; + atom.atom = parseStringToken(source, pos, diags); + return atom; + } + + SExpr atom; + atom.atom = parseAtomToken(source, pos); + return atom; + } + + static std::vector parseForms(const std::string& source, + std::vector& diags) { + std::vector out; + size_t pos = 0; + while (pos < source.size()) { + skipWhitespaceAndComments(source, pos); + if (pos >= source.size()) break; + out.push_back(parseSingleForm(source, pos, diags)); + } + return out; + } + + static void appendExpressionStatement(ASTNode* owner, const std::string& role, ASTNode* expr) { + if (!expr) return; + auto* stmt = new ExpressionStatement(); + stmt->id = IdGenerator::next("exprstmt"); + stmt->setChild("expression", expr); + owner->addChild(role, stmt); + } + + static ASTNode* convertExpression(const SExpr& expr); + + static ASTNode* convertIf(const SExpr& expr) { + if (expr.list.size() < 3) return nullptr; + auto* node = new IfStatement(); + node->id = IdGenerator::next("if"); + node->setChild("condition", convertExpression(expr.list[1])); + appendExpressionStatement(node, "thenBranch", convertExpression(expr.list[2])); + if (expr.list.size() > 3) { + appendExpressionStatement(node, "elseBranch", convertExpression(expr.list[3])); + } + return node; + } + + static ASTNode* convertCond(const SExpr& expr) { + IfStatement* root = nullptr; + IfStatement* cur = nullptr; + for (size_t i = 1; i < expr.list.size(); ++i) { + const auto& clause = expr.list[i]; + if (!clause.isList || clause.list.empty()) continue; + auto* node = new IfStatement(); + node->id = IdGenerator::next("if"); + node->setChild("condition", convertExpression(clause.list[0])); + if (clause.list.size() > 1) { + appendExpressionStatement(node, "thenBranch", convertExpression(clause.list[1])); + } + if (!root) root = node; + if (cur) cur->addChild("elseBranch", node); + cur = node; + } + return root; + } + + static ASTNode* convertLambda(const SExpr& expr) { + auto* lambda = new LambdaExpression(); + lambda->id = IdGenerator::next("lambda"); + if (expr.list.size() > 1 && expr.list[1].isList) { + for (const auto& p : expr.list[1].list) { + if (p.isList || p.atom.empty()) continue; + auto* param = new Parameter(IdGenerator::next("param"), p.atom); + lambda->addChild("parameters", param); + } + } + for (size_t i = 2; i < expr.list.size(); ++i) { + appendExpressionStatement(lambda, "body", convertExpression(expr.list[i])); + } + return lambda; + } + + static ASTNode* convertLet(const SExpr& expr) { + auto* block = new Block(); + block->id = IdGenerator::next("blk"); + if (expr.list.size() > 1 && expr.list[1].isList) { + for (const auto& binding : expr.list[1].list) { + if (!binding.isList || binding.list.empty()) continue; + std::string name = atomText(binding.list[0]); + if (name.empty()) continue; + auto* v = new Variable(IdGenerator::next("var"), name); + if (binding.list.size() > 1) { + v->setChild("value", convertExpression(binding.list[1])); + } + block->addChild("statements", v); + } + } + for (size_t i = 2; i < expr.list.size(); ++i) { + appendExpressionStatement(block, "statements", convertExpression(expr.list[i])); + } + return block; + } + + static ASTNode* convertDo(const SExpr& expr) { + auto* loop = new ForLoop(); + loop->id = IdGenerator::next("for"); + loop->iteratorName = "do"; + + if (expr.list.size() > 1 && expr.list[1].isList && !expr.list[1].list.empty()) { + const auto& firstBinding = expr.list[1].list[0]; + if (firstBinding.isList && !firstBinding.list.empty()) { + loop->iteratorName = atomText(firstBinding.list[0]); + if (firstBinding.list.size() > 1) { + loop->setChild("iterable", convertExpression(firstBinding.list[1])); + } + } + } + + size_t bodyStart = 3; + if (expr.list.size() > 2 && expr.list[2].isList && expr.list[2].list.size() > 1) { + appendExpressionStatement(loop, "body", convertExpression(expr.list[2].list[1])); + } + for (size_t i = bodyStart; i < expr.list.size(); ++i) { + appendExpressionStatement(loop, "body", convertExpression(expr.list[i])); + } + return loop; + } + + static ASTNode* convertBegin(const SExpr& expr) { + auto* block = new Block(); + block->id = IdGenerator::next("blk"); + for (size_t i = 1; i < expr.list.size(); ++i) { + appendExpressionStatement(block, "statements", convertExpression(expr.list[i])); + } + return block; + } + + static ASTNode* convertCall(const SExpr& expr) { + auto* call = new FunctionCall(); + call->id = IdGenerator::next("call"); + + std::string head = headSymbol(expr); + if (head == "call-with-current-continuation" || head == "call/cc") { + call->functionName = head; + auto* exec = new ExecAnnotation(); + exec->id = IdGenerator::next("anno"); + exec->mode = "continuation"; + call->addChild("annotations", exec); + } else { + call->functionName = atomText(expr.list[0]); + } + + size_t start = 1; + for (size_t i = start; i < expr.list.size(); ++i) { + ASTNode* arg = convertExpression(expr.list[i]); + if (arg) call->addChild("arguments", arg); + } + return call; + } + + static ASTNode* convertValues(const SExpr& expr) { + auto* call = new FunctionCall(); + call->id = IdGenerator::next("call"); + call->functionName = "values"; + for (size_t i = 1; i < expr.list.size(); ++i) { + ASTNode* arg = convertExpression(expr.list[i]); + if (arg) call->addChild("arguments", arg); + } + + auto* contract = new ContractAnnotation(); + contract->id = IdGenerator::next("anno"); + contract->returnShape = "multiple-values"; + call->addChild("annotations", contract); + return call; + } + + static ASTNode* convertQuote(const SExpr& expr) { + ASTNode* quoted = (expr.list.size() > 1) ? convertExpression(expr.list[1]) : nullptr; + if (!quoted) quoted = new VariableReference(IdGenerator::next("ref"), "nil"); + auto* meta = new MetaAnnotation(); + meta->id = IdGenerator::next("anno"); + meta->state = "quoted"; + quoted->addChild("annotations", meta); + return quoted; + } + + static void convertDefineFunction(const SExpr& form, Module* module) { + if (form.list.size() < 3 || !form.list[1].isList || form.list[1].list.empty()) return; + std::string name = atomText(form.list[1].list[0]); + auto* fn = new Function(IdGenerator::next("fn"), name); + + for (size_t i = 1; i < form.list[1].list.size(); ++i) { + std::string pname = atomText(form.list[1].list[i]); + if (pname.empty()) continue; + fn->addChild("parameters", new Parameter(IdGenerator::next("param"), pname)); + } + + for (size_t i = 2; i < form.list.size(); ++i) { + appendExpressionStatement(fn, "body", convertExpression(form.list[i])); + } + module->addChild("functions", fn); + } + + static void convertDefineVariable(const SExpr& form, Module* module) { + if (form.list.size() < 2) return; + std::string name = atomText(form.list[1]); + auto* var = new Variable(IdGenerator::next("var"), name); + if (form.list.size() > 2) { + var->setChild("value", convertExpression(form.list[2])); + } + module->addChild("variables", var); + } + + static void convertDefineSyntax(const SExpr& form, Module* module) { + if (form.list.size() < 2) return; + auto* macro = new MacroDefinition(IdGenerator::next("mac"), atomText(form.list[1])); + macro->isFunctionLike = true; + macro->body = "syntax-rules"; + + auto* meta = new MetaAnnotation(); + meta->id = IdGenerator::next("anno"); + meta->state = "hygienic"; + macro->addChild("annotations", meta); + + module->addChild("statements", macro); + } + + static void convertTopLevelForm(const SExpr& form, Module* module) { + if (!form.isList || form.list.empty()) return; + std::string head = headSymbol(form); + + if (head == "define") { + if (form.list.size() > 1 && form.list[1].isList) { + convertDefineFunction(form, module); + } else { + convertDefineVariable(form, module); + } + return; + } + + if (head == "define-syntax") { + convertDefineSyntax(form, module); + return; + } + + appendExpressionStatement(module, "statements", convertExpression(form)); + } +}; + +inline ASTNode* SchemeParser::convertExpression(const SExpr& expr) { + if (!expr.isList) { + if (expr.atom.empty()) return nullptr; + if (isInteger(expr.atom)) return new IntegerLiteral(IdGenerator::next("int"), std::stoi(expr.atom)); + if (isFloat(expr.atom)) return new FloatLiteral(IdGenerator::next("flt"), expr.atom); + if (isStringToken(expr.atom)) { + return new StringLiteral(IdGenerator::next("str"), expr.atom.substr(1, expr.atom.size() - 2)); + } + if (expr.atom == "#t" || expr.atom == "t") return new BooleanLiteral(IdGenerator::next("bool"), true); + if (expr.atom == "#f" || expr.atom == "nil") return new BooleanLiteral(IdGenerator::next("bool"), false); + return new VariableReference(IdGenerator::next("ref"), expr.atom); + } + + std::string head = headSymbol(expr); + if (head == "if") return convertIf(expr); + if (head == "cond") return convertCond(expr); + if (head == "lambda") return convertLambda(expr); + if (head == "let" || head == "let*" || head == "letrec") return convertLet(expr); + if (head == "do") return convertDo(expr); + if (head == "begin") return convertBegin(expr); + if (head == "values") return convertValues(expr); + if (head == "quote") return convertQuote(expr); + return convertCall(expr); +} diff --git a/editor/tests/step374_test.cpp b/editor/tests/step374_test.cpp new file mode 100644 index 0000000..4bd14e6 --- /dev/null +++ b/editor/tests/step374_test.cpp @@ -0,0 +1,208 @@ +// Step 374: Scheme Parser (12 tests) + +#include +#include +#include +#include "Pipeline.h" +#include "ast/SchemeParser.h" + +static bool hasBodyNode(const Function* fn, const std::string& conceptType) { + for (auto* stmt : fn->getChildren("body")) { + if (stmt->conceptType == conceptType) return true; + if (stmt->conceptType == "ExpressionStatement") { + ASTNode* expr = stmt->getChild("expression"); + if (expr && expr->conceptType == conceptType) return true; + } + } + return false; +} + +int main() { + int passed = 0; + + // Test 1: define function parsing + { + auto mod = SchemeParser::parseScheme("(define (add x y) (+ x y))"); + assert(mod->targetLanguage == "scheme"); + assert(mod->getChildren("functions").size() == 1); + auto* fn = static_cast(mod->getChildren("functions")[0]); + assert(fn->name == "add"); + assert(fn->getChildren("parameters").size() == 2); + std::cout << "Test 1 PASSED: define function parsing\n"; + passed++; + } + + // Test 2: define variable parsing + { + auto mod = SchemeParser::parseScheme("(define answer 42)"); + assert(mod->getChildren("variables").size() == 1); + auto* var = static_cast(mod->getChildren("variables")[0]); + assert(var->name == "answer"); + assert(var->getChild("value") != nullptr); + std::cout << "Test 2 PASSED: define variable parsing\n"; + passed++; + } + + // Test 3: lambda parsing + { + auto mod = SchemeParser::parseScheme("(define (mk) (lambda (x) (+ x 1)))"); + auto* fn = static_cast(mod->getChildren("functions")[0]); + assert(hasBodyNode(fn, "LambdaExpression")); + std::cout << "Test 3 PASSED: lambda parsing\n"; + passed++; + } + + // Test 4: let / let* / letrec parsing + { + std::string src = + "(define (a) (let ((x 1)) x))\n" + "(define (b) (let* ((x 1) (y 2)) (+ x y)))\n" + "(define (c) (letrec ((f (lambda (n) n))) (f 1)))"; + auto mod = SchemeParser::parseScheme(src); + auto* fa = static_cast(mod->getChildren("functions")[0]); + auto* fb = static_cast(mod->getChildren("functions")[1]); + auto* fc = static_cast(mod->getChildren("functions")[2]); + assert(hasBodyNode(fa, "Block")); + assert(hasBodyNode(fb, "Block")); + assert(hasBodyNode(fc, "Block")); + std::cout << "Test 4 PASSED: let variants parsing\n"; + passed++; + } + + // Test 5: if and cond parsing + { + std::string src = + "(define (s x) (if x 1 0))\n" + "(define (c x) (cond ((> x 0) 1) ((< x 0) -1) (else 0)))"; + auto mod = SchemeParser::parseScheme(src); + auto* f1 = static_cast(mod->getChildren("functions")[0]); + auto* f2 = static_cast(mod->getChildren("functions")[1]); + assert(hasBodyNode(f1, "IfStatement")); + assert(hasBodyNode(f2, "IfStatement")); + std::cout << "Test 5 PASSED: if/cond parsing\n"; + passed++; + } + + // Test 6: do loop parsing + { + std::string src = "(define (loopn n) (do ((i 0 (+ i 1))) ((> i n) i) (display i)))"; + auto mod = SchemeParser::parseScheme(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + assert(hasBodyNode(fn, "ForLoop")); + std::cout << "Test 6 PASSED: do loop parsing\n"; + passed++; + } + + // Test 7: call/cc continuation annotation + { + std::string src = + "(define (cc f) (call-with-current-continuation f))\n" + "(define (cc2 f) (call/cc f))"; + auto mod = SchemeParser::parseScheme(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + bool hasContinuation = false; + for (auto* stmt : fn->getChildren("body")) { + ASTNode* expr = stmt->getChild("expression"); + if (!expr || expr->conceptType != "FunctionCall") continue; + for (auto* anno : expr->getChildren("annotations")) { + if (anno->conceptType == "ExecAnnotation") { + auto* ex = static_cast(anno); + if (ex->mode == "continuation") hasContinuation = true; + } + } + } + assert(hasContinuation); + std::cout << "Test 7 PASSED: call/cc continuation annotation\n"; + passed++; + } + + // Test 8: define-syntax -> macro + hygienic meta + { + std::string src = "(define-syntax when1 (syntax-rules () ((_ t b) (if t b #f))))"; + auto mod = SchemeParser::parseScheme(src); + bool foundMacro = false; + bool hygienic = false; + for (auto* stmt : mod->getChildren("statements")) { + if (stmt->conceptType != "MacroDefinition") continue; + foundMacro = true; + for (auto* anno : stmt->getChildren("annotations")) { + if (anno->conceptType == "MetaAnnotation") { + auto* m = static_cast(anno); + if (m->state == "hygienic") hygienic = true; + } + } + } + assert(foundMacro && hygienic); + std::cout << "Test 8 PASSED: define-syntax hygienic macro mapping\n"; + passed++; + } + + // Test 9: values multi-return mapping + { + std::string src = "(define (pair x) (values x (+ x 1)))"; + auto mod = SchemeParser::parseScheme(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + bool hasContract = false; + for (auto* stmt : fn->getChildren("body")) { + ASTNode* expr = stmt->getChild("expression"); + if (!expr || expr->conceptType != "FunctionCall") continue; + for (auto* anno : expr->getChildren("annotations")) { + if (anno->conceptType == "ContractAnnotation") hasContract = true; + } + } + assert(hasContract); + std::cout << "Test 9 PASSED: values -> Contract annotation\n"; + passed++; + } + + // Test 10: begin form maps to sequence block + { + std::string src = "(define (f) (begin (display 1) (display 2) 3))"; + auto mod = SchemeParser::parseScheme(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + assert(hasBodyNode(fn, "Block")); + std::cout << "Test 10 PASSED: begin -> block sequence\n"; + passed++; + } + + // Test 11: deep nested s-expressions + { + std::string src = "(define (deep x) (a (b (c (d (e (f (g x))))))))"; + auto mod = SchemeParser::parseScheme(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + assert(hasBodyNode(fn, "FunctionCall")); + std::cout << "Test 11 PASSED: deep nesting parse\n"; + passed++; + } + + // Test 12: mixed source + pipeline route + diagnostics + { + std::string src = + "(define x 1)\n" + "(define (id y) y)\n" + "(define-syntax m (syntax-rules () ((_ z) z)))\n"; + auto mod = SchemeParser::parseScheme(src); + assert(mod->getChildren("variables").size() == 1); + assert(mod->getChildren("functions").size() == 1); + bool macroFound = false; + for (auto* stmt : mod->getChildren("statements")) { + if (stmt->conceptType == "MacroDefinition") macroFound = true; + } + assert(macroFound); + + Pipeline p; + std::vector diags; + auto parsedScheme = p.parse("(define (f x) x)", "scheme", diags); + auto parsedScm = p.parse("(define (f x) x)", "scm", diags); + assert(parsedScheme != nullptr && parsedScm != nullptr); + + auto malformed = SchemeParser::parseSchemeWithDiagnostics("(define (f x) (+ x 1)"); + assert(!malformed.diagnostics.empty()); + std::cout << "Test 12 PASSED: mixed forms + pipeline route + diagnostics\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index 5cbf229..915d07a 100644 --- a/progress.md +++ b/progress.md @@ -2565,6 +2565,52 @@ artifact compatibility with Semanno and compact AST tooling. - `editor/tests/step373_test.cpp` remains within project file-size guidance for test artifacts (`147` lines) +### Step 374: Scheme Parser +**Status:** PASS (12/12 tests) + +Added a dedicated Scheme parser with S-expression parsing and Scheme-specific +form mapping into the Whetstone AST, including continuation and hygienic macro +signals. + +**Files created:** +- `editor/src/ast/SchemeParser.h` — Scheme parse support: + - recursive S-expression reader with Scheme comment and quote handling + - top-level mapping for `define` (function/value) and `define-syntax` + - expression mapping for `lambda`, `let`/`let*`/`letrec`, `if`, `cond`, + `do`, `begin`, `call-with-current-continuation`/`call/cc`, `quote`, + and `values` + - continuation call mapping with `Exec(mode=continuation)` annotation + - `define-syntax` mapping to `MacroDefinition` with hygienic `Meta` signal +- `editor/tests/step374_test.cpp` — 12 tests covering: + 1. `define` function parsing + 2. `define` variable parsing + 3. `lambda` parsing + 4. `let` variants mapping + 5. `if`/`cond` mapping + 6. `do` loop mapping + 7. `call/cc` continuation annotation mapping + 8. `define-syntax` hygienic macro mapping + 9. `values` contract mapping + 10. `begin` sequence mapping + 11. deep nesting parsing + 12. mixed forms + pipeline scheme/scm routing + malformed diagnostics + +**Files modified:** +- `editor/src/ast/Parser.h` — include `ast/SchemeParser.h` +- `editor/src/Pipeline.h` — add parse routing for `\"scheme\"` and `\"scm\"` +- `editor/CMakeLists.txt` — `step374_test` target + +**Verification run:** +- `step374_test` — PASS (12/12) new step coverage +- `step373_test` — PASS (8/8) regression coverage +- `step372_test` — PASS (12/12) regression coverage +- `step371_test` — PASS (12/12) regression coverage +- `step370_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ast/SchemeParser.h` is within header size limit + (`461` lines <= `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)