From a7597a649c987d804ab61d78cda68fe246638af7 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 15:22:55 -0700 Subject: [PATCH] Step 405: add standalone F# parser and parse routing --- editor/CMakeLists.txt | 9 ++ editor/src/Pipeline.h | 4 + editor/src/ast/FSharpParser.h | 265 ++++++++++++++++++++++++++++++++++ editor/src/ast/Parser.h | 1 + editor/tests/step405_test.cpp | 220 ++++++++++++++++++++++++++++ progress.md | 54 +++++++ 6 files changed, 553 insertions(+) create mode 100644 editor/src/ast/FSharpParser.h create mode 100644 editor/tests/step405_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 7f61493..84651b3 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2524,4 +2524,13 @@ target_link_libraries(step404_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step405_test tests/step405_test.cpp) +target_include_directories(step405_test PRIVATE src) +target_link_libraries(step405_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 9920c46..3c5ae9a 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -118,6 +118,10 @@ public: auto pr = CSharpParser::parseCSharpWithDiagnostics(source); diags = std::move(pr.diagnostics); return std::move(pr.module); + } else if (language == "fsharp" || language == "f#" || language == "fs") { + auto pr = FSharpParser::parseFSharpWithDiagnostics(source); + diags = std::move(pr.diagnostics); + return std::move(pr.module); } else if (language == "c") { auto pr = CParser::parseCWithDiagnostics(source); diags = std::move(pr.diagnostics); diff --git a/editor/src/ast/FSharpParser.h b/editor/src/ast/FSharpParser.h new file mode 100644 index 0000000..5d058b6 --- /dev/null +++ b/editor/src/ast/FSharpParser.h @@ -0,0 +1,265 @@ +#pragma once + +#include "ASTNode.h" +#include "Module.h" +#include "Function.h" +#include "Variable.h" +#include "Statement.h" +#include "Expression.h" +#include "Annotation.h" +#include "ClassDeclaration.h" +#include "EnumNamespaceNodes.h" +#include "AsyncNodes.h" +#include "../ast/Parser.h" + +#include +#include +#include + +class FSharpParser { +public: + static std::unique_ptr parseFSharp(const std::string& source) { + auto module = std::make_unique(); + module->id = IdGenerator::next("mod"); + module->name = "parsed_fsharp_module"; + module->targetLanguage = "fsharp"; + parseTopLevel(source, module.get()); + return module; + } + + static ParseResult parseFSharpWithDiagnostics(const std::string& source) { + ParseResult result; + result.module = parseFSharp(source); + return result; + } + +private: + static void parseTopLevel(const std::string& source, Module* module) { + std::istringstream stream(source); + std::string line; + + bool inUnionType = false; + EnumDeclaration* currentUnion = nullptr; + + while (std::getline(stream, line)) { + std::string trimmed = trim(line); + if (trimmed.empty()) continue; + if (startsWith(trimmed, "//")) continue; + if (startsWith(trimmed, "(*")) continue; + + const bool topLevel = indentOf(line) == 0; + + if (topLevel && startsWith(trimmed, "module ")) { + auto* ns = new NamespaceDeclaration(); + ns->id = IdGenerator::next("ns"); + ns->name = trim(trimmed.substr(7)); + module->addChild("statements", ns); + inUnionType = false; + currentUnion = nullptr; + continue; + } + + if (topLevel && startsWith(trimmed, "type ")) { + inUnionType = false; + currentUnion = nullptr; + parseTypeDecl(trimmed, module, inUnionType, currentUnion); + continue; + } + + if (inUnionType && startsWith(trimmed, "|") && currentUnion) { + parseUnionCase(trimmed, currentUnion); + continue; + } + + if (topLevel && startsWith(trimmed, "let ")) { + parseLetDecl(trimmed, module); + continue; + } + + if (trimmed.find("match ") != std::string::npos && + trimmed.find(" with") != std::string::npos) { + auto* ifStmt = new IfStatement(); + ifStmt->id = IdGenerator::next("if"); + module->addChild("statements", ifStmt); + continue; + } + + if (trimmed.find("|>") != std::string::npos) { + parsePipelineCalls(trimmed, module); + continue; + } + } + } + + static void parseTypeDecl(const std::string& line, Module* module, + bool& inUnionType, EnumDeclaration*& currentUnion) { + const auto eqPos = line.find('='); + if (eqPos == std::string::npos) return; + + const std::string left = trim(line.substr(0, eqPos)); + const std::string rhs = trim(line.substr(eqPos + 1)); + const std::string typeName = trim(left.substr(5)); // after "type " + if (typeName.empty()) return; + + if (startsWith(rhs, "{")) { + auto* cls = new ClassDeclaration(); + cls->id = IdGenerator::next("cls"); + cls->name = typeName; + parseRecordFields(rhs, cls); + module->addChild("classes", cls); + inUnionType = false; + currentUnion = nullptr; + return; + } + + if (startsWith(rhs, "|")) { + auto* en = new EnumDeclaration(); + en->id = IdGenerator::next("enum"); + en->name = typeName; + module->addChild("statements", en); + currentUnion = en; + inUnionType = true; + parseUnionCase(rhs, en); + return; + } + + // Multiline union form: + // type Shape = + // | Circle + // | Rectangle of int + if (rhs.empty()) { + auto* en = new EnumDeclaration(); + en->id = IdGenerator::next("enum"); + en->name = typeName; + module->addChild("statements", en); + currentUnion = en; + inUnionType = true; + return; + } + } + + static void parseRecordFields(const std::string& rhs, ClassDeclaration* cls) { + auto start = rhs.find('{'); + auto end = rhs.rfind('}'); + if (start == std::string::npos || end == std::string::npos || end <= start) return; + + std::string fields = rhs.substr(start + 1, end - start - 1); + std::stringstream ss(fields); + std::string token; + while (std::getline(ss, token, ';')) { + std::string f = trim(token); + if (f.empty()) continue; + auto colon = f.find(':'); + std::string name = trim(colon == std::string::npos ? f : f.substr(0, colon)); + if (name.empty()) continue; + auto* var = new Variable(); + var->id = IdGenerator::next("fld"); + var->name = name; + cls->addChild("fields", var); + } + } + + static void parseUnionCase(const std::string& line, EnumDeclaration* en) { + if (!en) return; + std::string caseText = trim(line); + if (!caseText.empty() && caseText[0] == '|') { + caseText = trim(caseText.substr(1)); + } + auto ofPos = caseText.find(" of "); + std::string caseName = trim(ofPos == std::string::npos ? caseText : caseText.substr(0, ofPos)); + if (caseName.empty()) return; + auto* member = new EnumMember(); + member->id = IdGenerator::next("case"); + member->name = caseName; + en->addChild("members", member); + } + + static void parseLetDecl(const std::string& line, Module* module) { + std::string rest = trim(line.substr(4)); // after "let " + + bool isMutable = false; + if (startsWith(rest, "mutable ")) { + isMutable = true; + rest = trim(rest.substr(8)); + } + + auto eqPos = rest.find('='); + if (eqPos == std::string::npos) return; + + std::string left = trim(rest.substr(0, eqPos)); + std::string right = trim(rest.substr(eqPos + 1)); + if (left.empty()) return; + + std::vector tokens = splitWhitespace(left); + if (tokens.empty()) return; + + if (right.find("async {") != std::string::npos) { + auto* fn = new AsyncFunction(); + fn->id = IdGenerator::next("fn"); + fn->name = tokens[0]; + module->addChild("functions", fn); + return; + } + + if (tokens.size() >= 2) { + auto* fn = new Function(); + fn->id = IdGenerator::next("fn"); + fn->name = tokens[0]; + module->addChild("functions", fn); + return; + } + + auto* var = new Variable(); + var->id = IdGenerator::next("var"); + var->name = tokens[0]; + if (isMutable) { + auto* mut = new MutAnnotation(); + mut->id = IdGenerator::next("mut"); + mut->depth = "shallow"; + var->addChild("annotations", mut); + } + module->addChild("variables", var); + } + + static void parsePipelineCalls(const std::string& line, Module* module) { + size_t pos = 0; + while (true) { + pos = line.find("|>", pos); + if (pos == std::string::npos) break; + auto* call = new FunctionCall(); + call->id = IdGenerator::next("call"); + call->functionName = "pipeline"; + module->addChild("statements", call); + pos += 2; + } + } + + static std::vector splitWhitespace(const std::string& s) { + std::vector out; + std::stringstream ss(s); + std::string tok; + while (ss >> tok) out.push_back(tok); + return out; + } + + static bool startsWith(const std::string& s, const std::string& prefix) { + return s.rfind(prefix, 0) == 0; + } + + static int indentOf(const std::string& line) { + int indent = 0; + for (char c : line) { + if (c == ' ') indent++; + else if (c == '\t') indent += 4; + else break; + } + return indent; + } + + static std::string trim(const std::string& s) { + auto start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + auto end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + } +}; diff --git a/editor/src/ast/Parser.h b/editor/src/ast/Parser.h index 2ff44ac..ce87ff8 100644 --- a/editor/src/ast/Parser.h +++ b/editor/src/ast/Parser.h @@ -164,6 +164,7 @@ private: // Standalone parsers (not tree-sitter fragment includes) #include "ast/KotlinParser.h" #include "ast/CSharpParser.h" +#include "ast/FSharpParser.h" #include "ast/CParser.h" #include "ast/WatParser.h" #include "ast/CommonLispParser.h" diff --git a/editor/tests/step405_test.cpp b/editor/tests/step405_test.cpp new file mode 100644 index 0000000..af272a2 --- /dev/null +++ b/editor/tests/step405_test.cpp @@ -0,0 +1,220 @@ +// Step 405: F# Parser Tests (12 tests) + +#include "ast/FSharpParser.h" +#include "Pipeline.h" +#include "ast/Function.h" +#include "ast/Variable.h" +#include "ast/EnumNamespaceNodes.h" +#include "ast/ClassDeclaration.h" +#include "ast/Statement.h" +#include "ast/AsyncNodes.h" +#include "ast/Expression.h" + +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +// 1. let name params = body -> Function +void test_let_function() { + TEST(let_function); + auto mod = FSharpParser::parseFSharp("let add x y = x + y\n"); + CHECK(mod != nullptr, "module is null"); + auto fns = mod->getChildren("functions"); + CHECK(fns.size() == 1, "expected 1 function"); + CHECK(fns[0]->conceptType == "Function", "expected Function"); + auto* fn = static_cast(fns[0]); + CHECK(fn->name == "add", "expected function name add"); + PASS(); +} + +// 2. let mutable x = 5 -> Variable with MutAnnotation +void test_let_mutable_variable() { + TEST(let_mutable_variable); + auto mod = FSharpParser::parseFSharp("let mutable counter = 5\n"); + CHECK(mod != nullptr, "module is null"); + auto vars = mod->getChildren("variables"); + CHECK(vars.size() == 1, "expected 1 variable"); + auto* var = static_cast(vars[0]); + CHECK(var->name == "counter", "expected variable name counter"); + CHECK(!var->getChildren("annotations").empty(), "expected mutable annotation"); + PASS(); +} + +// 3. discriminated union -> EnumDeclaration +void test_discriminated_union() { + TEST(discriminated_union); + auto mod = FSharpParser::parseFSharp( + "type Shape =\n" + "| Circle\n" + "| Rectangle of int\n"); + CHECK(mod != nullptr, "module is null"); + auto stmts = mod->getChildren("statements"); + EnumDeclaration* en = nullptr; + for (auto* s : stmts) { + if (s->conceptType == "EnumDeclaration") { + en = static_cast(s); + break; + } + } + CHECK(en != nullptr, "expected enum declaration"); + CHECK(en->name == "Shape", "expected enum name Shape"); + CHECK(en->getChildren("members").size() == 2, "expected 2 union cases"); + PASS(); +} + +// 4. record type -> ClassDeclaration with fields +void test_record_type() { + TEST(record_type); + auto mod = FSharpParser::parseFSharp("type Person = { name: string; age: int }\n"); + CHECK(mod != nullptr, "module is null"); + auto classes = mod->getChildren("classes"); + CHECK(classes.size() == 1, "expected 1 class"); + auto* cls = static_cast(classes[0]); + CHECK(cls->name == "Person", "expected class Person"); + CHECK(cls->getChildren("fields").size() == 2, "expected 2 fields"); + PASS(); +} + +// 5. match x with -> IfStatement chain marker +void test_match_mapping() { + TEST(match_mapping); + auto mod = FSharpParser::parseFSharp( + "let classify x =\n" + " match x with\n" + " | 0 -> \"zero\"\n" + " | _ -> \"other\"\n"); + CHECK(mod != nullptr, "module is null"); + auto stmts = mod->getChildren("statements"); + bool foundIf = false; + for (auto* s : stmts) { + if (s->conceptType == "IfStatement") foundIf = true; + } + CHECK(foundIf, "expected IfStatement marker for match"); + PASS(); +} + +// 6. async { ... } -> AsyncFunction +void test_async_computation() { + TEST(async_computation); + auto mod = FSharpParser::parseFSharp("let fetchData = async { return 42 }\n"); + CHECK(mod != nullptr, "module is null"); + auto fns = mod->getChildren("functions"); + CHECK(fns.size() == 1, "expected 1 function"); + CHECK(fns[0]->conceptType == "AsyncFunction", "expected AsyncFunction"); + auto* fn = static_cast(fns[0]); + CHECK(fn->name == "fetchData", "expected async function name fetchData"); + PASS(); +} + +// 7. module Name -> NamespaceDeclaration +void test_module_namespace() { + TEST(module_namespace); + auto mod = FSharpParser::parseFSharp("module MyApp.Core\n"); + CHECK(mod != nullptr, "module is null"); + auto stmts = mod->getChildren("statements"); + NamespaceDeclaration* ns = nullptr; + for (auto* s : stmts) { + if (s->conceptType == "NamespaceDeclaration") { + ns = static_cast(s); + break; + } + } + CHECK(ns != nullptr, "expected namespace declaration"); + CHECK(ns->name == "MyApp.Core", "expected namespace MyApp.Core"); + PASS(); +} + +// 8. |> pipeline operators -> chained FunctionCalls +void test_pipeline_operator_calls() { + TEST(pipeline_operator_calls); + auto mod = FSharpParser::parseFSharp("value |> f |> g |> h\n"); + CHECK(mod != nullptr, "module is null"); + auto stmts = mod->getChildren("statements"); + int pipelineCalls = 0; + for (auto* s : stmts) { + if (s->conceptType == "FunctionCall") pipelineCalls++; + } + CHECK(pipelineCalls == 3, "expected 3 pipeline calls"); + PASS(); +} + +// 9. significant whitespace: indented let not top-level declaration +void test_significant_whitespace() { + TEST(significant_whitespace); + auto mod = FSharpParser::parseFSharp( + "let outer x =\n" + " let inner = x + 1\n" + " inner\n"); + CHECK(mod != nullptr, "module is null"); + auto fns = mod->getChildren("functions"); + auto vars = mod->getChildren("variables"); + CHECK(fns.size() == 1, "expected only top-level function"); + CHECK(vars.empty(), "expected no top-level variables from indented let"); + PASS(); +} + +// 10. parse with diagnostics entry point +void test_parse_with_diagnostics() { + TEST(parse_with_diagnostics); + auto result = FSharpParser::parseFSharpWithDiagnostics("let f x = x\n"); + CHECK(result.module != nullptr, "module is null"); + CHECK(result.module->getChildren("functions").size() == 1, "expected 1 function"); + PASS(); +} + +// 11. mixed top-level forms +void test_mixed_toplevel_forms() { + TEST(mixed_toplevel_forms); + auto mod = FSharpParser::parseFSharp( + "module Demo\n" + "type Status = | Ok | Error of string\n" + "type User = { id: int; name: string }\n" + "let mutable count = 0\n" + "let run value = value |> string\n"); + CHECK(mod != nullptr, "module is null"); + CHECK(mod->getChildren("classes").size() == 1, "expected 1 record class"); + CHECK(mod->getChildren("variables").size() == 1, "expected 1 variable"); + CHECK(mod->getChildren("functions").size() == 1, "expected 1 function"); + PASS(); +} + +// 12. Pipeline parse routing for fsharp/f#/fs +void test_pipeline_parse_routing() { + TEST(pipeline_parse_routing); + Pipeline p; + std::vector d1, d2, d3; + auto m1 = p.parse("let inc x = x + 1\n", "fsharp", d1); + auto m2 = p.parse("let inc x = x + 1\n", "f#", d2); + auto m3 = p.parse("let inc x = x + 1\n", "fs", d3); + CHECK(m1 != nullptr && m2 != nullptr && m3 != nullptr, "expected parse success for aliases"); + CHECK(m1->targetLanguage == "fsharp", "expected targetLanguage fsharp"); + CHECK(m2->targetLanguage == "fsharp", "expected alias targetLanguage fsharp"); + CHECK(m3->targetLanguage == "fsharp", "expected alias targetLanguage fsharp"); + PASS(); +} + +int main() { + std::cout << "Step 405: F# Parser Tests\n"; + + test_let_function(); // 1 + test_let_mutable_variable(); // 2 + test_discriminated_union(); // 3 + test_record_type(); // 4 + test_match_mapping(); // 5 + test_async_computation(); // 6 + test_module_namespace(); // 7 + test_pipeline_operator_calls(); // 8 + test_significant_whitespace(); // 9 + test_parse_with_diagnostics(); // 10 + test_mixed_toplevel_forms(); // 11 + test_pipeline_parse_routing(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 66f0330..8434691 100644 --- a/progress.md +++ b/progress.md @@ -3620,6 +3620,60 @@ from inferred complexity metadata, and explicit gap-reporting behavior. - `editor/src/MCPServer.h` (`1679` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`) +# Sprint 17 Progress — Language Batch 2 + +## Phase 17a: F# Parser + Generator + +### Step 405: F# Parser +**Status:** PASS (12/12 tests) + +Added a standalone F# parser covering core functional declarations and routing. +Parses top-level `let` function forms, mutable variables, discriminated unions, +record types, module declarations, async computation expressions, match markers, +and pipe-operator chains. + +**Files created:** +- `editor/src/ast/FSharpParser.h` — F# parse support: + - `parseFSharp(...)` and `parseFSharpWithDiagnostics(...)` + - `let name params = ...` → `Function` + - `let mutable x = ...` → `Variable` + `MutAnnotation` + - `type Name = | Case...` (single-line/multi-line) → `EnumDeclaration` + - `type Name = { field: Type; ... }` → `ClassDeclaration` with field children + - `module Name` → `NamespaceDeclaration` + - `match ... with` marker → `IfStatement` + - `|>` pipeline chains → `FunctionCall` markers + - indentation-aware top-level handling for significant-whitespace behavior +- `editor/tests/step405_test.cpp` — 12 tests covering: + 1. `let` function parsing + 2. mutable variable parsing + 3. discriminated union parsing + 4. record type parsing + 5. match mapping marker + 6. async computation parsing + 7. module/namespace parsing + 8. pipeline operator chain parsing + 9. significant-whitespace top-level behavior + 10. diagnostics entry point + 11. mixed top-level forms + 12. pipeline parse routing for `fsharp` / `f#` / `fs` + +**Files modified:** +- `editor/src/ast/Parser.h` — include `ast/FSharpParser.h` +- `editor/src/Pipeline.h` — parse routing for `fsharp`, `f#`, `fs` +- `editor/CMakeLists.txt` — `step405_test` target + +**Verification run:** +- `step405_test` — PASS (12/12) new step coverage +- `step404_test` — PASS (8/8) regression coverage +- `step403_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ast/FSharpParser.h` within header-size limit (`265` <= `600`) +- `editor/tests/step405_test.cpp` within test-file size guidance (`220` lines) +- Legacy oversized headers persist: + - `editor/src/MCPServer.h` (`1679` > `600`) + - `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)