Step 405: add standalone F# parser and parse routing
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
265
editor/src/ast/FSharpParser.h
Normal file
265
editor/src/ast/FSharpParser.h
Normal file
@@ -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 <string>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
class FSharpParser {
|
||||
public:
|
||||
static std::unique_ptr<Module> parseFSharp(const std::string& source) {
|
||||
auto module = std::make_unique<Module>();
|
||||
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<std::string> 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<std::string> splitWhitespace(const std::string& s) {
|
||||
std::vector<std::string> 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);
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
220
editor/tests/step405_test.cpp
Normal file
220
editor/tests/step405_test.cpp
Normal file
@@ -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 <iostream>
|
||||
#include <string>
|
||||
|
||||
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<Function*>(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<Variable*>(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<EnumDeclaration*>(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<ClassDeclaration*>(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<AsyncFunction*>(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<NamespaceDeclaration*>(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<ParseDiagnostic> 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;
|
||||
}
|
||||
54
progress.md
54
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)
|
||||
|
||||
Reference in New Issue
Block a user